Skip to content

Commit

Permalink
Improve rendering performance when pxPerSec is high (#909)
Browse files Browse the repository at this point in the history
* Adds an optional 'partialRender' parameter to enable
* Calculates and renders peaks only for current visible waveform
* Keeps track of currently calculated/rendered peaks to avoid
  duplicate calculation and only incremental scroll changes are rendered

Tested all combinations of Canvas/MultiCanvas and Wave/Bars rendering
at various zoom levels.
  • Loading branch information
elijah-taylor authored and katspaugh committed Jan 16, 2017
1 parent f9426cb commit 3d908c5
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 48 deletions.
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = function (grunt) {
'src/mediaelement.js',
'src/drawer.js',
'src/drawer.*.js',
'src/peakcache.js',
'src/html-init.js'
],
dest: 'dist/wavesurfer.js'
Expand Down
4 changes: 2 additions & 2 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = function (config) {
'jasmine-matchers'
],
hostname: 'localhost',
post: 9876,
port: 9876,
singleRun: true,
autoWatch: false,
files: [
Expand Down Expand Up @@ -54,4 +54,4 @@ module.exports = function (config) {
}

config.set(configuration);
};
};
97 changes: 97 additions & 0 deletions spec/peakcache.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
describe('peakcache', function() {
var peakcache;
var test_length = 200;
var test_length2 = 300;
var test_start = 50;
var test_end = 100;
var test_start2 = 100;
var test_end2 = 120;
var test_start3 = 120;
var test_end3 = 150;

var window_size = 20;

function __createPeakCache() {
peakcache = Object.create(WaveSurfer.PeakCache);
peakcache.init();
}

beforeEach(function (done) {
__createPeakCache();
done();
});

it('empty cache returns full range', function() {
var newranges = peakcache.addRangeToPeakCache(test_length, test_start, test_end);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_start);
expect(newranges[0][1]).toEqual(test_end);
});

it('different length clears cache', function() {
peakcache.addRangeToPeakCache(test_length, test_start, test_end);
var newranges = peakcache.addRangeToPeakCache(test_length2, test_start, test_end);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_start);
expect(newranges[0][1]).toEqual(test_end);
});

it('consecutive calls return no ranges', function() {
peakcache.addRangeToPeakCache(test_length, test_start, test_end);
var newranges = peakcache.addRangeToPeakCache(test_length, test_start, test_end);
expect(newranges.length).toEqual(0);
});

it('sliding window returns window sized range', function() {
var newranges = peakcache.addRangeToPeakCache(test_length, test_start, test_end);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_start);
expect(newranges[0][1]).toEqual(test_end);
var newranges = peakcache.addRangeToPeakCache(test_length, test_start + window_size, test_end + window_size);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_end);
expect(newranges[0][1]).toEqual(test_end + window_size);
var newranges = peakcache.addRangeToPeakCache(test_length, test_start + window_size * 2, test_end + window_size * 2);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_end + window_size);
expect(newranges[0][1]).toEqual(test_end + window_size * 2);
});

it('disjoint set creates two ranges', function() {
peakcache.addRangeToPeakCache(test_length, test_start, test_end);
peakcache.addRangeToPeakCache(test_length, test_start3, test_end3);
var ranges = peakcache.getCacheRanges();
expect(ranges.length).toEqual(2);
expect(ranges[0][0]).toEqual(test_start);
expect(ranges[0][1]).toEqual(test_end);
expect(ranges[1][0]).toEqual(test_start3);
expect(ranges[1][1]).toEqual(test_end3);
});

it('filling in disjoint sets coalesces', function() {
peakcache.addRangeToPeakCache(test_length, test_start, test_end);
peakcache.addRangeToPeakCache(test_length, test_start3, test_end3);
var newranges = peakcache.addRangeToPeakCache(test_length, test_start, test_end3);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_end);
expect(newranges[0][1]).toEqual(test_start3);
var ranges = peakcache.getCacheRanges();
expect(ranges.length).toEqual(1);
expect(ranges[0][0]).toEqual(test_start);
expect(ranges[0][1]).toEqual(test_end3);
});

