wavesurfer
Version:
Interactive navigable audio visualization using Web Audio and Canvas
343 lines (285 loc) • 12.4 kB
JavaScript
'use strict';
WaveSurfer.Drawer.MultiCanvas = Object.create(WaveSurfer.Drawer);
WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {
initDrawer: function (params) {
this.maxCanvasWidth = params.maxCanvasWidth != null ? params.maxCanvasWidth : 4000;
this.maxCanvasElementWidth = Math.round(this.maxCanvasWidth / this.params.pixelRatio);
if (this.maxCanvasWidth <= 1) {
throw 'maxCanvasWidth must be greater than 1.';
} else if (this.maxCanvasWidth % 2 == 1) {
throw 'maxCanvasWidth must be an even number.';
}
this.hasProgressCanvas = this.params.waveColor != this.params.progressColor;
this.halfPixel = 0.5 / this.params.pixelRatio;
this.canvases = [];
},
createElements: function () {
this.progressWave = this.wrapper.appendChild(
this.style(document.createElement('wave'), {
position: 'absolute',
zIndex: 2,
left: 0,
top: 0,
bottom: 0,
overflow: 'hidden',
width: '0',
display: 'none',
boxSizing: 'border-box',
borderRightStyle: 'solid',
borderRightWidth: this.params.cursorWidth + 'px',
borderRightColor: this.params.cursorColor
})
);
this.addCanvas();
},
updateSize: function () {
var totalWidth = Math.round(this.width / this.params.pixelRatio),
requiredCanvases = Math.ceil(totalWidth / this.maxCanvasElementWidth);
while (this.canvases.length < requiredCanvases) {
this.addCanvas();
}
while (this.canvases.length > requiredCanvases) {
this.removeCanvas();
}
for (var i in this.canvases) {
// Add some overlap to prevent vertical white stripes, keep the width even for simplicity.
var canvasWidth = this.maxCanvasWidth + 2 * Math.ceil(this.params.pixelRatio / 2);
if (i == this.canvases.length - 1) {
canvasWidth = this.width - (this.maxCanvasWidth * (this.canvases.length - 1));
}
this.updateDimensions(this.canvases[i], canvasWidth, this.height);
this.clearWaveForEntry(this.canvases[i]);
}
},
addCanvas: function () {
var entry = {},
leftOffset = this.maxCanvasElementWidth * this.canvases.length;
entry.wave = this.wrapper.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
zIndex: 1,
left: leftOffset + 'px',
top: 0,
bottom: 0
})
);
entry.waveCtx = entry.wave.getContext('2d');
if (this.hasProgressCanvas) {
entry.progress = this.progressWave.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
left: leftOffset + 'px',
top: 0,
bottom: 0
})
);
entry.progressCtx = entry.progress.getContext('2d');
}
this.canvases.push(entry);
},
removeCanvas: function () {
var lastEntry = this.canvases.pop();
lastEntry.wave.parentElement.removeChild(lastEntry.wave);
if (this.hasProgressCanvas) {
lastEntry.progress.parentElement.removeChild(lastEntry.progress);
}
},
updateDimensions: function (entry, width, height) {
var elementWidth = Math.round(width / this.params.pixelRatio),
totalWidth = Math.round(this.width / this.params.pixelRatio);
// Where the canvas starts and ends in the waveform, represented as a decimal between 0 and 1.
entry.start = (entry.waveCtx.canvas.offsetLeft / totalWidth) || 0;
entry.end = entry.start + elementWidth / totalWidth;
entry.waveCtx.canvas.width = width;
entry.waveCtx.canvas.height = height;
this.style(entry.waveCtx.canvas, { width: elementWidth + 'px'});
this.style(this.progressWave, { display: 'block'});
if (this.hasProgressCanvas) {
entry.progressCtx.canvas.width = width;
entry.progressCtx.canvas.height = height;
this.style(entry.progressCtx.canvas, { width: elementWidth + 'px'});
}
},
clearWave: function () {
for (var i in this.canvases) {
this.clearWaveForEntry(this.canvases[i]);
}
},
clearWaveForEntry: function (entry) {
entry.waveCtx.clearRect(0, 0, entry.waveCtx.canvas.width, entry.waveCtx.canvas.height);
if (this.hasProgressCanvas) {
entry.progressCtx.clearRect(0, 0, entry.progressCtx.canvas.width, entry.progressCtx.canvas.height);
}
},
drawBars: function (peaks, channelIndex, start, end) {
var my = this;
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(function(channelPeaks, i) {
my.drawBars(channelPeaks, i, start, end);
});
return;
} else {
peaks = channels[0];
}
}
// 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) {
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 / peakIndexScale;
var bar = this.params.barWidth * this.params.pixelRatio;
var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
var step = bar + gap;
var absmax = 1;
if (this.params.normalize) {
var max = WaveSurfer.util.max(peaks);
var min = WaveSurfer.util.min(peaks);
absmax = -min > max ? -min : max;
}
var scale = length / width;
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, start, end) {
var my = this;
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(function(channelPeaks, i) {
my.drawWave(channelPeaks, i, start, end);
});
return;
} else {
peaks = channels[0];
}
}
// Support arrays without negative peaks
var hasMinValues = [].some.call(peaks, function (val) { return val < 0; });
if (!hasMinValues) {
var reflectedPeaks = [];
for (var i = 0, len = peaks.length; i < len; i++) {
reflectedPeaks[2 * i] = peaks[i];
reflectedPeaks[2 * i + 1] = -peaks[i];
}
peaks = reflectedPeaks;
}
// A half-pixel offset makes lines crisp
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var absmax = 1;
if (this.params.normalize) {
var max = WaveSurfer.util.max(peaks);
var min = WaveSurfer.util.min(peaks);
absmax = -min > max ? -min : max;
}
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, 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, start, end);
this.drawLineToContext(entry, entry.progressCtx, peaks, absmax, halfH, offsetY, start, end);
}
},
drawLineToContext: function (entry, ctx, peaks, absmax, halfH, offsetY, start, end) {
if (!ctx) { return; }
var length = peaks.length / 2;
var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = this.width / length;
}
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((canvasStart - first) * scale + this.halfPixel, halfH + offsetY);
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 = 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);
}
ctx.closePath();
ctx.fill();
},
fillRect: function (x, y, width, height) {
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;
var intersection = {
x1: Math.max(x, i * this.maxCanvasWidth),
y1: y,
x2: Math.min(x + width, i * this.maxCanvasWidth + entry.waveCtx.canvas.width),
y2: y + height
};
if (intersection.x1 < intersection.x2) {
this.setFillStyles(entry);
this.fillRectToContext(entry.waveCtx,
intersection.x1 - leftOffset,
intersection.y1,
intersection.x2 - intersection.x1,
intersection.y2 - intersection.y1);
this.fillRectToContext(entry.progressCtx,
intersection.x1 - leftOffset,
intersection.y1,
intersection.x2 - intersection.x1,
intersection.y2 - intersection.y1);
}
}
},
fillRectToContext: function (ctx, x, y, width, height) {
if (!ctx) { return; }
ctx.fillRect(x, y, width, height);
},
setFillStyles: function (entry) {
entry.waveCtx.fillStyle = this.params.waveColor;
if (this.hasProgressCanvas) {
entry.progressCtx.fillStyle = this.params.progressColor;
}
},
updateProgress: function (pos) {
this.style(this.progressWave, { width: pos + 'px' });
},
getImage: function(type, quality) {
// combine all available canvasses together
var availableCanvas = '';
for (var i in this.canvases) {
var getEntry = this.canvases[i].wave.getContext('2d');
availableCanvas += getEntry.canvas.toDataURL(type, quality);
}
return availableCanvas;
}
});