wavesurfer.js
Version:
Interactive navigable audio visualization using Web Audio and Canvas
1,769 lines (1,516 loc) • 72.8 kB
JavaScript
/**
* wavesurfer.js
*
* https://github.com/katspaugh/wavesurfer.js
*
* This work is licensed under a Creative Commons Attribution 3.0 Unported License.
*/
'use strict';
var WaveSurfer = {
defaultParams: {
height : 128,
waveColor : '#999',
progressColor : '#555',
cursorColor : '#333',
cursorWidth : 1,
skipLength : 2,
minPxPerSec : 20,
pixelRatio : window.devicePixelRatio,
fillParent : true,
scrollParent : false,
hideScrollbar : false,
normalize : false,
audioContext : null,
container : null,
dragSelection : true,
loopSelection : true,
audioRate : 1,
interact : true,
renderer : 'Canvas',
backend : 'WebAudio'
},
init: function (params) {
// Extract relevant parameters (or defaults)
this.params = WaveSurfer.util.extend({}, this.defaultParams, params);
this.container = 'string' == typeof params.container ?
document.querySelector(this.params.container) :
this.params.container;
if (!this.container) {
throw new Error('Container element not found');
}
// Used to save the current volume when muting so we can
// restore once unmuted
this.savedVolume = 0;
// The current muted state
this.isMuted = false;
this.createDrawer();
this.createBackend();
},
createDrawer: function () {
var my = this;
this.drawer = Object.create(WaveSurfer.Drawer[this.params.renderer]);
this.drawer.init(this.container, this.params);
this.drawer.on('redraw', function () {
my.drawBuffer();
my.drawer.progress(my.backend.getPlayedPercents());
});
// Click-to-seek
this.drawer.on('click', function (e, progress) {
setTimeout(function () {
my.seekTo(progress);
}, 0);
});
// Relay the scroll event from the drawer
this.drawer.on('scroll', function (e) {
my.fireEvent('scroll', e);
});
},
createBackend: function () {
var my = this;
if (this.backend) {
this.backend.destroy();
}
this.backend = Object.create(WaveSurfer[this.params.backend]);
this.backend.on('finish', function () {
my.fireEvent('finish');
});
this.backend.on('audioprocess', function (time) {
my.fireEvent('audioprocess', time);
});
try {
this.backend.init(this.params);
} catch (e) {
if (e.message == "Your browser doesn't support Web Audio") {
this.params.backend = 'AudioElement';
this.backend = null;
this.createBackend();
}
}
},
restartAnimationLoop: function () {
var my = this;
var requestFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame;
var frame = function () {
if (!my.backend.isPaused()) {
my.drawer.progress(my.backend.getPlayedPercents());
requestFrame(frame);
}
};
frame();
},
getDuration: function () {
return this.backend.getDuration();
},
getCurrentTime: function () {
return this.backend.getCurrentTime();
},
play: function (start, end) {
this.backend.play(start, end);
this.restartAnimationLoop();
this.fireEvent('play');
},
pause: function () {
this.backend.pause();
this.fireEvent('pause');
},
playPause: function () {
this.backend.isPaused() ? this.play() : this.pause();
},
skipBackward: function (seconds) {
this.skip(-seconds || -this.params.skipLength);
},
skipForward: function (seconds) {
this.skip(seconds || this.params.skipLength);
},
skip: function (offset) {
var position = this.getCurrentTime() || 0;
var duration = this.getDuration() || 1;
position = Math.max(0, Math.min(duration, position + (offset || 0)));
this.seekAndCenter(position / duration);
},
seekAndCenter: function (progress) {
this.seekTo(progress);
this.drawer.recenter(progress);
},
seekTo: function (progress) {
var paused = this.backend.isPaused();
// avoid small scrolls while paused seeking
var oldScrollParent = this.params.scrollParent;
if (paused) {
this.params.scrollParent = false;
}
this.backend.seekTo(progress * this.getDuration());
this.drawer.progress(this.backend.getPlayedPercents());
if (!paused) {
this.backend.pause();
this.backend.play();
}
this.params.scrollParent = oldScrollParent;
this.fireEvent('seek', progress);
},
stop: function () {
this.pause();
this.seekTo(0);
this.drawer.progress(0);
},
/**
* Set the playback volume.
*
* @param {Number} newVolume A value between 0 and 1, 0 being no
* volume and 1 being full volume.
*/
setVolume: function (newVolume) {
this.backend.setVolume(newVolume);
},
/**
* Set the playback volume.
*
* @param {Number} rate A positive number. E.g. 0.5 means half the
* normal speed, 2 means double speed and so on.
*/
setPlaybackRate: function (rate) {
this.backend.setPlaybackRate(rate);
},
/**
* Toggle the volume on and off. It not currenly muted it will
* save the current volume value and turn the volume off.
* If currently muted then it will restore the volume to the saved
* value, and then rest the saved value.
*/
toggleMute: function () {
if (this.isMuted) {
// If currently muted then restore to the saved volume
// and update the mute properties
this.backend.setVolume(this.savedVolume);
this.isMuted = false;
} else {
// If currently not muted then save current volume,
// turn off the volume and update the mute properties
this.savedVolume = this.backend.getVolume();
this.backend.setVolume(0);
this.isMuted = true;
}
},
toggleScroll: function () {
this.params.scrollParent = !this.params.scrollParent;
this.drawBuffer();
},
toggleInteraction: function () {
this.params.interact = !this.params.interact;
},
drawBuffer: function () {
if (this.params.fillParent && !this.params.scrollParent) {
var length = this.drawer.getWidth();
} else {
length = Math.round(this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio);
}
var peaks = this.backend.getPeaks(length);
this.drawer.drawPeaks(peaks, length);
this.fireEvent('redraw', peaks, length);
},
/**
* Internal method.
*/
loadArrayBuffer: function (arraybuffer) {
var my = this;
this.backend.decodeArrayBuffer(arraybuffer, function (data) {
my.loadDecodedBuffer(data);
}, function () {
my.fireEvent('error', 'Error decoding audiobuffer');
});
},
/**
* Directly load an externally decoded AudioBuffer.
*/
loadDecodedBuffer: function (buffer) {
this.empty();
this.backend.load(buffer);
this.drawBuffer();
this.fireEvent('ready');
},
/**
* Loads audio data from a Blob or File object.
*
* @param {Blob|File} blob Audio data.
*/
loadBlob: function (blob) {
var my = this;
// Create file reader
var reader = new FileReader();
reader.addEventListener('progress', function (e) {
my.onProgress(e);
});
reader.addEventListener('load', function (e) {
my.empty();
my.loadArrayBuffer(e.target.result);
});
reader.addEventListener('error', function () {
my.fireEvent('error', 'Error reading file');
});
reader.readAsArrayBuffer(blob);
},
/**
* Loads audio and rerenders the waveform.
*/
load: function (url, peaks) {
switch (this.params.backend) {
case 'WebAudio': return this.loadBuffer(url);
case 'AudioElement': return this.loadAudioElement(url, peaks);
}
},
/**
* Loads audio using Web Audio buffer backend.
*/
loadBuffer: function (url) {
this.empty();
// load via XHR and render all at once
return this.downloadArrayBuffer(url, this.loadArrayBuffer.bind(this));
},
loadAudioElement: function (url, peaks) {
this.empty();
this.backend.load(url, peaks, this.container);
this.backend.once('canplay', (function () {
this.drawBuffer();
this.fireEvent('ready');
}).bind(this));
this.backend.once('error', (function (err) {
this.fireEvent('error', err);
}).bind(this));
},
downloadArrayBuffer: function (url, callback) {
var my = this;
var ajax = WaveSurfer.util.ajax({
url: url,
responseType: 'arraybuffer'
});
ajax.on('progress', function (e) {
my.onProgress(e);
});
ajax.on('success', callback);
ajax.on('error', function (e) {
my.fireEvent('error', 'XHR error: ' + e.target.statusText);
});
return ajax;
},
onProgress: function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
} else {
// Approximate progress with an asymptotic
// function, and assume downloads in the 1-3 MB range.
percentComplete = e.loaded / (e.loaded + 1000000);
}
this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
},
/**
* Exports PCM data into a JSON array and opens in a new window.
*/
exportPCM: function (length, accuracy, noWindow) {
length = length || 1024;
accuracy = accuracy || 10000;
noWindow = noWindow || false;
var peaks = this.backend.getPeaks(length, accuracy);
var arr = [].map.call(peaks, function (val) {
return Math.round(val * accuracy) / accuracy;
});
var json = JSON.stringify(arr);
if (!noWindow) {
window.open('data:application/json;charset=utf-8,' +
encodeURIComponent(json));
}
return json;
},
/**
* Display empty waveform.
*/
empty: function () {
if (!this.backend.isPaused()) {
this.stop();
this.backend.disconnectSource();
}
this.drawer.progress(0);
this.drawer.setWidth(0);
this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
},
/**
* Remove events, elements and disconnect WebAudio nodes.
*/
destroy: function () {
this.fireEvent('destroy');
this.unAll();
this.backend.destroy();
this.drawer.destroy();
}
};
/* Observer */
WaveSurfer.Observer = {
on: function (event, fn) {
if (!this.handlers) { this.handlers = {}; }
var handlers = this.handlers[event];
if (!handlers) {
handlers = this.handlers[event] = [];
}
handlers.push(fn);
},
un: function (event, fn) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
if (handlers) {
if (fn) {
for (var i = handlers.length - 1; i >= 0; i--) {
if (handlers[i] == fn) {
handlers.splice(i, 1);
}
}
} else {
handlers.length = 0;
}
}
},
unAll: function () {
this.handlers = null;
},
once: function (event, handler) {
var my = this;
var fn = function () {
handler();
setTimeout(function () {
my.un(event, fn);
}, 0);
};
this.on(event, fn);
},
fireEvent: function (event) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
var args = Array.prototype.slice.call(arguments, 1);
handlers && handlers.forEach(function (fn) {
fn.apply(null, args);
});
}
};
/* Common utilities */
WaveSurfer.util = {
extend: function (dest) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function (source) {
Object.keys(source).forEach(function (key) {
dest[key] = source[key];
});
});
return dest;
},
getId: function () {
return 'wavesurfer_' + Math.random().toString(32).substring(2);
},
max: function (values, min) {
var max = -Infinity;
for (var i = 0, len = values.length; i < len; i++) {
var val = values[i];
if (min != null) {
val = Math.abs(val - min);
}
if (val > max) { max = val; }
}
return max;
},
ajax: function (options) {
var ajax = Object.create(WaveSurfer.Observer);
var xhr = new XMLHttpRequest();
var fired100 = false;
xhr.open(options.method || 'GET', options.url, true);
xhr.responseType = options.responseType;
xhr.addEventListener('progress', function (e) {
ajax.fireEvent('progress', e);
if (e.lengthComputable && e.loaded == e.total) {
fired100 = true;
}
});
xhr.addEventListener('load', function (e) {
if (!fired100) {
ajax.fireEvent('progress', e);
}
ajax.fireEvent('load', e);
if (200 == xhr.status || 206 == xhr.status) {
ajax.fireEvent('success', xhr.response, e);
} else {
ajax.fireEvent('error', e);
}
});
xhr.addEventListener('error', function (e) {
ajax.fireEvent('error', e);
});
xhr.send();
ajax.xhr = xhr;
return ajax;
}
};
WaveSurfer.util.extend(WaveSurfer, WaveSurfer.Observer);
'use strict';
WaveSurfer.WebAudio = {
scriptBufferSize: 256,
fftSize: 128,
PLAYING_STATE: 0,
PAUSED_STATE: 1,
FINISHED_STATE: 2,
getAudioContext: function () {
if (!(window.AudioContext || window.webkitAudioContext)) {
throw new Error("Your browser doesn't support Web Audio");
}
if (!WaveSurfer.WebAudio.audioContext) {
WaveSurfer.WebAudio.audioContext = new (
window.AudioContext || window.webkitAudioContext
);
}
return WaveSurfer.WebAudio.audioContext;
},
init: function (params) {
this.params = params;
this.ac = params.audioContext || this.getAudioContext();
this.lastPlay = this.ac.currentTime;
this.startPosition = 0;
this.states = [
Object.create(WaveSurfer.WebAudio.state.playing),
Object.create(WaveSurfer.WebAudio.state.paused),
Object.create(WaveSurfer.WebAudio.state.finished)
];
this.setState(this.PAUSED_STATE);
this.createVolumeNode();
this.createScriptNode();
this.createAnalyserNode();
this.setPlaybackRate(this.params.audioRate);
},
disconnectFilters: function () {
if (this.filters) {
this.filters.forEach(function (filter) {
filter && filter.disconnect();
});
this.filters = null;
}
},
setState: function (state) {
if (this.state !== this.states[state]) {
this.state = this.states[state];
this.state.init.call(this);
}
},
// Unpacked filters
setFilter: function () {
this.setFilters([].slice.call(arguments));
},
/**
* @param {Array} filters Packed ilters array
*/
setFilters: function (filters) {
this.disconnectFilters();
if (filters && filters.length) {
this.filters = filters;
// Connect each filter in turn
filters.reduce(function (prev, curr) {
prev.connect(curr);
return curr;
}, this.analyser).connect(this.gainNode);
} else {
this.analyser.connect(this.gainNode);
}
},
createScriptNode: function () {
var my = this;
var bufferSize = this.scriptBufferSize;
if (this.ac.createScriptProcessor) {
this.scriptNode = this.ac.createScriptProcessor(bufferSize);
} else {
this.scriptNode = this.ac.createJavaScriptNode(bufferSize);
}
this.scriptNode.connect(this.ac.destination);
this.scriptNode.onaudioprocess = function () {
var time = my.getCurrentTime();
if (my.state === my.states[my.PLAYING_STATE]) {
my.fireEvent('audioprocess', time);
}
if (my.buffer && time > my.getDuration()) {
my.setState(my.FINISHED_STATE);
}
};
},
createAnalyserNode: function () {
this.analyser = this.ac.createAnalyser();
this.analyser.fftSize = this.fftSize;
this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.connect(this.gainNode);
},
/**
* Create the gain node needed to control the playback volume.
*/
createVolumeNode: function () {
// Create gain node using the AudioContext
if (this.ac.createGain) {
this.gainNode = this.ac.createGain();
} else {
this.gainNode = this.ac.createGainNode();
}
// Add the gain node to the graph
this.gainNode.connect(this.ac.destination);
},
/**
* Set the gain to a new value.
*
* @param {Number} newGain The new gain, a floating point value
* between 0 and 1. 0 being no gain and 1 being maximum gain.
*/
setVolume: function (newGain) {
this.gainNode.gain.value = newGain;
},
/**
* Get the current gain.
*
* @returns {Number} The current gain, a floating point value
* between 0 and 1. 0 being no gain and 1 being maximum gain.
*/
getVolume: function () {
return this.gainNode.gain.value;
},
decodeArrayBuffer: function (arraybuffer, callback, errback) {
var my = this;
this.ac.decodeAudioData(arraybuffer, function (data) {
my.buffer = data;
callback(data);
}, errback);
},
/**
* @returns {Float32Array} Array of peaks.
*/
getPeaks: function (length) {
var buffer = this.buffer;
var sampleSize = buffer.length / length;
var sampleStep = ~~(sampleSize / 10) || 1;
var channels = buffer.numberOfChannels;
var peaks = new Float32Array(length);
for (var c = 0; c < channels; c++) {
var chan = buffer.getChannelData(c);
for (var i = 0; i < length; i++) {
var start = ~~(i * sampleSize);
var end = ~~(start + sampleSize);
var max = 0;
for (var j = start; j < end; j += sampleStep) {
var value = chan[j];
if (value > max) {
max = value;
// faster than Math.abs
} else if (-value > max) {
max = -value;
}
}
if (c == 0 || max > peaks[i]) {
peaks[i] = max;
}
}
}
return peaks;
},
getPlayedPercents: function () {
return this.state.getPlayedPercents.call(this);
},
disconnectSource: function () {
if (this.source) {
this.source.disconnect();
}
},
/**
* Returns the real-time waveform data.
*
* @return {Uint8Array} The frequency data.
* Values range from 0 to 255.
*/
waveform: function () {
this.analyser.getByteTimeDomainData(this.analyserData);
return this.analyserData;
},
destroy: function () {
this.pause();
this.unAll();
this.buffer = null;
this.disconnectFilters();
this.disconnectSource();
this.gainNode.disconnect();
this.scriptNode.disconnect();
this.analyser.disconnect();
},
load: function (buffer) {
this.startPosition = 0;
this.lastPlay = this.ac.currentTime;
this.buffer = buffer;
this.createSource();
},
createSource: function () {
this.disconnectSource();
this.source = this.ac.createBufferSource();
//adjust for old browsers.
this.source.start = this.source.start || this.source.noteGrainOn;
this.source.stop = this.source.stop || this.source.noteOff;
this.source.playbackRate.value = this.playbackRate;
this.source.buffer = this.buffer;
this.source.connect(this.analyser);
},
isPaused: function () {
return this.state !== this.states[this.PLAYING_STATE];
},
getDuration: function () {
return this.buffer.duration || 0;
},
seekTo: function (start, end) {
if (start == null) {
start = this.getCurrentTime();
if (start >= this.getDuration()) {
start = 0;
}
}
if (end == null) {
end = this.getDuration();
}
this.startPosition = start;
this.lastPlay = this.ac.currentTime;
if (this.state === this.states[this.FINISHED_STATE]) {
this.setState(this.PAUSED_STATE);
}
return { start: start, end: end };
},
getPlayedTime: function () {
return (this.ac.currentTime - this.lastPlay) * this.playbackRate;
},
/**
* Plays the loaded audio region.
*
* @param {Number} start Start offset in seconds,
* relative to the beginning of a clip.
* @param {Number} end When to stop
* relative to the beginning of a clip.
*/
play: function (start, end) {
// need to re-create source on each playback
this.createSource();
var adjustedTime = this.seekTo(start, end);
start = adjustedTime.start;
end = adjustedTime.end;
this.source.start(0, start, end - start);
this.setState(this.PLAYING_STATE);
},
/**
* Pauses the loaded audio.
*/
pause: function () {
this.startPosition += this.getPlayedTime();
this.source && this.source.stop(0);
this.setState(this.PAUSED_STATE);
},
/**
* Returns the current time in seconds relative to the audioclip's duration.
*/
getCurrentTime: function () {
return this.state.getCurrentTime.call(this);
},
/**
* Set the audio source playback rate.
*/
setPlaybackRate: function (value) {
value = value || 1;
if (this.isPaused()) {
this.playbackRate = value;
} else {
this.pause();
this.playbackRate = value;
this.play();
}
}
};
WaveSurfer.WebAudio.state = {};
WaveSurfer.WebAudio.state.playing = {
init: function () {
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition + this.getPlayedTime();
}
};
WaveSurfer.WebAudio.state.paused = {
init: function () {
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition;
}
};
WaveSurfer.WebAudio.state.finished = {
init: function () {
this.fireEvent('finish');
},
getPlayedPercents: function () {
return 1;
},
getCurrentTime: function () {
return this.getDuration();
}
};
WaveSurfer.util.extend(WaveSurfer.WebAudio, WaveSurfer.Observer);
'use strict';
WaveSurfer.AudioElement = Object.create(WaveSurfer.WebAudio);
WaveSurfer.util.extend(WaveSurfer.AudioElement, {
init: function (params) {
this.params = params;
// Dummy media to catch errors
this.media = {
currentTime: 0,
duration: 0,
paused: true,
playbackRate: 1,
play: function () {},
pause: function () {}
};
},
load: function (url, peaks, container) {
var my = this;
var media = document.createElement('audio');
media.controls = false;
media.autoplay = false;
media.src = url;
media.addEventListener('error', function () {
my.fireEvent('error', 'Error loading media element');
});
media.addEventListener('canplay', function () {
my.fireEvent('canplay');
});
media.addEventListener('ended', function () {
my.fireEvent('finish');
});
media.addEventListener('timeupdate', function () {
my.fireEvent('audioprocess', my.getCurrentTime());
});
var prevMedia = container.querySelector('audio');
if (prevMedia) {
container.removeChild(prevMedia);
}
container.appendChild(media);
this.media = media;
this.peaks = peaks;
this.setPlaybackRate(this.playbackRate);
},
isPaused: function () {
return this.media.paused;
},
getDuration: function () {
var duration = this.media.duration;
if (duration >= Infinity) { // streaming audio
duration = this.media.seekable.end();
}
return duration;
},
getCurrentTime: function () {
return this.media.currentTime;
},
getPlayedPercents: function () {
return (this.getCurrentTime() / this.getDuration()) || 0;
},
/**
* Set the audio source playback rate.
*/
setPlaybackRate: function (value) {
this.playbackRate = value || 1;
this.media.playbackRate = this.playbackRate;
},
seekTo: function (start) {
if (start != null) {
this.media.currentTime = start;
}
},
/**
* Plays the loaded audio region.
*
* @param {Number} start Start offset in seconds,
* relative to the beginning of a clip.
*/
play: function (start) {
this.seekTo(start);
this.media.play();
},
/**
* Pauses the loaded audio.
*/
pause: function () {
this.media.pause();
},
getPeaks: function (length) {
return this.peaks || [];
},
getVolume: function () {
return this.media.volume;
},
setVolume: function (val) {
this.media.volume = val;
},
destroy: function () {
this.pause();
this.unAll();
this.media.parentNode && this.media.parentNode.removeChild(this.media);
this.media = null;
}
});
'use strict';
WaveSurfer.Drawer = {
init: function (container, params) {
this.container = container;
this.params = params;
this.width = 0;
this.height = params.height * this.params.pixelRatio;
this.lastPos = 0;
this.createWrapper();
this.createElements();
},
createWrapper: function () {
this.wrapper = this.container.appendChild(
document.createElement('wave')
);
this.style(this.wrapper, {
display: 'block',
position: 'relative',
userSelect: 'none',
webkitUserSelect: 'none',
height: this.params.height + 'px'
});
if (this.params.fillParent || this.params.scrollParent) {
this.style(this.wrapper, {
width: '100%',
overflowX: this.params.hideScrollbar ? 'hidden' : 'auto',
overflowY: 'hidden'
});
}
this.setupWrapperEvents();
},
handleEvent: function (e) {
e.preventDefault();
var bbox = this.wrapper.getBoundingClientRect();
return ((e.clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0;
},
setupWrapperEvents: function () {
var my = this;
this.wrapper.addEventListener('click', function (e) {
var scrollbarHeight = my.wrapper.offsetHeight - my.wrapper.clientHeight;
if (scrollbarHeight != 0) {
// scrollbar is visible. Check if click was on it
var bbox = my.wrapper.getBoundingClientRect();
if (e.clientY >= bbox.bottom - scrollbarHeight) {
// ignore mousedown as it was on the scrollbar
return;
}
}
if (my.params.interact) {
my.fireEvent('click', e, my.handleEvent(e));
}
});
},
drawPeaks: function (peaks, length) {
this.resetScroll();
this.setWidth(length);
if (this.params.normalize) {
var max = WaveSurfer.util.max(peaks);
} else {
max = 1;
}
this.drawWave(peaks, max);
},
style: function (el, styles) {
Object.keys(styles).forEach(function (prop) {
if (el.style[prop] != styles[prop]) {
el.style[prop] = styles[prop];
}
});
return el;
},
resetScroll: function () {
this.wrapper.scrollLeft = 0;
},
recenter: function (percent) {
var position = this.wrapper.scrollWidth * percent;
this.recenterOnPosition(position, true);
},
recenterOnPosition: function (position, immediate) {
var scrollLeft = this.wrapper.scrollLeft;
var half = ~~(this.wrapper.clientWidth / 2);
var target = position - half;
var offset = target - scrollLeft;
var maxScroll = this.wrapper.scrollWidth - this.wrapper.clientWidth;
if (maxScroll == 0) {
// no need to continue if scrollbar is not there
return;
}
// if the cursor is currently visible...
if (!immediate && -half <= offset && offset < half) {
// we'll limit the "re-center" rate.
var rate = 5;
offset = Math.max(-rate, Math.min(rate, offset));
target = scrollLeft + offset;
}
// limit target to valid range (0 to maxScroll)
target = Math.max(0, Math.min(maxScroll, target));
// no use attempting to scroll if we're not moving
if (target != scrollLeft) {
this.wrapper.scrollLeft = target;
}
},
getWidth: function () {
return Math.round(this.container.clientWidth * this.params.pixelRatio);
},
setWidth: function (width) {
if (width == this.width) { return; }
this.width = width;
if (this.params.fillParent || this.params.scrollParent) {
this.style(this.wrapper, {
width: ''
});
} else {
this.style(this.wrapper, {
width: ~~(this.width / this.params.pixelRatio) + 'px'
});
}
this.updateWidth();
},
progress: function (progress) {
var minPxDelta = 1 / this.params.pixelRatio;
var pos = Math.round(progress * this.width) * minPxDelta;
if (pos < this.lastPos || pos - this.lastPos >= minPxDelta) {
this.lastPos = pos;
if (this.params.scrollParent) {
var newPos = ~~(this.wrapper.scrollWidth * progress);
this.recenterOnPosition(newPos);
}
this.updateProgress(progress);
}
},
destroy: function () {
this.unAll();
this.container.removeChild(this.wrapper);
this.wrapper = null;
},
/* Renderer-specific methods */
createElements: function () {},
updateWidth: function () {},
drawWave: function (peaks, max) {},
clearWave: function () {},
updateProgress: function (position) {}
};
WaveSurfer.util.extend(WaveSurfer.Drawer, WaveSurfer.Observer);
'use strict';
WaveSurfer.Drawer.Canvas = Object.create(WaveSurfer.Drawer);
WaveSurfer.util.extend(WaveSurfer.Drawer.Canvas, {
createElements: function () {
var waveCanvas = this.wrapper.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
zIndex: 1
})
);
this.progressWave = this.wrapper.appendChild(
this.style(document.createElement('wave'), {
position: 'absolute',
zIndex: 2,
overflow: 'hidden',
width: '0',
height: this.params.height + 'px',
borderRightStyle: 'solid',
borderRightWidth: this.params.cursorWidth + 'px',
borderRightColor: this.params.cursorColor
})
);
var progressCanvas = this.progressWave.appendChild(
document.createElement('canvas')
);
this.waveCc = waveCanvas.getContext('2d');
this.progressCc = progressCanvas.getContext('2d');
},
updateWidth: function () {
var width = Math.round(this.width / this.params.pixelRatio);
[
this.waveCc,
this.progressCc
].forEach(function (cc) {
cc.canvas.width = this.width;
cc.canvas.height = this.height;
this.style(cc.canvas, { width: width + 'px'});
}, this);
this.clearWave();
},
clearWave: function () {
this.waveCc.clearRect(0, 0, this.width, this.height);
this.progressCc.clearRect(0, 0, this.width, this.height);
},
drawWave: function (peaks, max) {
// A half-pixel offset makes lines crisp
var $ = 0.5 / this.params.pixelRatio;
this.waveCc.fillStyle = this.params.waveColor;
this.progressCc.fillStyle = this.params.progressColor;
var halfH = this.height / 2;
var coef = halfH / max;
var length = peaks.length;
var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = this.width / peaks.length;
}
this.waveCc.beginPath();
this.waveCc.moveTo($, halfH);
this.progressCc.beginPath();
this.progressCc.moveTo($, halfH);
for (var i = 0; i < length; i++) {
var h = Math.round(peaks[i] * coef);
this.waveCc.lineTo(i * scale + $, halfH + h);
this.progressCc.lineTo(i * scale + $, halfH + h);
}
this.waveCc.lineTo(this.width + $, halfH);
this.progressCc.lineTo(this.width + $, halfH);
this.waveCc.moveTo($, halfH);
this.progressCc.moveTo($, halfH);
for (var i = 0; i < length; i++) {
var h = Math.round(peaks[i] * coef);
this.waveCc.lineTo(i * scale + $, halfH - h);
this.progressCc.lineTo(i * scale + $, halfH - h);
}
this.waveCc.lineTo(this.width + $, halfH);
this.waveCc.fill();
this.progressCc.lineTo(this.width + $, halfH);
this.progressCc.fill();
// Always draw a median line
this.waveCc.fillRect(0, halfH - $, this.width, $);
},
updateProgress: function (progress) {
var pos = Math.round(
this.width * progress
) / this.params.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
}
});
module.exports = WaveSurfer;
/**
* wavesurfer.js
*
* https://github.com/katspaugh/wavesurfer.js
*
* This work is licensed under a Creative Commons Attribution 3.0 Unported License.
*/
'use strict';
var WaveSurfer = {
defaultParams: {
height : 128,
waveColor : '#999',
progressColor : '#555',
cursorColor : '#333',
cursorWidth : 1,
skipLength : 2,
minPxPerSec : 20,
pixelRatio : window.devicePixelRatio,
fillParent : true,
scrollParent : false,
hideScrollbar : false,
normalize : false,
audioContext : null,
container : null,
dragSelection : true,
loopSelection : true,
audioRate : 1,
interact : true,
renderer : 'Canvas',
backend : 'WebAudio'
},
init: function (params) {
// Extract relevant parameters (or defaults)
this.params = WaveSurfer.util.extend({}, this.defaultParams, params);
this.container = 'string' == typeof params.container ?
document.querySelector(this.params.container) :
this.params.container;
if (!this.container) {
throw new Error('Container element not found');
}
// Used to save the current volume when muting so we can
// restore once unmuted
this.savedVolume = 0;
// The current muted state
this.isMuted = false;
this.createDrawer();
this.createBackend();
},
createDrawer: function () {
var my = this;
this.drawer = Object.create(WaveSurfer.Drawer[this.params.renderer]);
this.drawer.init(this.container, this.params);
this.drawer.on('redraw', function () {
my.drawBuffer();
my.drawer.progress(my.backend.getPlayedPercents());
});
// Click-to-seek
this.drawer.on('click', function (e, progress) {
setTimeout(function () {
my.seekTo(progress);
}, 0);
});
// Relay the scroll event from the drawer
this.drawer.on('scroll', function (e) {
my.fireEvent('scroll', e);
});
},
createBackend: function () {
var my = this;
if (this.backend) {
this.backend.destroy();
}
this.backend = Object.create(WaveSurfer[this.params.backend]);
this.backend.on('finish', function () {
my.fireEvent('finish');
});
this.backend.on('audioprocess', function (time) {
my.fireEvent('audioprocess', time);
});
try {
this.backend.init(this.params);
} catch (e) {
if (e.message == "Your browser doesn't support Web Audio") {
this.params.backend = 'AudioElement';
this.backend = null;
this.createBackend();
}
}
},
restartAnimationLoop: function () {
var my = this;
var requestFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame;
var frame = function () {
if (!my.backend.isPaused()) {
my.drawer.progress(my.backend.getPlayedPercents());
requestFrame(frame);
}
};
frame();
},
getDuration: function () {
return this.backend.getDuration();
},
getCurrentTime: function () {
return this.backend.getCurrentTime();
},
play: function (start, end) {
this.backend.play(start, end);
this.restartAnimationLoop();
this.fireEvent('play');
},
pause: function () {
this.backend.pause();
this.fireEvent('pause');
},
playPause: function () {
this.backend.isPaused() ? this.play() : this.pause();
},
skipBackward: function (seconds) {
this.skip(-seconds || -this.params.skipLength);
},
skipForward: function (seconds) {
this.skip(seconds || this.params.skipLength);
},
skip: function (offset) {
var position = this.getCurrentTime() || 0;
var duration = this.getDuration() || 1;
position = Math.max(0, Math.min(duration, position + (offset || 0)));
this.seekAndCenter(position / duration);
},
seekAndCenter: function (progress) {
this.seekTo(progress);
this.drawer.recenter(progress);
},
seekTo: function (progress) {
var paused = this.backend.isPaused();
// avoid small scrolls while paused seeking
var oldScrollParent = this.params.scrollParent;
if (paused) {
this.params.scrollParent = false;
}
this.backend.seekTo(progress * this.getDuration());
this.drawer.progress(this.backend.getPlayedPercents());
if (!paused) {
this.backend.pause();
this.backend.play();
}
this.params.scrollParent = oldScrollParent;
this.fireEvent('seek', progress);
},
stop: function () {
this.pause();
this.seekTo(0);
this.drawer.progress(0);
},
/**
* Set the playback volume.
*
* @param {Number} newVolume A value between 0 and 1, 0 being no
* volume and 1 being full volume.
*/
setVolume: function (newVolume) {
this.backend.setVolume(newVolume);
},
/**
* Set the playback volume.
*
* @param {Number} rate A positive number. E.g. 0.5 means half the
* normal speed, 2 means double speed and so on.
*/
setPlaybackRate: function (rate) {
this.backend.setPlaybackRate(rate);
},
/**
* Toggle the volume on and off. It not currenly muted it will
* save the current volume value and turn the volume off.
* If currently muted then it will restore the volume to the saved
* value, and then rest the saved value.
*/
toggleMute: function () {
if (this.isMuted) {
// If currently muted then restore to the saved volume
// and update the mute properties
this.backend.setVolume(this.savedVolume);
this.isMuted = false;
} else {
// If currently not muted then save current volume,
// turn off the volume and update the mute properties
this.savedVolume = this.backend.getVolume();
this.backend.setVolume(0);
this.isMuted = true;
}
},
toggleScroll: function () {
this.params.scrollParent = !this.params.scrollParent;
this.drawBuffer();
},
toggleInteraction: function () {
this.params.interact = !this.params.interact;
},
drawBuffer: function () {
if (this.params.fillParent && !this.params.scrollParent) {
var length = this.drawer.getWidth();
} else {
length = Math.round(this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio);
}
var peaks = this.backend.getPeaks(length);
this.drawer.drawPeaks(peaks, length);
this.fireEvent('redraw', peaks, length);
},
/**
* Internal method.
*/
loadArrayBuffer: function (arraybuffer) {
var my = this;
this.backend.decodeArrayBuffer(arraybuffer, function (data) {
my.loadDecodedBuffer(data);
}, function () {
my.fireEvent('error', 'Error decoding audiobuffer');
});
},
/**
* Directly load an externally decoded AudioBuffer.
*/
loadDecodedBuffer: function (buffer) {
this.empty();
this.backend.load(buffer);
this.drawBuffer();
this.fireEvent('ready');
},
/**
* Loads audio data from a Blob or File object.
*
* @param {Blob|File} blob Audio data.
*/
loadBlob: function (blob) {
var my = this;
// Create file reader
var reader = new FileReader();
reader.addEventListener('progress', function (e) {
my.onProgress(e);
});
reader.addEventListener('load', function (e) {
my.empty();
my.loadArrayBuffer(e.target.result);
});
reader.addEventListener('error', function () {
my.fireEvent('error', 'Error reading file');
});
reader.readAsArrayBuffer(blob);
},
/**
* Loads audio and rerenders the waveform.
*/
load: function (url, peaks) {
switch (this.params.backend) {
case 'WebAudio': return this.loadBuffer(url);
case 'AudioElement': return this.loadAudioElement(url, peaks);
}
},
/**
* Loads audio using Web Audio buffer backend.
*/
loadBuffer: function (url) {
this.empty();
// load via XHR and render all at once
return this.downloadArrayBuffer(url, this.loadArrayBuffer.bind(this));
},
loadAudioElement: function (url, peaks) {
this.empty();
this.backend.load(url, peaks, this.container);
this.backend.once('canplay', (function () {
this.drawBuffer();
this.fireEvent('ready');
}).bind(this));
this.backend.once('error', (function (err) {
this.fireEvent('error', err);
}).bind(this));
},
downloadArrayBuffer: function (url, callback) {
var my = this;
var ajax = WaveSurfer.util.ajax({
url: url,
responseType: 'arraybuffer'
});
ajax.on('progress', function (e) {
my.onProgress(e);
});
ajax.on('success', callback);
ajax.on('error', function (e) {
my.fireEvent('error', 'XHR error: ' + e.target.statusText);
});
return ajax;
},
onProgress: function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
} else {
// Approximate progress with an asymptotic
// function, and assume downloads in the 1-3 MB range.
percentComplete = e.loaded / (e.loaded + 1000000);
}
this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
},
/**
* Exports PCM data into a JSON array and opens in a new window.
*/
exportPCM: function (length, accuracy, noWindow) {
length = length || 1024;
accuracy = accuracy || 10000;
noWindow = noWindow || false;
var peaks = this.backend.getPeaks(length, accuracy);
var arr = [].map.call(peaks, function (val) {
return Math.round(val * accuracy) / accuracy;
});
var json = JSON.stringify(arr);
if (!noWindow) {
window.open('data:application/json;charset=utf-8,' +
encodeURIComponent(json));
}
return json;
},
/**
* Display empty waveform.
*/
empty: function () {
if (!this.backend.isPaused()) {
this.stop();
this.backend.disconnectSource();
}
this.drawer.progress(0);
this.drawer.setWidth(0);
this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
},
/**
* Remove events, elements and disconnect WebAudio nodes.
*/
destroy: function () {
this.fireEvent('destroy');
this.unAll();
this.backend.destroy();
this.drawer.destroy();
}
};
/* Observer */
WaveSurfer.Observer = {
on: function (event, fn) {
if (!this.handlers) { this.handlers = {}; }
var handlers = this.handlers[event];
if (!handlers) {
handlers = this.handlers[event] = [];
}
handlers.push(fn);
},
un: function (event, fn) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
if (handlers) {
if (fn) {
for (var i = handlers.length - 1; i >= 0; i--) {
if (handlers[i] == fn) {
handlers.splice(i, 1);
}
}
} else {
handlers.length = 0;
}
}
},
unAll: function () {
this.handlers = null;
},
once: function (event, handler) {
var my = this;
var fn = function () {
handler();
setTimeout(function () {
my.un(event, fn);
}, 0);
};
this.on(event, fn);
},
fireEvent: function (event) {
if (!this.handlers) { return; }
var handlers = this.handlers[event];
var args = Array.prototype.slice.call(arguments, 1);
handlers && handlers.forEach(function (fn) {
fn.apply(null, args);
});
}
};
/* Common utilities */
WaveSurfer.util = {
extend: function (dest) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function (source) {
Object.keys(source).forEach(function (key) {
dest[key] = source[key];
});
});
return dest;
},
getId: function () {
return 'wavesurfer_' + Math.random().toString(32).substring(2);
},
max: function (values, min) {
var max = -Infinity;
for (var i = 0, len = values.length; i < len; i++) {
var val = values[i];
if (min != null) {
val = Math.abs(val - min);
}
if (val > max) { max = val; }
}
return max;
},
ajax: function (options) {
var ajax = Object.create(WaveSurfer.Observer);
var xhr =