it('filling in disjoint sets coalesces / edge cases', function() {
peakcache.addRangeToPeakCache(test_length, test_start, test_end);
peakcache.addRangeToPeakCache(test_length, test_start3, test_end3);
var newranges = peakcache.addRangeToPeakCache(test_length, test_start2, test_end2);
expect(newranges.length).toEqual(1);
expect(newranges[0][0]).toEqual(test_end);
expect(newranges[0][1]).toEqual(test_start3);
var ranges = peakcache.getCacheRanges();
expect(ranges.length).toEqual(1);
expect(ranges[0][0]).toEqual(test_start);
expect(ranges[0][1]).toEqual(test_end3);
});

});
21 changes: 12 additions & 9 deletions src/drawer.canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
}
},

drawBars: function (peaks, channelIndex) {
drawBars: function (peaks, channelIndex, start, end) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
Expand All @@ -81,8 +81,10 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
// Bar wave draws the bottom only as a reflection of the top,
// so we don't need negative values
var hasMinVals = [].some.call(peaks, function (val) { return val < 0; });
// Skip every other value if there are negatives.
var peakIndexScale = 1;
if (hasMinVals) {
peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
peakIndexScale = 2;
}

// A half-pixel offset makes lines crisp
Expand All @@ -91,7 +93,7 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = peaks.length;
var length = peaks.length / peakIndexScale;
var bar = this.params.barWidth * this.params.pixelRatio;
var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
var step = bar + gap;
Expand All @@ -113,14 +115,15 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
[ this.waveCc, this.progressCc ].forEach(function (cc) {
if (!cc) { return; }

for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
for (var i = (start / scale); i < (end / scale); i += step) {
var peak = peaks[Math.floor(i * scale * peakIndexScale)] || 0;
var h = Math.round(peak / absmax * halfH);
cc.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2);
}
}, this);
},

