import * as util from './util'; import MultiCanvas from './drawer.multicanvas'; import WebAudio from './webaudio'; import MediaElement from './mediaelement'; import PeakCache from './peakcache'; /* * This work is licensed under a BSD-3-Clause License. */ /** @external {HTMLElement} https://developer.mozilla.org/en/docs/Web/API/HTMLElement */ /** @external {OfflineAudioContext} https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext */ /** @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File */ /** @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob */ /** @external {CanvasRenderingContext2D} https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D */ /** @external {MediaStreamConstraints} https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints */ /** @external {AudioNode} https://developer.mozilla.org/de/docs/Web/API/AudioNode */ /** * @typedef {Object} WavesurferParams * @property {AudioContext} audioContext=null Use your own previously * initialized AudioContext or leave blank. * @property {number} audioRate=1 Speed at which to play audio. Lower number is * slower. * @property {boolean} autoCenter=true If a scrollbar is present, center the * waveform around the progress * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'` In most cases * you don't have to set this manually. MediaElement is a fallback for * unsupported browsers. * @property {number} barHeight=1 The height of the wave * @property {boolean} closeAudioContext=false Close and nullify all audio * contexts when the destroy method is called. * @property {!string|HTMLElement} container CSS selector or HTML element where * the waveform should be drawn. This is the only required parameter. * @property {string} cursorColor='#333' The fill color of the cursor indicating * the playhead position. * @property {number} cursorWidth=1 Measured in pixels. * @property {boolean} fillParent=true Whether to fill the entire container or * draw only according to `minPxPerSec`. * @property {boolean} forceDecode=false Force decoding of audio using web audio * when zooming to get a more detailed waveform. * @property {number} height=128 The height of the waveform. Measured in * pixels. * @property {boolean} hideScrollbar=false Whether to hide the horizontal * scrollbar when one would normally be shown. * @property {boolean} interact=true Whether the mouse interaction will be * enabled at initialization. You can switch this parameter at any time later * on. * @property {boolean} loopSelection=true (Use with regions plugin) Enable * looping of selected regions * @property {number} maxCanvasWidth=4000 Maximum width of a single canvas in * pixels, excluding a small overlap (2 * `pixelRatio`, rounded up to the next * even integer). If the waveform is longer than this value, additional canvases * will be used to render the waveform, which is useful for very large waveforms * that may be too wide for browsers to draw on a single canvas. * @property {boolean} mediaControls=false (Use with backend `MediaElement`) * this enables the native controls for the media element * @property {string} mediaType='audio' (Use with backend `MediaElement`) * `'audio'|'video'` * @property {number} minPxPerSec=20 Minimum number of pixels per second of * audio. * @property {boolean} normalize=false If true, normalize by the maximum peak * instead of 1.0. * @property {boolean} partialRender=false Use the PeakCache to improve * rendering speed of large waveforms * @property {number} pixelRatio=window.devicePixelRatio The pixel ratio used to * calculate display * @property {PluginDefinition[]} plugins=[] An array of plugin definitions to * register during instantiation, they will be directly initialised unless they * are added with the `deferInit` property set to true. * @property {string} progressColor='#555' The fill color of the part of the * waveform behind the cursor. * @property {Object} renderer=MultiCanvas Can be used to inject a custom * renderer. * @property {boolean|number} responsive=false If set to `true` resize the * waveform, when the window is resized. This is debounced with a `100ms` * timeout by default. If this parameter is a number it represents that timeout. * @property {boolean} scrollParent=false Whether to scroll the container with a * lengthy waveform. Otherwise the waveform is shrunk to the container width * (see fillParent). * @property {number} skipLength=2 Number of seconds to skip with the * skipForward() and skipBackward() methods. * @property {boolean} splitChannels=false Render with seperate waveforms for * the channels of the audio * @property {string} waveColor='#999' The fill color of the waveform after the * cursor. */ /** * @typedef {Object} PluginDefinition * @desc The Object used to describe a plugin * @example wavesurfer.addPlugin(pluginDefinition); * @property {string} name The name of the plugin, the plugin instance will be * added as a property to the wavesurfer instance under this name * @property {?Object} staticProps The properties that should be added to the * wavesurfer instance as static properties * @property {?boolean} deferInit Don't initialise plugin * automatically * @property {Object} params={} The plugin parameters, they are the first parameter * passed to the plugin class constructor function * @property {PluginClass} instance The plugin instance factory, is called with * the dependency specified in extends. Returns the plugin class. */ /** * @interface PluginClass * * @desc This is the interface which is implemented by all plugin classes. Note * that this only turns into an observer after being passed through * `wavesurfer.addPlugin`. * * @extends {Observer} */ class PluginClass { /** * Plugin definition factory * * This function must be used to create a plugin definition which can be * used by wavesurfer to correctly instantiate the plugin. * * @param {Object} params={} The plugin params (specific to the plugin) * @return {PluginDefinition} an object representing the plugin */ create(params) {} /** * Construct the plugin * * @param {Object} ws The wavesurfer instance * @param {Object} params={} The plugin params (specific to the plugin) */ constructor(ws, params) {} /** * Initialise the plugin * * Start doing something. This is called by * `wavesurfer.initPlugin(pluginName)` */ init() {} /** * Destroy the plugin instance * * Stop doing something. This is called by * `wavesurfer.destroyPlugin(pluginName)` */ destroy() {} } /** * WaveSurfer core library class * * @extends {Observer} * @example * const params = { * container: '#waveform', * waveColor: 'violet', * progressColor: 'purple' * }; * * // initialise like this * const wavesurfer = WaveSurfer.create(params); * * // or like this ... * const wavesurfer = new WaveSurfer(params); * wavesurfer.init(); * * // load audio file * wavesurfer.load('example/media/demo.wav'); */ export default class WaveSurfer extends util.Observer { /** @private */ defaultParams = { audioContext : null, audioRate : 1, autoCenter : true, backend : 'WebAudio', barHeight : 1, container : null, cursorColor : '#333', cursorWidth : 1, dragSelection : true, fillParent : true, forceDecode : false, height : 128, hideScrollbar : false, interact : true, loopSelection : true, maxCanvasWidth: 4000, mediaContainer: null, mediaControls : false, mediaType : 'audio', minPxPerSec : 20, normalize : false, partialRender : false, pixelRatio : window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI, plugins : [], progressColor : '#555', renderer : MultiCanvas, responsive : false, scrollParent : false, skipLength : 2, splitChannels : false, waveColor : '#999' } /** @private */ backends = { MediaElement, WebAudio } /** * Instantiate this class, call its `init` function and returns it * * @param {WavesurferParams} params * @return {Object} WaveSurfer instance * @example const wavesurfer = WaveSurfer.create(params); */ static create(params) { const wavesurfer = new WaveSurfer(params); return wavesurfer.init(); } /** * Functions in the `util` property are available as a prototype property to * all instances * * @type {Object} * @example * const wavesurfer = WaveSurfer.create(params); * wavesurfer.util.style(myElement, { background: 'blue' }); */ util = util /** * Functions in the `util` property are available as a static property of the * WaveSurfer class * * @type {Object} * @example * WaveSurfer.util.style(myElement, { background: 'blue' }); */ static util = util /** * Initialise wavesurfer instance * * @param {WavesurferParams} params Instantiation options for wavesurfer * @example * const wavesurfer = new WaveSurfer(params); * @returns {this} */ constructor(params) { super(); /** * Extract relevant parameters (or defaults) * @private */ this.params = util.extend({}, this.defaultParams, params); /** @private */ 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) { /** @private */ this.mediaContainer = this.container; } else if (typeof this.params.mediaContainer == 'string') { /** @private */ this.mediaContainer = document.querySelector(this.params.mediaContainer); } else { /** @private */ this.mediaContainer = this.params.mediaContainer; } if (!this.mediaContainer) { throw new Error('Media Container element not found'); } if (this.params.maxCanvasWidth <= 1) { throw new Error('maxCanvasWidth must be greater than 1'); } else if (this.params.maxCanvasWidth % 2 == 1) { throw new Error('maxCanvasWidth must be an even number'); } /** * @private Used to save the current volume when muting so we can * restore once unmuted * @type {number} */ this.savedVolume = 0; /** * @private The current muted state * @type {boolean} */ this.isMuted = false; /** * @private Will hold a list of event descriptors that need to be * cancelled on subsequent loads of audio * @type {Object[]} */ this.tmpEvents = []; /** * @private Holds any running audio downloads * @type {Observer} */ this.currentAjax = null; /** @private */ this.arraybuffer = null; /** @private */ this.drawer = null; /** @private */ this.backend = null; /** @private */ this.peakCache = null; // cache constructor objects if (typeof this.params.renderer !== 'function') { throw new Error('Renderer parameter is invalid'); } /** * @private The uninitialised Drawer class */ this.Drawer = this.params.renderer; /** * @private The uninitialised Backend class */ this.Backend = this.backends[this.params.backend]; /** * @private map of plugin names that are currently initialised */ this.initialisedPluginList = {}; /** @private */ this.isDestroyed = false; /** @private */ this.isReady = false; // responsive debounced event listener. If this.params.responsive is not // set, this is never called. Use 100ms or this.params.responsive as // timeout for the debounce function. let prevWidth = 0; this._onResize = util.debounce(() => { if (prevWidth != this.drawer.wrapper.clientWidth) { prevWidth = this.drawer.wrapper.clientWidth; this.drawBuffer(); } }, typeof this.params.responsive === 'number' ? this.params.responsive : 100); return this; } /** * Initialise the wave * * @example * var wavesurfer = new WaveSurfer(params); * wavesurfer.init(); * @return {this} */ init() { this.registerPlugins(this.params.plugins); this.createDrawer(); this.createBackend(); this.createPeakCache(); return this; } /** * Add and initialise array of plugins (if `plugin.deferInit` is falsey), * this function is called in the init function of wavesurfer * * @param {PluginDefinition[]} plugins An array of plugin definitions * @emits {WaveSurfer#plugins-registered} Called with the array of plugin definitions * @return {this} */ registerPlugins(plugins) { // first instantiate all the plugins plugins.forEach(plugin => this.addPlugin(plugin)); // now run the init functions plugins.forEach(plugin => { // call init function of the plugin if deferInit is falsey // in that case you would manually use initPlugins() if (!plugin.deferInit) { this.initPlugin(plugin.name); } }); this.fireEvent('plugins-registered', plugins); return this; } /** * Add a plugin object to wavesurfer * * @param {PluginDefinition} plugin A plugin definition * @emits {WaveSurfer#plugin-added} Called with the name of the plugin that was added * @example wavesurfer.addPlugin(WaveSurfer.minimap()); * @return {this} */ addPlugin(plugin) { if (!plugin.name) { throw new Error('Plugin does not have a name!'); } if (!plugin.instance) { throw new Error(`Plugin ${plugin.name} does not have an instance property!`); } // staticProps properties are applied to wavesurfer instance if (plugin.staticProps) { Object.keys(plugin.staticProps).forEach(pluginStaticProp => { /** * Properties defined in a plugin definition's `staticProps` property are added as * staticProps properties of the WaveSurfer instance */ this[pluginStaticProp] = plugin.staticProps[pluginStaticProp]; }); } const Instance = plugin.instance; // turn the plugin instance into an observer const observerPrototypeKeys = Object.getOwnPropertyNames(util.Observer.prototype); observerPrototypeKeys.forEach(key => { Instance.prototype[key] = util.Observer.prototype[key]; }); /** * Instantiated plugin classes are added as a property of the wavesurfer * instance * @type {Object} */ this[plugin.name] = new Instance(plugin.params || {}, this); this.fireEvent('plugin-added', plugin.name); return this; } /** * Initialise a plugin * * @param {string} name A plugin name * @emits WaveSurfer#plugin-initialised * @example wavesurfer.initPlugin('minimap'); * @return {this} */ initPlugin(name) { if (!this[name]) { throw new Error(`Plugin ${name} has not been added yet!`); } if (this.initialisedPluginList[name]) { // destroy any already initialised plugins this.destroyPlugin(name); } this[name].init(); this.initialisedPluginList[name] = true; this.fireEvent('plugin-initialised', name); return this; } /** * Destroy a plugin * * @param {string} name A plugin name * @emits WaveSurfer#plugin-destroyed * @example wavesurfer.destroyPlugin('minimap'); * @returns {this} */ destroyPlugin(name) { if (!this[name]) { throw new Error(`Plugin ${name} has not been added yet and cannot be destroyed!`); } if (!this.initialisedPluginList[name]) { throw new Error(`Plugin ${name} is not active and cannot be destroyed!`); } if (typeof this[name].destroy !== 'function') { throw new Error(`Plugin ${name} does not have a destroy function!`); } this[name].destroy(); delete this.initialisedPluginList[name]; this.fireEvent('plugin-destroyed', name); return this; } /** * Destroy all initialised plugins. Convenience function to use when * wavesurfer is removed * * @private */ destroyAllPlugins() { Object.keys(this.initialisedPluginList).forEach(name => this.destroyPlugin(name)); } /** * Create the drawer and draw the waveform * * @private * @emits WaveSurfer#drawer-created */ createDrawer() { this.drawer = new this.Drawer(this.container, this.params); this.drawer.init(); this.fireEvent('drawer-created', this.drawer); if (this.params.responsive) { window.addEventListener('resize', this._onResize, true); } this.drawer.on('redraw', () => { this.drawBuffer(); this.drawer.progress(this.backend.getPlayedPercents()); }); // Click-to-seek this.drawer.on('click', (e, progress) => { setTimeout(() => this.seekTo(progress), 0); }); // Relay the scroll event from the drawer this.drawer.on('scroll', e => { if (this.params.partialRender) { this.drawBuffer(); } this.fireEvent('scroll', e); }); } /** * Create the backend * * @private * @emits WaveSurfer#backend-created */ createBackend() { if (this.backend) { this.backend.destroy(); } // Back compat if (this.params.backend == 'AudioElement') { this.params.backend = 'MediaElement'; } if (this.params.backend == 'WebAudio' && !this.Backend.prototype.supportsWebAudio.call(null)) { this.params.backend = 'MediaElement'; } this.backend = new this.Backend(this.params); this.backend.init(); this.fireEvent('backend-created', this.backend); this.backend.on('finish', () => this.fireEvent('finish')); this.backend.on('play', () => this.fireEvent('play')); this.backend.on('pause', () => this.fireEvent('pause')); this.backend.on('audioprocess', time => { this.drawer.progress(this.backend.getPlayedPercents()); this.fireEvent('audioprocess', time); }); } /** * Create the peak cache * * @private */ createPeakCache() { if (this.params.partialRender) { this.peakCache = new PeakCache(); } } /** * Get the duration of the audio clip * * @example const duration = wavesurfer.getDuration(); * @return {number} Duration in seconds */ getDuration() { return this.backend.getDuration(); } /** * Get the current playback position * * @example const currentTime = wavesurfer.getCurrentTime(); * @return {number} Playback position in seconds */ getCurrentTime() { return this.backend.getCurrentTime(); } /** * Set the current play time in seconds. * * @param {Number} seconds A positive number in seconds. E.g. 10 means 10 * seconds, 60 means 1 minute */ setCurrentTime(seconds) { if (this.getDuration() >= seconds) { this.seekTo(1); } else { this.seekTo(seconds/this.getDuration()); } } /** * Starts playback from the current position. Optional start and end * measured in seconds can be used to set the range of audio to play. * * @param {?number} start Position to start at * @param {?number} end Position to end at * @emits WaveSurfer#interaction * @example * // play from second 1 to 5 * wavesurfer.play(1, 5); */ play(start, end) { this.fireEvent('interaction', () => this.play(start, end)); this.backend.play(start, end); } /** * Stops playback * * @example wavesurfer.pause(); */ pause() { this.backend.isPaused() || this.backend.pause(); } /** * Toggle playback * * @example wavesurfer.playPause(); */ playPause() { this.backend.isPaused() ? this.play() : this.pause(); } /** * Get the current playback state * * @example const isPlaying = wavesurfer.isPlaying(); * @return {boolean} False if paused, true if playing */ isPlaying() { return !this.backend.isPaused(); } /** * Skip backward * * @param {?number} seconds Amount to skip back, if not specified `skipLength` * is used * @example wavesurfer.skipBackward(); */ skipBackward(seconds) { this.skip(-seconds || -this.params.skipLength); } /** * Skip forward * * @param {?number} seconds Amount to skip back, if not specified `skipLength` * is used * @example wavesurfer.skipForward(); */ skipForward(seconds) { this.skip(seconds || this.params.skipLength); } /** * Skip a number of seconds from the current position (use a negative value * to go backwards). * * @param {number} offset Amount to skip back or forwards * @example * // go back 2 seconds * wavesurfer.skip(-2); */ skip(offset) { const duration = this.getDuration() || 1; let position = this.getCurrentTime() || 0; position = Math.max(0, Math.min(duration, position + (offset || 0))); this.seekAndCenter(position / duration); } /** * Seeks to a position and centers the view * * @param {number} progress Between 0 (=beginning) and 1 (=end) * @example * // seek and go to the middle of the audio * wavesurfer.seekTo(0.5); */ seekAndCenter(progress) { this.seekTo(progress); this.drawer.recenter(progress); } /** * Seeks to a position * * @param {number} progress Between 0 (=beginning) and 1 (=end) * @emits WaveSurfer#interaction * @emits WaveSurfer#seek * @example * // seek to the middle of the audio * wavesurfer.seekTo(0.5); */ seekTo(progress) { this.fireEvent('interaction', () => this.seekTo(progress)); const paused = this.backend.isPaused(); // avoid draw wrong position while playing backward seeking if (!paused) { this.backend.pause(); } // avoid small scrolls while paused seeking const oldScrollParent = this.params.scrollParent; this.params.scrollParent = false; this.backend.seekTo(progress * this.getDuration()); this.drawer.progress(this.backend.getPlayedPercents()); if (!paused) { this.backend.play(); } this.params.scrollParent = oldScrollParent; this.fireEvent('seek', progress); } /** * Stops and goes to the beginning. * * @example wavesurfer.stop(); */ stop() { 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(newVolume) { this.backend.setVolume(newVolume); } /** * Get the playback volume. * * @return {number} A value between 0 and 1, 0 being no * volume and 1 being full volume. */ getVolume () { return this.backend.getVolume(); } /** * 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. * @example wavesurfer.setPlaybackRate(2); */ setPlaybackRate(rate) { this.backend.setPlaybackRate(rate); } /** * Get the playback rate. * * @return {number} */ getPlaybackRate() { return this.backend.getPlaybackRate(); } /** * 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. * * @example wavesurfer.toggleMute(); */ toggleMute() { this.setMute(!this.isMuted); } /** * Enable or disable muted audio * * @param {boolean} mute * @example * // unmute * wavesurfer.setMute(false); */ setMute(mute) { // ignore all muting requests if the audio is already in that state if (mute === this.isMuted) { return; } if (mute) { // 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; } else { // If currently muted then restore to the saved volume // and update the mute properties this.backend.setVolume(this.savedVolume); this.isMuted = false; } } /** * Get the current mute status. * * @example const isMuted = wavesurfer.getMute(); * @return {boolean} */ getMute() { return this.isMuted; } /** * Get the list of current set filters as an array. * * Filters must be set with setFilters method first * * @return {array} */ getFilters() { return this.backend.filters || []; } /** * Toggles `scrollParent` and redraws * * @example wavesurfer.toggleScroll(); */ toggleScroll() { this.params.scrollParent = !this.params.scrollParent; this.drawBuffer(); } /** * Toggle mouse interaction * * @example wavesurfer.toggleInteraction(); */ toggleInteraction() { this.params.interact = !this.params.interact; } /** * Get the correct peaks for current wave viewport and render wave * * @private * @emits WaveSurfer#redraw */ drawBuffer() { const nominalWidth = Math.round( this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio ); const parentWidth = this.drawer.getWidth(); let width = nominalWidth; let start = this.drawer.getScrollX(); let end = Math.max(start + parentWidth, width); // Fill container if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) { width = parentWidth; start = 0; end = width; } let peaks; if (this.params.partialRender) { const newRanges = this.peakCache.addRangeToPeakCache(width, start, end); let i; for (i = 0; i < newRanges.length; i++) { peaks = this.backend.getPeaks(width, newRanges[i][0], newRanges[i][1]); this.drawer.drawPeaks(peaks, width, newRanges[i][0], newRanges[i][1]); } } else { peaks = this.backend.getPeaks(width, start, end); this.drawer.drawPeaks(peaks, width, start, end); } this.fireEvent('redraw', peaks, width); } /** * Horizontally zooms the waveform in and out. It also changes the parameter * `minPxPerSec` and enables the `scrollParent` option. Calling the function * with a falsey parameter will reset the zoom state. * * @param {?number} pxPerSec Number of horizontal pixels per second of * audio, if none is set the waveform returns to unzoomed state * @emits WaveSurfer#zoom * @example wavesurfer.zoom(20); */ zoom(pxPerSec) { if (!pxPerSec) { this.params.minPxPerSec = this.defaultParams.minPxPerSec; this.params.scrollParent = false; } else { this.params.minPxPerSec = pxPerSec; this.params.scrollParent = true; } this.drawBuffer(); this.drawer.progress(this.backend.getPlayedPercents()); this.drawer.recenter( this.getCurrentTime() / this.getDuration() ); this.fireEvent('zoom', pxPerSec); } /** * Decode buffer and load * * @private * @param {ArrayBuffer} arraybuffer */ loadArrayBuffer(arraybuffer) { this.decodeArrayBuffer(arraybuffer, data => { if (!this.isDestroyed) { this.loadDecodedBuffer(data); } }); } /** * Directly load an externally decoded AudioBuffer * * @private * @param {AudioBuffer} buffer * @emits WaveSurfer#ready */ loadDecodedBuffer(buffer) { this.backend.load(buffer); this.drawBuffer(); this.fireEvent('ready'); this.isReady = true; } /** * Loads audio data from a Blob or File object * * @param {Blob|File} blob Audio data * @example */ loadBlob(blob) { // Create file reader const reader = new FileReader(); reader.addEventListener('progress', e => this.onProgress(e)); reader.addEventListener('load', e => this.loadArrayBuffer(e.target.result)); reader.addEventListener('error', () => this.fireEvent('error', 'Error reading file')); reader.readAsArrayBuffer(blob); this.empty(); } /** * Loads audio and re-renders the waveform. * * @param {string} url The url of the audio file * @param {?number[]|number[][]} peaks Wavesurfer does not have to decode the audio to * render the waveform if this is specified * @param {?string} preload (Use with backend `MediaElement`) * `'none'|'metadata'|'auto'` Preload attribute for the media element * @example * // using ajax or media element to load (depending on backend) * wavesurfer.load('http://example.com/demo.wav'); * * // setting preload attribute with media element backend and supplying * peaks wavesurfer.load( * 'http://example.com/demo.wav', * [0.0218, 0.0183, 0.0165, 0.0198, 0.2137, 0.2888], * true, * ); */ load(url, peaks, preload) { this.empty(); this.isMuted = false; switch (this.params.backend) { case 'WebAudio': return this.loadBuffer(url, peaks); case 'MediaElement': return this.loadMediaElement(url, peaks, preload); } } /** * Loads audio using Web Audio buffer backend. * * @private * @param {string} url * @param {?number[]|number[][]} peaks */ loadBuffer(url, peaks) { const load = action => { if (action) { this.tmpEvents.push(this.once('ready', action)); } return this.getArrayBuffer(url, (data) => this.loadArrayBuffer(data)); }; if (peaks) { this.backend.setPeaks(peaks); this.drawBuffer(); this.tmpEvents.push(this.once('interaction', load)); } else { return load(); } } /** * Either create a media element, or load an existing media element. * * @private * @param {string|HTMLElement} urlOrElt Either a path to a media file, or an * existing HTML5 Audio/Video Element * @param {number[]|number[][]} peaks Array of peaks. Required to bypass web audio * dependency * @param {?boolean} preload Set to true if the preload attribute of the * audio element should be enabled */ loadMediaElement(urlOrElt, peaks, preload) { let url = urlOrElt; if (typeof urlOrElt === 'string') { this.backend.load(url, this.mediaContainer, peaks, preload); } else { const elt = urlOrElt; this.backend.loadElt(elt, peaks); // If peaks are not provided, // url = element.src so we can get peaks with web audio url = elt.src; } this.tmpEvents.push( this.backend.once('canplay', () => { this.drawBuffer(); this.fireEvent('ready'); this.isReady = true; }), this.backend.once('error', err => this.fireEvent('error', err)) ); // If no pre-decoded peaks provided or pre-decoded peaks are // provided with forceDecode flag, attempt to download the // audio file and decode it with Web Audio. if (peaks) { this.backend.setPeaks(peaks); } if ((!peaks || this.params.forceDecode) && this.backend.supportsWebAudio()) { this.getArrayBuffer(url, arraybuffer => { this.decodeArrayBuffer(arraybuffer, buffer => { this.backend.buffer = buffer; this.backend.setPeaks(null); this.drawBuffer(); this.fireEvent('waveform-ready'); }); }); } } /** * Decode an array buffer and pass data to a callback * * @private * @param {Object} arraybuffer * @param {function} callback */ decodeArrayBuffer(arraybuffer, callback) { this.arraybuffer = arraybuffer; this.backend.decodeArrayBuffer( arraybuffer, data => { // Only use the decoded data if we haven't been destroyed or // another decode started in the meantime if (!this.isDestroyed && this.arraybuffer == arraybuffer) { callback(data); this.arraybuffer = null; } }, () => this.fireEvent('error', 'Error decoding audiobuffer') ); } /** * Load an array buffer by ajax and pass to a callback * * @param {string} url * @param {function} callback * @private */ getArrayBuffer(url, callback) { const ajax = util.ajax({ url: url, responseType: 'arraybuffer' }); this.currentAjax = ajax; this.tmpEvents.push( ajax.on('progress', e => { this.onProgress(e); }), ajax.on('success', (data, e) => { callback(data); this.currentAjax = null; }), ajax.on('error', e => { this.fireEvent('error', 'XHR error: ' + e.target.statusText); this.currentAjax = null; }) ); return ajax; } /** * Called while the audio file is loading * * @private * @param {Event} e * @emits WaveSurfer#loading */ onProgress(e) { let percentComplete; if (e.lengthComputable) { 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. * * @param {number} length=1024 The scale in which to export the peaks. (Integer) * @param {number} accuracy=10000 (Integer) * @param {?boolean} noWindow Set to true to disable opening a new * window with the JSON * @param {number} start * @todo Update exportPCM to work with new getPeaks signature * @return {JSON} JSON of peaks */ exportPCM(length, accuracy, noWindow, start) { length = length || 1024; start = start || 0; accuracy = accuracy || 10000; noWindow = noWindow || false; const peaks = this.backend.getPeaks(length, start); const arr = [].map.call(peaks, val => Math.round(val * accuracy) / accuracy); const json = JSON.stringify(arr); if (!noWindow) { window.open('data:application/json;charset=utf-8,' + encodeURIComponent(json)); } return json; } /** * Save waveform image as data URI. * * The default format is `image/png`. Other supported types are * `image/jpeg` and `image/webp`. * * @param {string} format='image/png' * @param {number} quality=1 * @return {string} data URI of image */ exportImage(format, quality) { if (!format) { format = 'image/png'; } if (!quality) { quality = 1; } return this.drawer.getImage(format, quality); } /** * Cancel any ajax request currently in progress */ cancelAjax() { if (this.currentAjax) { this.currentAjax.xhr.abort(); this.currentAjax = null; } } /** * @private */ clearTmpEvents() { this.tmpEvents.forEach(e => e.un()); } /** * Display empty waveform. */ empty() { 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. * * @emits WaveSurfer#destroy */ destroy() { this.destroyAllPlugins(); this.fireEvent('destroy'); this.cancelAjax(); this.clearTmpEvents(); this.unAll(); if (this.params.responsive) { window.removeEventListener('resize', this._onResize, true); } this.backend.destroy(); this.drawer.destroy(); this.isDestroyed = true; this.arraybuffer = null; } }