// ==UserScript==
// @name WaveSurfer - Tampermonkey
// @namespace https://wavesurfer-js.org/
// @version 1.0
// @description WaveSurfer
// @author katspaugh
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module unless amdModuleId is set
define('wavesurfer', [], function () {
return (root['WaveSurfer'] = factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
root['WaveSurfer'] = factory();
}
}(this, function () {
'use strict';
var WaveSurfer = {
defaultParams: {
height : 128,
waveColor : '#999',
progressColor : '#555',
cursorColor : '#333',
cursorWidth : 1,
skipLength : 2,
minPxPerSec : 20,
pixelRatio : window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
fillParent : true,
scrollParent : false,
hideScrollbar : false,
normalize : false,
audioContext : null,
container : null,
dragSelection : true,
loopSelection : true,
audioRate : 1,
interact : true,
splitChannels : false,
mediaContainer: null,
mediaControls : false,
renderer : 'Canvas',
backend : 'WebAudio',
mediaType : 'audio',
autoCenter : true
},
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');
}
if (this.params.mediaContainer == null) {
this.mediaContainer = this.container;
} else if (typeof this.params.mediaContainer == 'string') {
this.mediaContainer = document.querySelector(this.params.mediaContainer);
} else {
this.mediaContainer = this.params.mediaContainer;
}
if (!this.mediaContainer) {
throw new Error('Media 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;
// Will hold a list of event descriptors that need to be
// cancelled on subsequent loads of audio
this.tmpEvents = [];
// Holds any running audio downloads
this.currentAjax = null;
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();
}
// Back compat
if (this.params.backend == 'AudioElement') {
this.params.backend = 'MediaElement';
}
if (this.params.backend == 'WebAudio' && !WaveSurfer.WebAudio.supportsWebAudio()) {
this.params.backend = 'MediaElement';
}
this.backend = Object.create(WaveSurfer[this.params.backend]);
this.backend.init(this.params);
this.backend.on('finish', function () { my.fireEvent('finish'); });
this.backend.on('play', function () { my.fireEvent('play'); });
this.backend.on('pause', function () { my.fireEvent('pause'); });
this.backend.on('audioprocess', function (time) {
my.drawer.progress(my.backend.getPlayedPercents());
my.fireEvent('audioprocess', time);
});
},
getDuration: function () {
return this.backend.getDuration();
},
getCurrentTime: function () {
return this.backend.getCurrentTime();
},
play: function (start, end) {
this.backend.play(start, end);
},
pause: function () {
this.backend.pause();
},
playPause: function () {
this.backend.isPaused() ? this.play() : this.pause();
},
isPlaying: function () {
return !this.backend.isPaused();
},
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 rate.
*
* @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 () {
var nominalWidth = Math.round(
this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
);
var parentWidth = this.drawer.getWidth();
var width = nominalWidth;
// Fill container
if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
width = parentWidth;
}
var peaks = this.backend.getPeaks(width);
this.drawer.drawPeaks(peaks, width);
this.fireEvent('redraw', peaks, width);
},
zoom: function (pxPerSec) {
this.params.minPxPerSec = pxPerSec;
this.params.scrollParent = true;
this.drawBuffer();
this.seekAndCenter(
this.getCurrentTime() / this.getDuration()
);
this.fireEvent('zoom', pxPerSec);
},
/**
* Internal method.
*/
loadArrayBuffer: function (arraybuffer) {
this.decodeArrayBuffer(arraybuffer, function (data) {
this.loadDecodedBuffer(data);
}.bind(this));
},
/**
* Directly load an externally decoded AudioBuffer.
*/
loadDecodedBuffer: function (buffer) {
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.loadArrayBuffer(e.target.result);
});
reader.addEventListener('error', function () {
my.fireEvent('error', 'Error reading file');
});
reader.readAsArrayBuffer(blob);
this.empty();
},
/**
* Loads audio and rerenders the waveform.
*/
load: function (url, peaks) {
switch (this.params.backend) {
case 'WebAudio': return this.loadBuffer(url);
case 'MediaElement': return this.loadMediaElement(url, peaks);
}
},
/**
* Loads audio using Web Audio buffer backend.
*/
loadBuffer: function (url) {
this.empty();
// load via XHR and render all at once
return this.getArrayBuffer(url, this.loadArrayBuffer.bind(this));
},
/**
* Either create a media element, or load
* an existing media element.
* @param {String|HTMLElement} urlOrElt Either a path to a media file,
* or an existing HTML5 Audio/Video
* Element
* @param {Array} [peaks] Array of peaks. Required to bypass
* web audio dependency
*/
loadMediaElement: function (urlOrElt, peaks) {
this.empty();
var url, elt;
if (typeof urlOrElt === 'string') {
url = urlOrElt;
this.backend.load(url, this.mediaContainer, peaks);
} else {
elt = urlOrElt;
this.backend.loadElt(elt, peaks);
// if peaks are not provided,
// url = element.src so we can get peaks with web audio
if (!peaks) {
url = elt.src;
}
}
this.tmpEvents.push(
this.backend.once('canplay', (function () {
this.drawBuffer();
this.fireEvent('ready');
}).bind(this)),
this.backend.once('error', (function (err) {
this.fireEvent('error', err);
}).bind(this))
);
// If no pre-decoded peaks provided, attempt to download the
// audio file and decode it with Web Audio.
if (url && !peaks && this.backend.supportsWebAudio()) {
this.getArrayBuffer(url, (function (arraybuffer) {
this.decodeArrayBuffer(arraybuffer, (function (buffer) {
this.backend.buffer = buffer;
this.drawBuffer();
}).bind(this));
}).bind(this));
}
},
decodeArrayBuffer: function (arraybuffer, callback) {
this.backend.decodeArrayBuffer(
arraybuffer,
this.fireEvent.bind(this, 'decoded'),
this.fireEvent.bind(this, 'error', 'Error decoding audiobuffer')
);
this.tmpEvents.push(
this.once('decoded', callback)
);
},
getArrayBuffer: function (url, callback) {
var my = this;
var ajax = WaveSurfer.util.ajax({
url: url,
responseType: 'arraybuffer'
});
this.currentAjax = ajax;
this.tmpEvents.push(
ajax.on('progress', function (e) {
my.onProgress(e);
}),
ajax.on('success', function (data, e) {
callback(data);
my.currentAjax = null;
}),
ajax.on('error', function (e) {
my.fireEvent('error', 'XHR error: ' + e.target.statusText);
my.currentAjax = null;
})
);
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;
},
cancelAjax: function () {
if (this.currentAjax) {
this.currentAjax.xhr.abort();
this.currentAjax = null;
}
},
clearTmpEvents: function () {
this.tmpEvents.forEach(function (e) { e.un(); });
},
/**
* Display empty waveform.
*/
empty: function () {
if (!this.backend.isPaused()) {
this.stop();
this.backend.disconnectSource();
}
this.cancelAjax();
this.clearTmpEvents();
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.cancelAjax();
this.clearTmpEvents();
this.unAll();
this.backend.destroy();
this.drawer.destroy();
}
};
WaveSurfer.create = function (params) {
var wavesurfer = Object.create(WaveSurfer);
wavesurfer.init(params);
return wavesurfer;
};
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;
},
min: function(values) {
var min = +Infinity;
for (var i in values) {
if (values[i] < min) {
min = values[i];
}
}
return min;
},
max: function(values) {
var max = -Infinity;
for (var i in values) {
if (values[i] > max) {
max = values[i];
}
}
return max;
},
getId: function () {
return 'wavesurfer_' + Math.random().toString(32).substring(2);
},
ajax: function (options) {
var ajax = Object.create(WaveSurfer.Observer);
var xhr = GM_xmlhttpRequest({
method: options.method || 'GET',
url: options.url,
responseType: options.responseType || 'json',
onprogress: function (e) {
ajax.fireEvent('progress', e);
if (e.lengthComputable && e.loaded == e.total) {
fired100 = true;
}
},
onload: function (e) {
if (!fired100) {
ajax.fireEvent('progress', e);
}
ajax.fireEvent('load', e);
if (200 == e.status || 206 == e.status) {
ajax.fireEvent('success', e.response, e);
} else {
ajax.fireEvent('error', e);
}
},
onerror: function (e) {
ajax.fireEvent('error', e);
}
});
var fired100 = false;
ajax.xhr = xhr;
return ajax;
}
};
/* Observer */
WaveSurfer.Observer = {
/**
* Attach a handler function for an event.
*/
on: function (event, fn) {
if (!this.handlers) { this.handlers = {}; }
var handlers = this.handlers[event];
if (!handlers) {
handlers = this.handlers[event] = [];
}
handlers.push(fn);
// Return an event descriptor
return {
name: event,
callback: fn,
un: this.un.bind(this, event, fn)
};
},
/**
* Remove an event handler.
*/
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;
}
}
},
/**
* Remove all event handlers.
*/
unAll: function () {
this.handlers = null;
},
/**
* Attach a handler to an event. The handler is executed at most once per
* event type.
*/
once: function (event, handler) {
var my = this;
var fn = function () {
handler.apply(this, arguments);
setTimeout(function () {
my.un(event, fn);
}, 0);
};
return 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);
});
}
};
/* Make the main WaveSurfer object an observer */
WaveSurfer.util.extend(WaveSurfer, WaveSurfer.Observer);
'use strict';
WaveSurfer.WebAudio = {
scriptBufferSize: 256,
PLAYING_STATE: 0,
PAUSED_STATE: 1,
FINISHED_STATE: 2,
supportsWebAudio: function () {
return !!(window.AudioContext || window.webkitAudioContext);
},
getAudioContext: function () {
if (!WaveSurfer.WebAudio.audioContext) {
WaveSurfer.WebAudio.audioContext = new (
window.AudioContext || window.webkitAudioContext
);
}
return WaveSurfer.WebAudio.audioContext;
},
getOfflineAudioContext: function (sampleRate) {
if (!WaveSurfer.WebAudio.offlineAudioContext) {
WaveSurfer.WebAudio.offlineAudioContext = new (
window.OfflineAudioContext || window.webkitOfflineAudioContext
)(1, 2, sampleRate);
}
return WaveSurfer.WebAudio.offlineAudioContext;
},
init: function (params) {
this.params = params;
this.ac = params.audioContext || this.getAudioContext();
this.lastPlay = this.ac.currentTime;
this.startPosition = 0;
this.scheduledPause = null;
this.states = [
Object.create(WaveSurfer.WebAudio.state.playing),
Object.create(WaveSurfer.WebAudio.state.paused),
Object.create(WaveSurfer.WebAudio.state.finished)
];
this.createVolumeNode();
this.createScriptNode();
this.createAnalyserNode();
this.setState(this.PAUSED_STATE);
this.setPlaybackRate(this.params.audioRate);
},
disconnectFilters: function () {
if (this.filters) {
this.filters.forEach(function (filter) {
filter && filter.disconnect();
});
this.filters = null;
// Reconnect direct path
this.analyser.connect(this.gainNode);
}
},
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) {
// Remove existing filters
this.disconnectFilters();
// Insert filters if filter array not empty
if (filters && filters.length) {
this.filters = filters;
// Disconnect direct path before inserting filters
this.analyser.disconnect();
// Connect each filter in turn
filters.reduce(function (prev, curr) {
prev.connect(curr);
return curr;
}, this.analyser).connect(this.gainNode);
}
},
createScriptNode: function () {
if (this.ac.createScriptProcessor) {
this.scriptNode = this.ac.createScriptProcessor(this.scriptBufferSize);
} else {
this.scriptNode = this.ac.createJavaScriptNode(this.scriptBufferSize);
}
this.scriptNode.connect(this.ac.destination);
},
addOnAudioProcess: function () {
var my = this;
this.scriptNode.onaudioprocess = function () {
var time = my.getCurrentTime();
if (time >= my.getDuration()) {
my.setState(my.FINISHED_STATE);
my.fireEvent('pause');
} else if (time >= my.scheduledPause) {
my.setState(my.PAUSED_STATE);
my.fireEvent('pause');
} else if (my.state === my.states[my.PLAYING_STATE]) {
my.fireEvent('audioprocess', time);
}
};
},
removeOnAudioProcess: function () {
this.scriptNode.onaudioprocess = null;
},
createAnalyserNode: function () {
this.analyser = this.ac.createAnalyser();
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) {
if (!this.offlineAc) {
this.offlineAc = this.getOfflineAudioContext(this.ac ? this.ac.sampleRate : 44100);
}
this.offlineAc.decodeAudioData(arraybuffer, (function (data) {
callback(data);
}).bind(this), errback);
},
/**
* Compute the max and min value of the waveform when broken into
* <length> subranges.
* @param {Number} How many subranges to break the waveform into.
* @returns {Array} Array of 2*<length> peaks or array of arrays
* of peaks consisting of (max, min) values for each subrange.
*/
getPeaks: function (length) {
var sampleSize = this.buffer.length / length;
var sampleStep = ~~(sampleSize / 10) || 1;
var channels = this.buffer.numberOfChannels;
var splitPeaks = [];
var mergedPeaks = [];
for (var c = 0; c < channels; c++) {
var peaks = splitPeaks[c] = [];
var chan = this.buffer.getChannelData(c);
for (var i = 0; i < length; i++) {
var start = ~~(i * sampleSize);
var end = ~~(start + sampleSize);
var min = 0;
var max = 0;
for (var j = start; j < end; j += sampleStep) {
var value = chan[j];
if (value > max) {
max = value;
}
if (value < min) {
min = value;
}
}
peaks[2 * i] = max;
peaks[2 * i + 1] = min;
if (c == 0 || max > mergedPeaks[2 * i]) {
mergedPeaks[2 * i] = max;
}
if (c == 0 || min < mergedPeaks[2 * i + 1]) {
mergedPeaks[2 * i + 1] = min;
}
}
}
return this.params.splitChannels ? splitPeaks : mergedPeaks;
},
getPlayedPercents: function () {
return this.state.getPlayedPercents.call(this);
},
disconnectSource: function () {
if (this.source) {
this.source.disconnect();
}
},
destroy: function () {
if (!this.isPaused()) {
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 () {
if (!this.buffer) {
return 0;
}
return this.buffer.duration;
},
seekTo: function (start, end) {
this.scheduledPause = null;
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.scheduledPause = end;
this.source.start(0, start, end - start);
this.setState(this.PLAYING_STATE);
this.fireEvent('play');
},
/**
* Pauses the loaded audio.
*/
pause: function () {
this.scheduledPause = null;
this.startPosition += this.getPlayedTime();
this.source && this.source.stop(0);
this.setState(this.PAUSED_STATE);
this.fireEvent('pause');
},
/**
* 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 () {
this.addOnAudioProcess();
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition + this.getPlayedTime();
}
};
WaveSurfer.WebAudio.state.paused = {
init: function () {
this.removeOnAudioProcess();
},
getPlayedPercents: function () {
var duration = this.getDuration();
return (this.getCurrentTime() / duration) || 0;
},
getCurrentTime: function () {
return this.startPosition;
}
};
WaveSurfer.WebAudio.state.finished = {
init: function () {
this.removeOnAudioProcess();
this.fireEvent('finish');
},
getPlayedPercents: function () {
return 1;
},
getCurrentTime: function () {
return this.getDuration();
}
};
WaveSurfer.util.extend(WaveSurfer.WebAudio, WaveSurfer.Observer);
'use strict';
WaveSurfer.MediaElement = Object.create(WaveSurfer.WebAudio);
WaveSurfer.util.extend(WaveSurfer.MediaElement, {
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 () {}
};
this.mediaType = params.mediaType.toLowerCase();
this.elementPosition = params.elementPosition;
this.setPlaybackRate(this.params.audioRate);
this.createTimer();
},
/**
* Create a timer to provide a more precise `audioprocess' event.
*/
createTimer: function () {
var my = this;
var playing = false;
var onAudioProcess = function () {
if (my.isPaused()) { return; }
my.fireEvent('audioprocess', my.getCurrentTime());
// Call again in the next frame
var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
requestAnimationFrame(onAudioProcess);
};
this.on('play', onAudioProcess);
},
/**
* Create media element with url as its source,
* and append to container element.
* @param {String} url path to media file
* @param {HTMLElement} container HTML element
* @param {Array} peaks array of peak data
*/
load: function (url, container, peaks) {
var my = this;
var media = document.createElement(this.mediaType);
media.controls = this.params.mediaControls;
media.autoplay = this.params.autoplay || false;
media.preload = 'auto';
media.src = url;
media.style.width = '100%';
var prevMedia = container.querySelector(this.mediaType);
if (prevMedia) {
container.removeChild(prevMedia);
}
container.appendChild(media);
this._load(media, peaks);
},
/**
* Load existing media element.
* @param {MediaElement} elt HTML5 Audio or Video element
* @param {Array} peaks array of peak data
*/
loadElt: function (elt, peaks) {
var my = this;
var media = elt;
media.controls = this.params.mediaControls;
media.autoplay = this.params.autoplay || false;
this._load(media, peaks);
},
/**
* Private method called by both load (from url)
* and loadElt (existing media element).
* @param {MediaElement} media HTML5 Audio or Video element
* @param {Array} peaks array of peak data
* @private
*/
_load: function (media, peaks) {
var my = this;
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');
});
this.media = media;
this.peaks = peaks;
this.onPlayEnd = null;
this.buffer = null;
this.setPlaybackRate(this.playbackRate);
},
isPaused: function () {
return !this.media || this.media.paused;
},
getDuration: function () {
var duration = this.media.duration;
if (duration >= Infinity) { // streaming audio
duration = this.media.seekable.end(0);
}
return duration;
},
getCurrentTime: function () {
return this.media && 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;
}
this.clearPlayEnd();
},
/**
* Plays the loaded audio region.
*
* @param {Number} start Start offset in seconds,
* relative to the beginning of a clip.
* @param {Number} end End offset in seconds,
* relative to the beginning of a clip.
*/
play: function (start, end) {
this.seekTo(start);
this.media.play();
end && this.setPlayEnd(end);
this.fireEvent('play');
},
/**
* Pauses the loaded audio.
*/
pause: function () {
this.media && this.media.pause();
this.clearPlayEnd();
this.fireEvent('pause');
},
setPlayEnd: function (end) {
var my = this;
this.onPlayEnd = function (time) {
if (time >= end) {
my.pause();
my.seekTo(end);
}
};
this.on('audioprocess', this.onPlayEnd);
},
clearPlayEnd: function () {
if (this.onPlayEnd) {
this.un('audioprocess', this.onPlayEnd);
this.onPlayEnd = null;
}
},
getPeaks: function (length) {
if (this.buffer) {
return WaveSurfer.WebAudio.getPeaks.call(this, 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 && this.media.parentNode && this.media.parentNode.removeChild(this.media);
this.media = null;
}
});
//For backwards compatibility
WaveSurfer.AudioElement = WaveSurfer.MediaElement;
'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.initDrawer(params);
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();
var nominalWidth = this.width;
var parentWidth = this.getWidth();
var progress;
if (!this.params.fillParent && nominalWidth < parentWidth) {
progress = ((e.clientX - bbox.left) * this.params.pixelRatio / nominalWidth) || 0;
if (progress > 1) {
progress = 1;
}
} else {
progress = ((e.clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0;
}
return progress;
},
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));
}
});
this.wrapper.addEventListener('scroll', function (e) {
my.fireEvent('scroll', e);
});
},
drawPeaks: function (peaks, length) {
this.resetScroll();
this.setWidth(length);
this.params.barWidth ?
this.drawBars(peaks) :
this.drawWave(peaks);
},
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 () {
if (this.wrapper !== null) {
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.updateSize();
},
setHeight: function (height) {
if (height == this.height) { return; }
this.height = height;
this.style(this.wrapper, {
height: ~~(this.height / this.params.pixelRatio) + 'px'
});
this.updateSize();
},
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 && this.params.autoCenter) {
var newPos = ~~(this.wrapper.scrollWidth * progress);
this.recenterOnPosition(newPos);
}
this.updateProgress(progress);
}
},
destroy: function () {
this.unAll();
if (this.wrapper) {
this.container.removeChild(this.wrapper);
this.wrapper = null;
}
},
/* Renderer-specific methods */
initDrawer: function () {},
createElements: function () {},
updateSize: 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,
left: 0,
top: 0,
bottom: 0
})
);
this.waveCc = waveCanvas.getContext('2d');
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
})
);
if (this.params.waveColor != this.params.progressColor) {
var progressCanvas = this.progressWave.appendChild(
document.createElement('canvas')
);
this.progressCc = progressCanvas.getContext('2d');
}
},
updateSize: function () {
var width = Math.round(this.width / this.params.pixelRatio);
this.waveCc.canvas.width = this.width;
this.waveCc.canvas.height = this.height;
this.style(this.waveCc.canvas, { width: width + 'px'});
this.style(this.progressWave, { display: 'block'});
if (this.progressCc) {
this.progressCc.canvas.width = this.width;
this.progressCc.canvas.height = this.height;
this.style(this.progressCc.canvas, { width: width + 'px'});
}
this.clearWave();
},
clearWave: function () {
this.waveCc.clearRect(0, 0, this.width, this.height);
if (this.progressCc) {
this.progressCc.clearRect(0, 0, this.width, this.height);
}
},
drawBars: function (peaks, channelIndex) {
// 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(this.drawBars, this);
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; });
if (hasMinVals) {
peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
}
// A half-pixel offset makes lines crisp
var $ = 0.5 / this.params.pixelRatio;
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 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) {
absmax = Math.max.apply(Math, peaks);
}
var scale = length / width;
this.waveCc.fillStyle = this.params.waveColor;
if (this.progressCc) {
this.progressCc.fillStyle = this.params.progressColor;
}
[ 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);
cc.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2);
}
}, this);
},
drawWave: function (peaks, channelIndex) {
// 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(this.drawWave, this);
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 $ = 0.5 / this.params.pixelRatio;
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = ~~(peaks.length / 2);
var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = this.width / length;
}
var absmax = 1;
if (this.params.normalize) {
var max = Math.max.apply(Math, peaks);
var min = Math.min.apply(Math, peaks);
absmax = -min > max ? -min : max;
}
this.waveCc.fillStyle = this.params.waveColor;
if (this.progressCc) {
this.progressCc.fillStyle = this.params.progressColor;
}
[ this.waveCc, this.progressCc ].forEach(function (cc) {
if (!cc) { return; }
cc.beginPath();
cc.moveTo($, halfH + offsetY);
for (var i = 0; i < length; 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--) {
var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
cc.lineTo(i * scale + $, halfH - h + offsetY);
}
cc.closePath();
cc.fill();
// Always draw a median line
cc.fillRect(0, halfH + offsetY - $, this.width, $);
}, this);
},
updateProgress: function (progress) {
var pos = Math.round(
this.width * progress
) / this.params.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
}
});
'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 = {};
var 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);
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) {
// 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(this.drawBars, this);
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; });
if (hasMinVals) {
peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
}
// 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 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) {
absmax = WaveSurfer.util.max(peaks);
}
var scale = length / width;
this.canvases[0].waveCtx.fillStyle = this.params.waveColor;
if (this.canvases[0].progressCtx) {
this.canvases[0].progressCtx.fillStyle = this.params.progressColor;
}
for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
this.fillRect(i + this.halfPixel, halfH - h + offsetY, bar + this.halfPixel, h * 2);
}
},
drawWave: function (peaks, channelIndex) {
// 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(this.drawWave, this);
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 length = ~~(peaks.length / this.canvases.length / 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(length, peaks, absmax, halfH, offsetY);
// Always draw a median line
this.fillRect(0, halfH + offsetY - this.halfPixel, this.width, this.halfPixel);
},
drawLine: function (length, peaks, absmax, halfH, offsetY) {
for (var index in this.canvases) {
var entry = this.canvases[index];
this.setFillStyles(entry);
this.drawLineToContext(entry.waveCtx, length, index, peaks, absmax, halfH, offsetY);
this.drawLineToContext(entry.progressCtx, length, index, peaks, absmax, halfH, offsetY);
}
},
drawLineToContext: function (ctx, length, index, peaks, absmax, halfH, offsetY) {
if (!ctx) { return; }
var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = ctx.canvas.width / length;
}
var first = index * length,
last = first + length + 1;
ctx.beginPath();
ctx.moveTo(this.halfPixel, halfH + offsetY);
for (var i = first; i < last; i++) {
var h = Math.round(peaks[2 * i] / 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);
ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
}
ctx.closePath();
ctx.fill();
},
fillRect: function (x, y, width, height) {
for (var i in this.canvases) {
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 (progress) {
var pos = Math.round(
this.width * progress
) / this.params.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
}
});
'use strict';
/* Init from HTML */
(function () {
var init = function () {
var containers = document.querySelectorAll('wavesurfer');
Array.prototype.forEach.call(containers, function (el) {
var params = WaveSurfer.util.extend({
container: el,
backend: 'MediaElement',
mediaControls: true
}, el.dataset);
el.style.display = 'block';
var wavesurfer = WaveSurfer.create(params);
if (el.dataset.peaks) {
var peaks = JSON.parse(el.dataset.peaks);
}
wavesurfer.load(el.dataset.url, peaks);
});
};
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
}());
return WaveSurfer;
}));