drawWave: function (peaks, channelIndex) {
drawWave: function (peaks, channelIndex, start, end) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
Expand Down Expand Up @@ -172,16 +175,16 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
if (!cc) { return; }

cc.beginPath();
cc.moveTo($, halfH + offsetY);
cc.moveTo(start * scale + $, halfH + offsetY);

for (var i = 0; i < length; i++) {
for (var i = start; i < end; i++) {
var h = Math.round(peaks[2 * i] / absmax * halfH);
cc.lineTo(i * scale + $, halfH - h + offsetY);
}

// Draw the bottom edge going backwards, to make a single
// closed hull to fill.
for (var i = length - 1; i >= 0; i--) {
for (var i = end - 1; i >= start; i--) {
var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
cc.lineTo(i * scale + $, halfH - h + offsetY);
}
Expand Down
15 changes: 11 additions & 4 deletions src/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,12 @@ WaveSurfer.Drawer = {
});
},

drawPeaks: function (peaks, length) {
this.resetScroll();
drawPeaks: function (peaks, length, start, end) {
this.setWidth(length);

this.params.barWidth ?
this.drawBars(peaks) :
this.drawWave(peaks);
this.drawBars(peaks, 0, start, end) :
this.drawWave(peaks, 0, start, end);
},

style: function (el, styles) {
Expand Down Expand Up @@ -145,11 +144,19 @@ WaveSurfer.Drawer = {

},

getScrollX: function() {
return Math.round(this.wrapper.scrollLeft * this.params.pixelRatio);
},

getWidth: function () {
return Math.round(this.container.clientWidth * this.params.pixelRatio);
},

setWidth: function (width) {
if (this.width == width) {
return;
}

this.width = width;

if (this.params.fillParent || this.params.scrollParent) {
Expand Down
45 changes: 28 additions & 17 deletions src/drawer.multicanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {
}
},

drawBars: function (peaks, channelIndex) {
drawBars: function (peaks, channelIndex, start, end) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
Expand All @@ -153,16 +153,18 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {
// Bar wave draws the bottom only as a reflection of the top,
// so we don't need negative values
var hasMinVals = [].some.call(peaks, function (val) { return val < 0; });
// Skip every other value if there are negatives.
var peakIndexScale = 1;
if (hasMinVals) {
peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
peakIndexScale = 2;
}

// A half-pixel offset makes lines crisp
var width = this.width;
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = peaks.length;
var length = peaks.length / peakIndexScale;
var bar = this.params.barWidth * this.params.pixelRatio;
var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
var step = bar + gap;
Expand All @@ -176,13 +178,14 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {

var scale = length / width;

for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
for (var i = (start / scale); i < (end / scale); i += step) {
var peak = peaks[Math.floor(i * scale * peakIndexScale)] || 0;
var h = Math.round(peak / absmax * halfH);
this.fillRect(i + this.halfPixel, halfH - h + offsetY, bar + this.halfPixel, h * 2);
}
},

drawWave: function (peaks, channelIndex) {
drawWave: function (peaks, channelIndex, start, end) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
Expand Down Expand Up @@ -218,24 +221,24 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {
absmax = -min > max ? -min : max;
}

this.drawLine(peaks, absmax, halfH, offsetY);
this.drawLine(peaks, absmax, halfH, offsetY, start, end);

// Always draw a median line
this.fillRect(0, halfH + offsetY - this.halfPixel, this.width, this.halfPixel);
},

drawLine: function (peaks, absmax, halfH, offsetY) {
drawLine: function (peaks, absmax, halfH, offsetY, start, end) {
for (var index in this.canvases) {
var entry = this.canvases[index];

this.setFillStyles(entry);

this.drawLineToContext(entry, entry.waveCtx, peaks, absmax, halfH, offsetY);
this.drawLineToContext(entry, entry.progressCtx, peaks, absmax, halfH, offsetY);
this.drawLineToContext(entry, entry.waveCtx, peaks, absmax, halfH, offsetY, start, end);
this.drawLineToContext(entry, entry.progressCtx, peaks, absmax, halfH, offsetY, start, end);
}
},

drawLineToContext: function (entry, ctx, peaks, absmax, halfH, offsetY) {
drawLineToContext: function (entry, ctx, peaks, absmax, halfH, offsetY, start, end) {
if (!ctx) { return; }

var length = peaks.length / 2;
Expand All @@ -247,19 +250,24 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {

var first = Math.round(length * entry.start),
last = Math.round(length * entry.end);
if (first > end || last < start) { return; }
var canvasStart = Math.max(first, start);
var canvasEnd = Math.min(last, end);

ctx.beginPath();
ctx.moveTo(this.halfPixel, halfH + offsetY);
ctx.moveTo((canvasStart - first) * scale + this.halfPixel, halfH + offsetY);

for (var i = first; i < last; i++) {
var h = Math.round(peaks[2 * i] / absmax * halfH);
for (var i = canvasStart; i < canvasEnd; i++) {
var peak = peaks[2 * i] || 0;
var h = Math.round(peak / absmax * halfH);
ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
}

// Draw the bottom edge going backwards, to make a single
// closed hull to fill.
for (var i = last - 1; i >= first; i--) {
var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
for (var i = canvasEnd - 1; i >= canvasStart; i--) {
var peak = peaks[2 * i + 1] || 0;
var h = Math.round(peak / absmax * halfH);
ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
}

Expand All @@ -268,7 +276,10 @@ WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {
},

fillRect: function (x, y, width, height) {
for (var i in this.canvases) {
var startCanvas = Math.floor(x / this.maxCanvasWidth);
var endCanvas = Math.min(Math.ceil((x + width) / this.maxCanvasWidth) + 1,
this.canvases.length);
for (var i = startCanvas; i < endCanvas; i++) {
var entry = this.canvases[i],
leftOffset = i * this.maxCanvasWidth;

Expand Down
4 changes: 2 additions & 2 deletions src/mediaelement.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ WaveSurfer.util.extend(WaveSurfer.MediaElement, {
}
},

getPeaks: function (length) {
getPeaks: function (length, start, end) {
if (this.buffer) {
return WaveSurfer.WebAudio.getPeaks.call(this, length);
return WaveSurfer.WebAudio.getPeaks.call(this, length, start, end);
}
return this.peaks || [];
},
Expand Down
Loading

0 comments on commit 3d908c5

Please sign in to comment.