source: gs3-extensions/mars-src/trunk/src/wavesurfer-renderer/wavesurfer.js@ 36964

Last change on this file since 36964 was 34809, checked in by davidb, 3 years ago

Proof of concept stage reached, where AV values are used to drive the width and alpha value of the progress-bar displayed when playing a song

  • Property svn:executable set to *
File size: 53.2 KB
Line 
1import * as util from './util';
2import MultiCanvas from './drawer.multicanvas';
3import SpectrumMultiCanvas from './drawer.spectrummulticanvas';
4import WebAudio from './webaudio';
5import MediaElement from './mediaelement';
6import PeakCache from './peakcache';
7import MediaElementWebAudio from './mediaelement-webaudio';
8
9/*
10 * This work is licensed under a BSD-3-Clause License.
11 */
12
13/** @external {HTMLElement} https://developer.mozilla.org/en/docs/Web/API/HTMLElement */
14/** @external {OfflineAudioContext} https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext */
15/** @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File */
16/** @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob */
17/** @external {CanvasRenderingContext2D} https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D */
18/** @external {MediaStreamConstraints} https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints */
19/** @external {AudioNode} https://developer.mozilla.org/de/docs/Web/API/AudioNode */
20
21/**
22 * @typedef {Object} WavesurferParams
23 * @property {AudioContext} audioContext=null Use your own previously
24 * initialized AudioContext or leave blank.
25 * @property {number} audioRate=1 Speed at which to play audio. Lower number is
26 * slower.
27 * @property {ScriptProcessorNode} audioScriptProcessor=null Use your own previously
28 * initialized ScriptProcessorNode or leave blank.
29 * @property {boolean} autoCenter=true If a scrollbar is present, center the
30 * waveform on current progress
31 * @property {number} autoCenterRate=5 If autoCenter is active, rate at which the
32 * waveform is centered
33 * @property {boolean} autoCenterImmediately=false If autoCenter is active, immediately
34 * center waveform on current progress
35 * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'|'MediaElementWebAudio'` In most cases
36 * you don't have to set this manually. MediaElement is a fallback for unsupported browsers.
37 * MediaElementWebAudio allows to use WebAudio API also with big audio files, loading audio like with
38 * MediaElement backend (HTML5 audio tag). You have to use the same methods of MediaElement backend for loading and
39 * playback, giving also peaks, so the audio data are not decoded. In this way you can use WebAudio features, like filters,
40 * also with audio with big duration. For example:
41 * ` wavesurfer.load(url | HTMLMediaElement, peaks, preload, duration);
42 * wavesurfer.play();
43 * wavesurfer.setFilter(customFilter);
44 * `
45 * @property {string} backgroundColor=null Change background color of the
46 * waveform container.
47 * @property {number} barHeight=1 The height of the wave bars.
48 * @property {number} barRadius=0 The radius of the wave bars. Makes bars rounded
49 * @property {number} barGap=null The optional spacing between bars of the wave,
50 * if not provided will be calculated in legacy format.
51 * @property {number} barWidth=null Draw the waveform using bars.
52 * @property {number} barMinHeight=null If specified, draw at least a bar of this height,
53 * eliminating waveform gaps
54 * @property {boolean} closeAudioContext=false Close and nullify all audio
55 * contexts when the destroy method is called.
56 * @property {!string|HTMLElement} container CSS selector or HTML element where
57 * the waveform should be drawn. This is the only required parameter.
58 * @property {string} cursorColor='#333' The fill color of the cursor indicating
59 * the playhead position.
60 * @property {number} cursorWidth=1 Measured in pixels.
61 * @property {object} drawingContextAttributes={desynchronized: false} Drawing context
62 * attributes.
63 * @property {number} duration=null Optional audio length so pre-rendered peaks
64 * can be display immediately for example.
65 * @property {boolean} fillParent=true Whether to fill the entire container or
66 * draw only according to `minPxPerSec`.
67 * @property {boolean} forceDecode=false Force decoding of audio using web audio
68 * when zooming to get a more detailed waveform.
69 * @property {number} height=128 The height of the waveform. Measured in
70 * pixels.
71 * @property {boolean} hideScrollbar=false Whether to hide the horizontal
72 * scrollbar when one would normally be shown.
73 * @property {boolean} interact=true Whether the mouse interaction will be
74 * enabled at initialization. You can switch this parameter at any time later
75 * on.
76 * @property {boolean} loopSelection=true (Use with regions plugin) Enable
77 * looping of selected regions
78 * @property {number} maxCanvasWidth=4000 Maximum width of a single canvas in
79 * pixels, excluding a small overlap (2 * `pixelRatio`, rounded up to the next
80 * even integer). If the waveform is longer than this value, additional canvases
81 * will be used to render the waveform, which is useful for very large waveforms
82 * that may be too wide for browsers to draw on a single canvas.
83 * @property {boolean} mediaControls=false (Use with backend `MediaElement` or `MediaElementWebAudio`)
84 * this enables the native controls for the media element
85 * @property {string} mediaType='audio' (Use with backend `MediaElement` or `MediaElementWebAudio`)
86 * `'audio'|'video'` ('video' only for `MediaElement`)
87 * @property {number} minPxPerSec=20 Minimum number of pixels per second of
88 * audio.
89 * @property {boolean} normalize=false If true, normalize by the maximum peak
90 * instead of 1.0.
91 * @property {boolean} partialRender=false Use the PeakCache to improve
92 * rendering speed of large waveforms
93 * @property {number} pixelRatio=window.devicePixelRatio The pixel ratio used to
94 * calculate display
95 * @property {PluginDefinition[]} plugins=[] An array of plugin definitions to
96 * register during instantiation, they will be directly initialised unless they
97 * are added with the `deferInit` property set to true.
98 * @property {string} progressColor='#555' The fill color of the part of the
99 * waveform behind the cursor. When `progressColor` and `waveColor` are the same
100 * the progress wave is not rendered at all.
101 * @property {boolean} removeMediaElementOnDestroy=true Set to false to keep the
102 * media element in the DOM when the player is destroyed. This is useful when
103 * reusing an existing media element via the `loadMediaElement` method.
104 * @property {Object} renderer=MultiCanvas Can be used to inject a custom
105 * renderer.
106 * @property {boolean|number} responsive=false If set to `true` resize the
107 * waveform, when the window is resized. This is debounced with a `100ms`
108 * timeout by default. If this parameter is a number it represents that timeout.
109 * @property {boolean} rtl=false If set to `true`, renders waveform from
110 * right-to-left.
111 * @property {boolean} scrollParent=false Whether to scroll the container with a
112 * lengthy waveform. Otherwise the waveform is shrunk to the container width
113 * (see fillParent).
114 * @property {number} skipLength=2 Number of seconds to skip with the
115 * skipForward() and skipBackward() methods.
116 * @property {boolean} splitChannels=false Render with separate waveforms for
117 * the channels of the audio
118 * @property {string} waveColor='#999' The fill color of the waveform after the
119 * cursor.
120 * @property {object} xhr={} XHR options. For example:
121 * `let xhr = {
122 * cache: 'default',
123 * mode: 'cors',
124 * method: 'GET',
125 * credentials: 'same-origin',
126 * redirect: 'follow',
127 * referrer: 'client',
128 * headers: [
129 * {
130 * key: 'Authorization',
131 * value: 'my-token'
132 * }
133 * ]
134 * };`
135 */
136
137/**
138 * @typedef {Object} PluginDefinition
139 * @desc The Object used to describe a plugin
140 * @example wavesurfer.addPlugin(pluginDefinition);
141 * @property {string} name The name of the plugin, the plugin instance will be
142 * added as a property to the wavesurfer instance under this name
143 * @property {?Object} staticProps The properties that should be added to the
144 * wavesurfer instance as static properties
145 * @property {?boolean} deferInit Don't initialise plugin
146 * automatically
147 * @property {Object} params={} The plugin parameters, they are the first parameter
148 * passed to the plugin class constructor function
149 * @property {PluginClass} instance The plugin instance factory, is called with
150 * the dependency specified in extends. Returns the plugin class.
151 */
152
153/**
154 * @interface PluginClass
155 *
156 * @desc This is the interface which is implemented by all plugin classes. Note
157 * that this only turns into an observer after being passed through
158 * `wavesurfer.addPlugin`.
159 *
160 * @extends {Observer}
161 */
162class PluginClass {
163 /**
164 * Plugin definition factory
165 *
166 * This function must be used to create a plugin definition which can be
167 * used by wavesurfer to correctly instantiate the plugin.
168 *
169 * It returns a `PluginDefinition` object representing the plugin.
170 *
171 * @param {Object} params={} The plugin params (specific to the plugin)
172 */
173 create(params) {}
174 /**
175 * Construct the plugin
176 *
177 * @param {Object} params={} The plugin params (specific to the plugin)
178 * @param {Object} ws The wavesurfer instance
179 */
180 constructor(params, ws) {}
181 /**
182 * Initialise the plugin
183 *
184 * Start doing something. This is called by
185 * `wavesurfer.initPlugin(pluginName)`
186 */
187 init() {}
188 /**
189 * Destroy the plugin instance
190 *
191 * Stop doing something. This is called by
192 * `wavesurfer.destroyPlugin(pluginName)`
193 */
194 destroy() {}
195}
196
197/**
198 * WaveSurfer core library class
199 *
200 * @extends {Observer}
201 * @example
202 * const params = {
203 * container: '#waveform',
204 * waveColor: 'violet',
205 * progressColor: 'purple'
206 * };
207 *
208 * // initialise like this
209 * const wavesurfer = WaveSurfer.create(params);
210 *
211 * // or like this ...
212 * const wavesurfer = new WaveSurfer(params);
213 * wavesurfer.init();
214 *
215 * // load audio file
216 * wavesurfer.load('example/media/demo.wav');
217 */
218export default class WaveSurfer extends util.Observer {
219 /** @private */
220 defaultParams = {
221 audioContext: null,
222 audioScriptProcessor: null,
223 audioRate: 1,
224 autoCenter: true,
225 autoCenterRate: 5,
226 autoCenterImmediately: false,
227 backend: 'WebAudio',
228 backgroundColor: null,
229 barHeight: 1,
230 barRadius: 0,
231 barGap: null,
232 barMinHeight: null,
233 container: null,
234 cursorColor: '#333',
235 cursorWidth: 1,
236 dragSelection: true,
237 drawingContextAttributes: {
238 // Boolean that hints the user agent to reduce the latency
239 // by desynchronizing the canvas paint cycle from the event
240 // loop
241 desynchronized: false
242 },
243 duration: null,
244 fillParent: true,
245 forceDecode: false,
246 height: 128,
247 hideScrollbar: false,
248 interact: true,
249 loopSelection: true,
250 maxCanvasWidth: 4000,
251 mediaContainer: null,
252 mediaControls: false,
253 mediaType: 'audio',
254 minPxPerSec: 20,
255 normalize: false,
256 partialRender: false,
257 pixelRatio:
258 window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
259 plugins: [],
260 progressColor: '#555',
261 removeMediaElementOnDestroy: true,
262 renderer: SpectrumMultiCanvas, // **** GS3 mod
263 responsive: false,
264 rtl: false,
265 scrollParent: false,
266 skipLength: 2,
267 splitChannels: false,
268 splitChannelsOptions: {
269 overlay: false,
270 channelColors: {},
271 filterChannels: [],
272 },
273 waveColor: '#999',
274 xhr: {}
275 };
276
277 /** @private */
278 backends = {
279 MediaElement,
280 WebAudio,
281 MediaElementWebAudio
282 };
283
284 /**
285 * Instantiate this class, call its `init` function and returns it
286 *
287 * @param {WavesurferParams} params The wavesurfer parameters
288 * @return {Object} WaveSurfer instance
289 * @example const wavesurfer = WaveSurfer.create(params);
290 */
291 static create(params) {
292 const wavesurfer = new WaveSurfer(params);
293 return wavesurfer.init();
294 }
295
296 /**
297 * The library version number is available as a static property of the
298 * WaveSurfer class
299 *
300 * @type {String}
301 * @example
302 * console.log('Using wavesurfer.js ' + WaveSurfer.VERSION);
303 */
304 static VERSION = __VERSION__;
305
306 /**
307 * Functions in the `util` property are available as a prototype property to
308 * all instances
309 *
310 * @type {Object}
311 * @example
312 * const wavesurfer = WaveSurfer.create(params);
313 * wavesurfer.util.style(myElement, { background: 'blue' });
314 */
315 util = util;
316
317 /**
318 * Functions in the `util` property are available as a static property of the
319 * WaveSurfer class
320 *
321 * @type {Object}
322 * @example
323 * WaveSurfer.util.style(myElement, { background: 'blue' });
324 */
325 static util = util;
326
327 /**
328 * Initialise wavesurfer instance
329 *
330 * @param {WavesurferParams} params Instantiation options for wavesurfer
331 * @example
332 * const wavesurfer = new WaveSurfer(params);
333 * @returns {this} Wavesurfer instance
334 */
335 constructor(params) {
336 super();
337 /**
338 * Extract relevant parameters (or defaults)
339 * @private
340 */
341 this.params = Object.assign({}, this.defaultParams, params);
342
343 /** @private */
344 this.container =
345 'string' == typeof params.container
346 ? document.querySelector(this.params.container)
347 : this.params.container;
348
349 if (!this.container) {
350 throw new Error('Container element not found');
351 }
352
353 if (this.params.mediaContainer == null) {
354 /** @private */
355 this.mediaContainer = this.container;
356 } else if (typeof this.params.mediaContainer == 'string') {
357 /** @private */
358 this.mediaContainer = document.querySelector(
359 this.params.mediaContainer
360 );
361 } else {
362 /** @private */
363 this.mediaContainer = this.params.mediaContainer;
364 }
365
366 if (!this.mediaContainer) {
367 throw new Error('Media Container element not found');
368 }
369
370 if (this.params.maxCanvasWidth <= 1) {
371 throw new Error('maxCanvasWidth must be greater than 1');
372 } else if (this.params.maxCanvasWidth % 2 == 1) {
373 throw new Error('maxCanvasWidth must be an even number');
374 }
375
376 if (this.params.rtl === true) {
377 util.style(this.container, { transform: 'rotateY(180deg)' });
378 }
379
380 if (this.params.backgroundColor) {
381 this.setBackgroundColor(this.params.backgroundColor);
382 }
383
384 /**
385 * @private Used to save the current volume when muting so we can
386 * restore once unmuted
387 * @type {number}
388 */
389 this.savedVolume = 0;
390
391 /**
392 * @private The current muted state
393 * @type {boolean}
394 */
395 this.isMuted = false;
396
397 /**
398 * @private Will hold a list of event descriptors that need to be
399 * canceled on subsequent loads of audio
400 * @type {Object[]}
401 */
402 this.tmpEvents = [];
403
404 /**
405 * @private Holds any running audio downloads
406 * @type {Observer}
407 */
408 this.currentRequest = null;
409 /** @private */
410 this.arraybuffer = null;
411 /** @private */
412 this.drawer = null;
413 /** @private */
414 this.backend = null;
415 /** @private */
416 this.peakCache = null;
417
418 // cache constructor objects
419 if (typeof this.params.renderer !== 'function') {
420 throw new Error('Renderer parameter is invalid');
421 }
422 /**
423 * @private The uninitialised Drawer class
424 */
425 this.Drawer = this.params.renderer;
426
427 /**
428 * @private The uninitialised Backend class
429 */
430 // Back compat
431 if (this.params.backend == 'AudioElement') {
432 this.params.backend = 'MediaElement';
433 }
434
435 if (
436 (this.params.backend == 'WebAudio' ||
437 this.params.backend === 'MediaElementWebAudio') &&
438 !WebAudio.prototype.supportsWebAudio.call(null)
439 ) {
440 this.params.backend = 'MediaElement';
441 }
442 this.Backend = this.backends[this.params.backend];
443
444 /**
445 * @private map of plugin names that are currently initialised
446 */
447 this.initialisedPluginList = {};
448 /** @private */
449 this.isDestroyed = false;
450
451 /**
452 * Get the current ready status.
453 *
454 * @example const isReady = wavesurfer.isReady;
455 * @return {boolean}
456 */
457 this.isReady = false;
458
459 // responsive debounced event listener. If this.params.responsive is not
460 // set, this is never called. Use 100ms or this.params.responsive as
461 // timeout for the debounce function.
462 let prevWidth = 0;
463 this._onResize = util.debounce(
464 () => {
465 if (
466 prevWidth != this.drawer.wrapper.clientWidth &&
467 !this.params.scrollParent
468 ) {
469 prevWidth = this.drawer.wrapper.clientWidth;
470 this.drawer.fireEvent('redraw');
471 }
472 },
473 typeof this.params.responsive === 'number'
474 ? this.params.responsive
475 : 100
476 );
477
478 return this;
479 }
480
481 /**
482 * Initialise the wave
483 *
484 * @example
485 * var wavesurfer = new WaveSurfer(params);
486 * wavesurfer.init();
487 * @return {this} The wavesurfer instance
488 */
489 init() {
490 this.registerPlugins(this.params.plugins);
491 this.createDrawer();
492 this.createBackend();
493 this.createPeakCache();
494 return this;
495 }
496
497 /**
498 * Add and initialise array of plugins (if `plugin.deferInit` is falsey),
499 * this function is called in the init function of wavesurfer
500 *
501 * @param {PluginDefinition[]} plugins An array of plugin definitions
502 * @emits {WaveSurfer#plugins-registered} Called with the array of plugin definitions
503 * @return {this} The wavesurfer instance
504 */
505 registerPlugins(plugins) {
506 // first instantiate all the plugins
507 plugins.forEach(plugin => this.addPlugin(plugin));
508
509 // now run the init functions
510 plugins.forEach(plugin => {
511 // call init function of the plugin if deferInit is falsey
512 // in that case you would manually use initPlugins()
513 if (!plugin.deferInit) {
514 this.initPlugin(plugin.name);
515 }
516 });
517 this.fireEvent('plugins-registered', plugins);
518 return this;
519 }
520
521 /**
522 * Get a map of plugin names that are currently initialised
523 *
524 * @example wavesurfer.getPlugins();
525 * @return {Object} Object with plugin names
526 */
527 getActivePlugins() {
528 return this.initialisedPluginList;
529 }
530
531 /**
532 * Add a plugin object to wavesurfer
533 *
534 * @param {PluginDefinition} plugin A plugin definition
535 * @emits {WaveSurfer#plugin-added} Called with the name of the plugin that was added
536 * @example wavesurfer.addPlugin(WaveSurfer.minimap());
537 * @return {this} The wavesurfer instance
538 */
539 addPlugin(plugin) {
540 if (!plugin.name) {
541 throw new Error('Plugin does not have a name!');
542 }
543 if (!plugin.instance) {
544 throw new Error(
545 `Plugin ${plugin.name} does not have an instance property!`
546 );
547 }
548
549 // staticProps properties are applied to wavesurfer instance
550 if (plugin.staticProps) {
551 Object.keys(plugin.staticProps).forEach(pluginStaticProp => {
552 /**
553 * Properties defined in a plugin definition's `staticProps` property are added as
554 * staticProps properties of the WaveSurfer instance
555 */
556 this[pluginStaticProp] = plugin.staticProps[pluginStaticProp];
557 });
558 }
559
560 const Instance = plugin.instance;
561
562 // turn the plugin instance into an observer
563 const observerPrototypeKeys = Object.getOwnPropertyNames(
564 util.Observer.prototype
565 );
566 observerPrototypeKeys.forEach(key => {
567 Instance.prototype[key] = util.Observer.prototype[key];
568 });
569
570 /**
571 * Instantiated plugin classes are added as a property of the wavesurfer
572 * instance
573 * @type {Object}
574 */
575 this[plugin.name] = new Instance(plugin.params || {}, this);
576 this.fireEvent('plugin-added', plugin.name);
577 return this;
578 }
579
580 /**
581 * Initialise a plugin
582 *
583 * @param {string} name A plugin name
584 * @emits WaveSurfer#plugin-initialised
585 * @example wavesurfer.initPlugin('minimap');
586 * @return {this} The wavesurfer instance
587 */
588 initPlugin(name) {
589 if (!this[name]) {
590 throw new Error(`Plugin ${name} has not been added yet!`);
591 }
592 if (this.initialisedPluginList[name]) {
593 // destroy any already initialised plugins
594 this.destroyPlugin(name);
595 }
596 this[name].init();
597 this.initialisedPluginList[name] = true;
598 this.fireEvent('plugin-initialised', name);
599 return this;
600 }
601
602 /**
603 * Destroy a plugin
604 *
605 * @param {string} name A plugin name
606 * @emits WaveSurfer#plugin-destroyed
607 * @example wavesurfer.destroyPlugin('minimap');
608 * @returns {this} The wavesurfer instance
609 */
610 destroyPlugin(name) {
611 if (!this[name]) {
612 throw new Error(
613 `Plugin ${name} has not been added yet and cannot be destroyed!`
614 );
615 }
616 if (!this.initialisedPluginList[name]) {
617 throw new Error(
618 `Plugin ${name} is not active and cannot be destroyed!`
619 );
620 }
621 if (typeof this[name].destroy !== 'function') {
622 throw new Error(`Plugin ${name} does not have a destroy function!`);
623 }
624
625 this[name].destroy();
626 delete this.initialisedPluginList[name];
627 this.fireEvent('plugin-destroyed', name);
628 return this;
629 }
630
631 /**
632 * Destroy all initialised plugins. Convenience function to use when
633 * wavesurfer is removed
634 *
635 * @private
636 */
637 destroyAllPlugins() {
638 Object.keys(this.initialisedPluginList).forEach(name =>
639 this.destroyPlugin(name)
640 );
641 }
642
643 /**
644 * Create the drawer and draw the waveform
645 *
646 * @private
647 * @emits WaveSurfer#drawer-created
648 */
649 createDrawer() {
650 this.drawer = new this.Drawer(this.container, this.params);
651 this.drawer.wavesurfer = this; // **** // GS3 edit
652
653 this.drawer.init();
654 this.fireEvent('drawer-created', this.drawer);
655
656 if (this.params.responsive !== false) {
657 window.addEventListener('resize', this._onResize, true);
658 window.addEventListener('orientationchange', this._onResize, true);
659 }
660
661 this.drawer.on('redraw', () => {
662 this.drawBuffer();
663 this.drawer.progress(this.backend.getPlayedPercents());
664 });
665
666 // Click-to-seek
667 this.drawer.on('click', (e, progress) => {
668 setTimeout(() => this.seekTo(progress), 0);
669 });
670
671 // Relay the scroll event from the drawer
672 this.drawer.on('scroll', e => {
673 if (this.params.partialRender) {
674 this.drawBuffer();
675 }
676 this.fireEvent('scroll', e);
677 });
678 }
679
680 /**
681 * Create the backend
682 *
683 * @private
684 * @emits WaveSurfer#backend-created
685 */
686 createBackend() {
687 if (this.backend) {
688 this.backend.destroy();
689 }
690
691 this.backend = new this.Backend(this.params);
692 this.backend.init();
693 this.fireEvent('backend-created', this.backend);
694
695 this.backend.on('finish', () => {
696 this.drawer.progress(this.backend.getPlayedPercents());
697 this.fireEvent('finish');
698 });
699 this.backend.on('play', () => this.fireEvent('play'));
700 this.backend.on('pause', () => this.fireEvent('pause'));
701
702 this.backend.on('audioprocess', time => {
703 this.drawer.progress(this.backend.getPlayedPercents());
704 this.fireEvent('audioprocess', time);
705 });
706
707 // only needed for MediaElement and MediaElementWebAudio backend
708 if (
709 this.params.backend === 'MediaElement' ||
710 this.params.backend === 'MediaElementWebAudio'
711 ) {
712 this.backend.on('seek', () => {
713 this.drawer.progress(this.backend.getPlayedPercents());
714 });
715
716 this.backend.on('volume', () => {
717 let newVolume = this.getVolume();
718 this.fireEvent('volume', newVolume);
719
720 if (this.backend.isMuted !== this.isMuted) {
721 this.isMuted = this.backend.isMuted;
722 this.fireEvent('mute', this.isMuted);
723 }
724 });
725 }
726 }
727
728 /**
729 * Create the peak cache
730 *
731 * @private
732 */
733 createPeakCache() {
734 if (this.params.partialRender) {
735 this.peakCache = new PeakCache();
736 }
737 }
738
739 /**
740 * Get the duration of the audio clip
741 *
742 * @example const duration = wavesurfer.getDuration();
743 * @return {number} Duration in seconds
744 */
745 getDuration() {
746 return this.backend.getDuration();
747 }
748
749 /**
750 * Get the current playback position
751 *
752 * @example const currentTime = wavesurfer.getCurrentTime();
753 * @return {number} Playback position in seconds
754 */
755 getCurrentTime() {
756 return this.backend.getCurrentTime();
757 }
758
759 /**
760 * Set the current play time in seconds.
761 *
762 * @param {number} seconds A positive number in seconds. E.g. 10 means 10
763 * seconds, 60 means 1 minute
764 */
765 setCurrentTime(seconds) {
766 if (seconds >= this.getDuration()) {
767 this.seekTo(1);
768 } else {
769 this.seekTo(seconds / this.getDuration());
770 }
771 }
772
773 /**
774 * Starts playback from the current position. Optional start and end
775 * measured in seconds can be used to set the range of audio to play.
776 *
777 * @param {?number} start Position to start at
778 * @param {?number} end Position to end at
779 * @emits WaveSurfer#interaction
780 * @return {Promise} Result of the backend play method
781 * @example
782 * // play from second 1 to 5
783 * wavesurfer.play(1, 5);
784 */
785 play(start, end) {
786 this.fireEvent('interaction', () => this.play(start, end));
787 return this.backend.play(start, end);
788 }
789
790 /**
791 * Set a point in seconds for playback to stop at.
792 *
793 * @param {number} position Position (in seconds) to stop at
794 * @version 3.3.0
795 */
796 setPlayEnd(position) {
797 this.backend.setPlayEnd(position);
798 }
799
800 /**
801 * Stops and pauses playback
802 *
803 * @example wavesurfer.pause();
804 * @return {Promise} Result of the backend pause method
805 */
806 pause() {
807 if (!this.backend.isPaused()) {
808 return this.backend.pause();
809 }
810 }
811
812 /**
813 * Toggle playback
814 *
815 * @example wavesurfer.playPause();
816 * @return {Promise} Result of the backend play or pause method
817 */
818 playPause() {
819 return this.backend.isPaused() ? this.play() : this.pause();
820 }
821
822 /**
823 * Get the current playback state
824 *
825 * @example const isPlaying = wavesurfer.isPlaying();
826 * @return {boolean} False if paused, true if playing
827 */
828 isPlaying() {
829 return !this.backend.isPaused();
830 }
831
832 /**
833 * Skip backward
834 *
835 * @param {?number} seconds Amount to skip back, if not specified `skipLength`
836 * is used
837 * @example wavesurfer.skipBackward();
838 */
839 skipBackward(seconds) {
840 this.skip(-seconds || -this.params.skipLength);
841 }
842
843 /**
844 * Skip forward
845 *
846 * @param {?number} seconds Amount to skip back, if not specified `skipLength`
847 * is used
848 * @example wavesurfer.skipForward();
849 */
850 skipForward(seconds) {
851 this.skip(seconds || this.params.skipLength);
852 }
853
854 /**
855 * Skip a number of seconds from the current position (use a negative value
856 * to go backwards).
857 *
858 * @param {number} offset Amount to skip back or forwards
859 * @example
860 * // go back 2 seconds
861 * wavesurfer.skip(-2);
862 */
863 skip(offset) {
864 const duration = this.getDuration() || 1;
865 let position = this.getCurrentTime() || 0;
866 position = Math.max(0, Math.min(duration, position + (offset || 0)));
867 this.seekAndCenter(position / duration);
868 }
869
870 /**
871 * Seeks to a position and centers the view
872 *
873 * @param {number} progress Between 0 (=beginning) and 1 (=end)
874 * @example
875 * // seek and go to the middle of the audio
876 * wavesurfer.seekTo(0.5);
877 */
878 seekAndCenter(progress) {
879 this.seekTo(progress);
880 this.drawer.recenter(progress);
881 }
882
883 /**
884 * Seeks to a position
885 *
886 * @param {number} progress Between 0 (=beginning) and 1 (=end)
887 * @emits WaveSurfer#interaction
888 * @emits WaveSurfer#seek
889 * @example
890 * // seek to the middle of the audio
891 * wavesurfer.seekTo(0.5);
892 */
893 seekTo(progress) {
894 // return an error if progress is not a number between 0 and 1
895 if (
896 typeof progress !== 'number' ||
897 !isFinite(progress) ||
898 progress < 0 ||
899 progress > 1
900 ) {
901 throw new Error(
902 'Error calling wavesurfer.seekTo, parameter must be a number between 0 and 1!'
903 );
904 }
905 this.fireEvent('interaction', () => this.seekTo(progress));
906
907 const paused = this.backend.isPaused();
908 // avoid draw wrong position while playing backward seeking
909 if (!paused) {
910 this.backend.pause();
911 }
912 // avoid small scrolls while paused seeking
913 const oldScrollParent = this.params.scrollParent;
914 this.params.scrollParent = false;
915 this.backend.seekTo(progress * this.getDuration());
916 this.drawer.progress(progress);
917
918 if (!paused) {
919 this.backend.play();
920 }
921 this.params.scrollParent = oldScrollParent;
922 this.fireEvent('seek', progress);
923 }
924
925 /**
926 * Stops and goes to the beginning.
927 *
928 * @example wavesurfer.stop();
929 */
930 stop() {
931 this.pause();
932 this.seekTo(0);
933 this.drawer.progress(0);
934 }
935
936 /**
937 * Sets the ID of the audio device to use for output and returns a Promise.
938 *
939 * @param {string} deviceId String value representing underlying output
940 * device
941 * @returns {Promise} `Promise` that resolves to `undefined` when there are
942 * no errors detected.
943 */
944 setSinkId(deviceId) {
945 return this.backend.setSinkId(deviceId);
946 }
947
948 /**
949 * Set the playback volume.
950 *
951 * @param {number} newVolume A value between 0 and 1, 0 being no
952 * volume and 1 being full volume.
953 * @emits WaveSurfer#volume
954 */
955 setVolume(newVolume) {
956 this.backend.setVolume(newVolume);
957 this.fireEvent('volume', newVolume);
958 }
959
960 /**
961 * Get the playback volume.
962 *
963 * @return {number} A value between 0 and 1, 0 being no
964 * volume and 1 being full volume.
965 */
966 getVolume() {
967 return this.backend.getVolume();
968 }
969
970 /**
971 * Set the playback rate.
972 *
973 * @param {number} rate A positive number. E.g. 0.5 means half the normal
974 * speed, 2 means double speed and so on.
975 * @example wavesurfer.setPlaybackRate(2);
976 */
977 setPlaybackRate(rate) {
978 this.backend.setPlaybackRate(rate);
979 }
980
981 /**
982 * Get the playback rate.
983 *
984 * @return {number} The current playback rate.
985 */
986 getPlaybackRate() {
987 return this.backend.getPlaybackRate();
988 }
989
990 /**
991 * Toggle the volume on and off. If not currently muted it will save the
992 * current volume value and turn the volume off. If currently muted then it
993 * will restore the volume to the saved value, and then rest the saved
994 * value.
995 *
996 * @example wavesurfer.toggleMute();
997 */
998 toggleMute() {
999 this.setMute(!this.isMuted);
1000 }
1001
1002 /**
1003 * Enable or disable muted audio
1004 *
1005 * @param {boolean} mute Specify `true` to mute audio.
1006 * @emits WaveSurfer#volume
1007 * @emits WaveSurfer#mute
1008 * @example
1009 * // unmute
1010 * wavesurfer.setMute(false);
1011 * console.log(wavesurfer.getMute()) // logs false
1012 */
1013 setMute(mute) {
1014 // ignore all muting requests if the audio is already in that state
1015 if (mute === this.isMuted) {
1016 this.fireEvent('mute', this.isMuted);
1017 return;
1018 }
1019
1020 if (this.backend.setMute) {
1021 // Backends such as the MediaElement backend have their own handling
1022 // of mute, let them handle it.
1023 this.backend.setMute(mute);
1024 this.isMuted = mute;
1025 } else {
1026 if (mute) {
1027 // If currently not muted then save current volume,
1028 // turn off the volume and update the mute properties
1029 this.savedVolume = this.backend.getVolume();
1030 this.backend.setVolume(0);
1031 this.isMuted = true;
1032 this.fireEvent('volume', 0);
1033 } else {
1034 // If currently muted then restore to the saved volume
1035 // and update the mute properties
1036 this.backend.setVolume(this.savedVolume);
1037 this.isMuted = false;
1038 this.fireEvent('volume', this.savedVolume);
1039 }
1040 }
1041 this.fireEvent('mute', this.isMuted);
1042 }
1043
1044 /**
1045 * Get the current mute status.
1046 *
1047 * @example const isMuted = wavesurfer.getMute();
1048 * @return {boolean} Current mute status
1049 */
1050 getMute() {
1051 return this.isMuted;
1052 }
1053
1054 /**
1055 * Get the list of current set filters as an array.
1056 *
1057 * Filters must be set with setFilters method first
1058 *
1059 * @return {array} List of enabled filters
1060 */
1061 getFilters() {
1062 return this.backend.filters || [];
1063 }
1064
1065 /**
1066 * Toggles `scrollParent` and redraws
1067 *
1068 * @example wavesurfer.toggleScroll();
1069 */
1070 toggleScroll() {
1071 this.params.scrollParent = !this.params.scrollParent;
1072 this.drawBuffer();
1073 }
1074
1075 /**
1076 * Toggle mouse interaction
1077 *
1078 * @example wavesurfer.toggleInteraction();
1079 */
1080 toggleInteraction() {
1081 this.params.interact = !this.params.interact;
1082 }
1083
1084 /**
1085 * Get the fill color of the waveform after the cursor.
1086 *
1087 * @return {string} A CSS color string.
1088 */
1089 getWaveColor() {
1090 return this.params.waveColor;
1091 }
1092
1093 /**
1094 * Set the fill color of the waveform after the cursor.
1095 *
1096 * @param {string} color A CSS color string.
1097 * @example wavesurfer.setWaveColor('#ddd');
1098 */
1099 setWaveColor(color) {
1100 this.params.waveColor = color;
1101 this.drawBuffer();
1102 }
1103
1104 /**
1105 * Get the fill color of the waveform behind the cursor.
1106 *
1107 * @return {string} A CSS color string.
1108 */
1109 getProgressColor() {
1110 return this.params.progressColor;
1111 }
1112
1113 /**
1114 * Set the fill color of the waveform behind the cursor.
1115 *
1116 * @param {string} color A CSS color string.
1117 * @example wavesurfer.setProgressColor('#400');
1118 */
1119 setProgressColor(color) {
1120 this.params.progressColor = color;
1121 this.drawBuffer();
1122 }
1123
1124 /**
1125 * Get the background color of the waveform container.
1126 *
1127 * @return {string} A CSS color string.
1128 */
1129 getBackgroundColor() {
1130 return this.params.backgroundColor;
1131 }
1132
1133 /**
1134 * Set the background color of the waveform container.
1135 *
1136 * @param {string} color A CSS color string.
1137 * @example wavesurfer.setBackgroundColor('#FF00FF');
1138 */
1139 setBackgroundColor(color) {
1140 this.params.backgroundColor = color;
1141 util.style(this.container, { background: this.params.backgroundColor });
1142 }
1143
1144 /**
1145 * Get the fill color of the cursor indicating the playhead
1146 * position.
1147 *
1148 * @return {string} A CSS color string.
1149 */
1150 getCursorColor() {
1151 return this.params.cursorColor;
1152 }
1153
1154 /**
1155 * Set the fill color of the cursor indicating the playhead
1156 * position.
1157 *
1158 * @param {string} color A CSS color string.
1159 * @example wavesurfer.setCursorColor('#222');
1160 */
1161 setCursorColor(color) {
1162 this.params.cursorColor = color;
1163 this.drawer.updateCursor();
1164 }
1165
1166 /**
1167 * Get the height of the waveform.
1168 *
1169 * @return {number} Height measured in pixels.
1170 */
1171 getHeight() {
1172 return this.params.height;
1173 }
1174
1175 /**
1176 * Set the height of the waveform.
1177 *
1178 * @param {number} height Height measured in pixels.
1179 * @example wavesurfer.setHeight(200);
1180 */
1181 setHeight(height) {
1182 this.params.height = height;
1183 this.drawer.setHeight(height * this.params.pixelRatio);
1184 this.drawBuffer();
1185 }
1186
1187 /**
1188 * Hide channels from being drawn on the waveform if splitting channels.
1189 *
1190 * For example, if we want to draw only the peaks for the right stereo channel:
1191 *
1192 * const wavesurfer = new WaveSurfer.create({...splitChannels: true});
1193 * wavesurfer.load('stereo_audio.mp3');
1194 *
1195 * wavesurfer.setFilteredChannel([0]); <-- hide left channel peaks.
1196 *
1197 * @param {array} channelIndices Channels to be filtered out from drawing.
1198 * @version 4.0.0
1199 */
1200 setFilteredChannels(channelIndices) {
1201 this.params.splitChannelsOptions.filterChannels = channelIndices;
1202 this.drawBuffer();
1203 }
1204
1205 /**
1206 * Get the correct peaks for current wave view-port and render wave
1207 *
1208 * @private
1209 * @emits WaveSurfer#redraw
1210 */
1211 drawBuffer() {
1212 const nominalWidth = Math.round(
1213 this.getDuration() *
1214 this.params.minPxPerSec *
1215 this.params.pixelRatio
1216 );
1217 const parentWidth = this.drawer.getWidth();
1218 let width = nominalWidth;
1219 // always start at 0 after zooming for scrolling : issue redraw left part
1220 let start = 0;
1221 let end = Math.max(start + parentWidth, width);
1222 // Fill container
1223 if (
1224 this.params.fillParent &&
1225 (!this.params.scrollParent || nominalWidth < parentWidth)
1226 ) {
1227 width = parentWidth;
1228 start = 0;
1229 end = width;
1230 }
1231
1232 let peaks;
1233 if (this.params.partialRender) {
1234 const newRanges = this.peakCache.addRangeToPeakCache(
1235 width,
1236 start,
1237 end
1238 );
1239 let i;
1240 for (i = 0; i < newRanges.length; i++) {
1241 peaks = this.backend.getPeaks(
1242 width,
1243 newRanges[i][0],
1244 newRanges[i][1]
1245 );
1246 this.drawer.drawPeaks(
1247 peaks,
1248 width,
1249 newRanges[i][0],
1250 newRanges[i][1]
1251 );
1252 }
1253 } else {
1254 peaks = this.backend.getPeaks(width, start, end);
1255 this.drawer.drawPeaks(peaks, width, start, end);
1256 }
1257 this.fireEvent('redraw', peaks, width);
1258 }
1259
1260 /**
1261 * Horizontally zooms the waveform in and out. It also changes the parameter
1262 * `minPxPerSec` and enables the `scrollParent` option. Calling the function
1263 * with a falsey parameter will reset the zoom state.
1264 *
1265 * @param {?number} pxPerSec Number of horizontal pixels per second of
1266 * audio, if none is set the waveform returns to unzoomed state
1267 * @emits WaveSurfer#zoom
1268 * @example wavesurfer.zoom(20);
1269 */
1270 zoom(pxPerSec) {
1271 if (!pxPerSec) {
1272 this.params.minPxPerSec = this.defaultParams.minPxPerSec;
1273 this.params.scrollParent = false;
1274 } else {
1275 this.params.minPxPerSec = pxPerSec;
1276 this.params.scrollParent = true;
1277 }
1278
1279 this.drawBuffer();
1280 this.drawer.progress(this.backend.getPlayedPercents());
1281
1282 this.drawer.recenter(this.getCurrentTime() / this.getDuration());
1283 this.fireEvent('zoom', pxPerSec);
1284 }
1285
1286 /**
1287 * Decode buffer and load
1288 *
1289 * @private
1290 * @param {ArrayBuffer} arraybuffer Buffer to process
1291 */
1292 loadArrayBuffer(arraybuffer) {
1293 this.decodeArrayBuffer(arraybuffer, data => {
1294 if (!this.isDestroyed) {
1295 this.loadDecodedBuffer(data);
1296 }
1297 });
1298 }
1299
1300 /**
1301 * Directly load an externally decoded AudioBuffer
1302 *
1303 * @private
1304 * @param {AudioBuffer} buffer Buffer to process
1305 * @emits WaveSurfer#ready
1306 */
1307 loadDecodedBuffer(buffer) {
1308 this.backend.load(buffer);
1309 this.drawBuffer();
1310 this.isReady = true;
1311 this.fireEvent('ready');
1312 }
1313
1314 /**
1315 * Loads audio data from a Blob or File object
1316 *
1317 * @param {Blob|File} blob Audio data
1318 * @example
1319 */
1320 loadBlob(blob) {
1321 // Create file reader
1322 const reader = new FileReader();
1323 reader.addEventListener('progress', e => this.onProgress(e));
1324 reader.addEventListener('load', e =>
1325 this.loadArrayBuffer(e.target.result)
1326 );
1327 reader.addEventListener('error', () =>
1328 this.fireEvent('error', 'Error reading file')
1329 );
1330 reader.readAsArrayBuffer(blob);
1331 this.empty();
1332 }
1333
1334 /**
1335 * Loads audio and re-renders the waveform.
1336 *
1337 * @param {string|HTMLMediaElement} url The url of the audio file or the
1338 * audio element with the audio
1339 * @param {number[]|Number.<Array[]>} peaks Wavesurfer does not have to decode
1340 * the audio to render the waveform if this is specified
1341 * @param {?string} preload (Use with backend `MediaElement` and `MediaElementWebAudio`)
1342 * `'none'|'metadata'|'auto'` Preload attribute for the media element
1343 * @param {?number} duration The duration of the audio. This is used to
1344 * render the peaks data in the correct size for the audio duration (as
1345 * befits the current `minPxPerSec` and zoom value) without having to decode
1346 * the audio.
1347 * @returns {void}
1348 * @throws Will throw an error if the `url` argument is empty.
1349 * @example
1350 * // uses fetch or media element to load file (depending on backend)
1351 * wavesurfer.load('http://example.com/demo.wav');
1352 *
1353 * // setting preload attribute with media element backend and supplying
1354 * // peaks
1355 * wavesurfer.load(
1356 * 'http://example.com/demo.wav',
1357 * [0.0218, 0.0183, 0.0165, 0.0198, 0.2137, 0.2888],
1358 * true
1359 * );
1360 */
1361 load(url, peaks, preload, duration) {
1362 if (!url) {
1363 throw new Error('url parameter cannot be empty');
1364 }
1365 this.empty();
1366 if (preload) {
1367 // check whether the preload attribute will be usable and if not log
1368 // a warning listing the reasons why not and nullify the variable
1369 const preloadIgnoreReasons = {
1370 "Preload is not 'auto', 'none' or 'metadata'":
1371 ['auto', 'metadata', 'none'].indexOf(preload) === -1,
1372 'Peaks are not provided': !peaks,
1373 "Backend is not of type 'MediaElement' or 'MediaElementWebAudio'":
1374 ['MediaElement', 'MediaElementWebAudio'].indexOf(
1375 this.params.backend
1376 ) === -1,
1377 'Url is not of type string': typeof url !== 'string'
1378 };
1379 const activeReasons = Object.keys(preloadIgnoreReasons).filter(
1380 reason => preloadIgnoreReasons[reason]
1381 );
1382 if (activeReasons.length) {
1383 // eslint-disable-next-line no-console
1384 console.warn(
1385 'Preload parameter of wavesurfer.load will be ignored because:\n\t- ' +
1386 activeReasons.join('\n\t- ')
1387 );
1388 // stop invalid values from being used
1389 preload = null;
1390 }
1391 }
1392
1393 switch (this.params.backend) {
1394 case 'WebAudio':
1395 return this.loadBuffer(url, peaks, duration);
1396 case 'MediaElement':
1397 case 'MediaElementWebAudio':
1398 return this.loadMediaElement(url, peaks, preload, duration);
1399 }
1400 }
1401
1402 /**
1403 * Loads audio using Web Audio buffer backend.
1404 *
1405 * @private
1406 * @param {string} url URL of audio file
1407 * @param {number[]|Number.<Array[]>} peaks Peaks data
1408 * @param {?number} duration Optional duration of audio file
1409 * @returns {void}
1410 */
1411 loadBuffer(url, peaks, duration) {
1412 const load = action => {
1413 if (action) {
1414 this.tmpEvents.push(this.once('ready', action));
1415 }
1416 return this.getArrayBuffer(url, data => this.loadArrayBuffer(data));
1417 };
1418
1419 if (peaks) {
1420 this.backend.setPeaks(peaks, duration);
1421 this.drawBuffer();
1422 this.tmpEvents.push(this.once('interaction', load));
1423 } else {
1424 return load();
1425 }
1426 }
1427
1428 /**
1429 * Either create a media element, or load an existing media element.
1430 *
1431 * @private
1432 * @param {string|HTMLMediaElement} urlOrElt Either a path to a media file, or an
1433 * existing HTML5 Audio/Video Element
1434 * @param {number[]|Number.<Array[]>} peaks Array of peaks. Required to bypass web audio
1435 * dependency
1436 * @param {?boolean} preload Set to true if the preload attribute of the
1437 * audio element should be enabled
1438 * @param {?number} duration Optional duration of audio file
1439 */
1440 loadMediaElement(urlOrElt, peaks, preload, duration) {
1441 let url = urlOrElt;
1442
1443 if (typeof urlOrElt === 'string') {
1444 this.backend.load(url, this.mediaContainer, peaks, preload);
1445 } else {
1446 const elt = urlOrElt;
1447 this.backend.loadElt(elt, peaks);
1448
1449 // If peaks are not provided,
1450 // url = element.src so we can get peaks with web audio
1451 url = elt.src;
1452 }
1453
1454 this.tmpEvents.push(
1455 this.backend.once('canplay', () => {
1456 // ignore when backend was already destroyed
1457 if (!this.backend.destroyed) {
1458 this.drawBuffer();
1459 this.isReady = true;
1460 this.fireEvent('ready');
1461 }
1462 }),
1463 this.backend.once('error', err => this.fireEvent('error', err))
1464 );
1465
1466 // If no pre-decoded peaks provided or pre-decoded peaks are
1467 // provided with forceDecode flag, attempt to download the
1468 // audio file and decode it with Web Audio.
1469 if (peaks) {
1470 this.backend.setPeaks(peaks, duration);
1471 }
1472
1473 if (
1474 (!peaks || this.params.forceDecode) &&
1475 this.backend.supportsWebAudio()
1476 ) {
1477 this.getArrayBuffer(url, arraybuffer => {
1478 this.decodeArrayBuffer(arraybuffer, buffer => {
1479 this.backend.buffer = buffer;
1480 this.backend.setPeaks(null);
1481 this.drawBuffer();
1482 this.fireEvent('waveform-ready');
1483 });
1484 });
1485 }
1486 }
1487
1488 /**
1489 * Decode an array buffer and pass data to a callback
1490 *
1491 * @private
1492 * @param {Object} arraybuffer The array buffer to decode
1493 * @param {function} callback The function to call on complete
1494 */
1495 decodeArrayBuffer(arraybuffer, callback) {
1496 this.arraybuffer = arraybuffer;
1497 this.backend.decodeArrayBuffer(
1498 arraybuffer,
1499 data => {
1500 // Only use the decoded data if we haven't been destroyed or
1501 // another decode started in the meantime
1502 if (!this.isDestroyed && this.arraybuffer == arraybuffer) {
1503 callback(data);
1504 this.arraybuffer = null;
1505 }
1506 },
1507 () => this.fireEvent('error', 'Error decoding audiobuffer')
1508 );
1509 }
1510
1511 /**
1512 * Load an array buffer using fetch and pass the result to a callback
1513 *
1514 * @param {string} url The URL of the file object
1515 * @param {function} callback The function to call on complete
1516 * @returns {util.fetchFile} fetch call
1517 * @private
1518 */
1519 getArrayBuffer(url, callback) {
1520 let options = Object.assign(
1521 {
1522 url: url,
1523 responseType: 'arraybuffer'
1524 },
1525 this.params.xhr
1526 );
1527 const request = util.fetchFile(options);
1528
1529 this.currentRequest = request;
1530
1531 this.tmpEvents.push(
1532 request.on('progress', e => {
1533 this.onProgress(e);
1534 }),
1535 request.on('success', data => {
1536 callback(data);
1537 this.currentRequest = null;
1538 }),
1539 request.on('error', e => {
1540 this.fireEvent('error', e);
1541 this.currentRequest = null;
1542 })
1543 );
1544
1545 return request;
1546 }
1547
1548 /**
1549 * Called while the audio file is loading
1550 *
1551 * @private
1552 * @param {Event} e Progress event
1553 * @emits WaveSurfer#loading
1554 */
1555 onProgress(e) {
1556 let percentComplete;
1557 if (e.lengthComputable) {
1558 percentComplete = e.loaded / e.total;
1559 } else {
1560 // Approximate progress with an asymptotic
1561 // function, and assume downloads in the 1-3 MB range.
1562 percentComplete = e.loaded / (e.loaded + 1000000);
1563 }
1564 this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
1565 }
1566
1567 /**
1568 * Exports PCM data into a JSON array and opens in a new window.
1569 *
1570 * @param {number} length=1024 The scale in which to export the peaks
1571 * @param {number} accuracy=10000
1572 * @param {?boolean} noWindow Set to true to disable opening a new
1573 * window with the JSON
1574 * @param {number} start Start index
1575 * @param {number} end End index
1576 * @return {Promise} Promise that resolves with array of peaks
1577 */
1578 exportPCM(length, accuracy, noWindow, start, end) {
1579 length = length || 1024;
1580 start = start || 0;
1581 accuracy = accuracy || 10000;
1582 noWindow = noWindow || false;
1583 const peaks = this.backend.getPeaks(length, start, end);
1584 const arr = [].map.call(
1585 peaks,
1586 val => Math.round(val * accuracy) / accuracy
1587 );
1588 return new Promise((resolve, reject) => {
1589 const json = JSON.stringify(arr);
1590
1591 if (!noWindow) {
1592 window.open(
1593 'data:application/json;charset=utf-8,' +
1594 encodeURIComponent(json)
1595 );
1596 }
1597 resolve(json);
1598 });
1599 }
1600
1601 /**
1602 * Save waveform image as data URI.
1603 *
1604 * The default format is `'image/png'`. Other supported types are
1605 * `'image/jpeg'` and `'image/webp'`.
1606 *
1607 * @param {string} format='image/png' A string indicating the image format.
1608 * The default format type is `'image/png'`.
1609 * @param {number} quality=1 A number between 0 and 1 indicating the image
1610 * quality to use for image formats that use lossy compression such as
1611 * `'image/jpeg'`` and `'image/webp'`.
1612 * @param {string} type Image data type to return. Either 'dataURL' (default)
1613 * or 'blob'.
1614 * @return {string|string[]|Promise} When using `'dataURL'` type this returns
1615 * a single data URL or an array of data URLs, one for each canvas. When using
1616 * `'blob'` type this returns a `Promise` resolving with an array of `Blob`
1617 * instances, one for each canvas.
1618 */
1619 exportImage(format, quality, type) {
1620 if (!format) {
1621 format = 'image/png';
1622 }
1623 if (!quality) {
1624 quality = 1;
1625 }
1626 if (!type) {
1627 type = 'dataURL';
1628 }
1629
1630 return this.drawer.getImage(format, quality, type);
1631 }
1632
1633 /**
1634 * Cancel any fetch request currently in progress
1635 */
1636 cancelAjax() {
1637 if (this.currentRequest && this.currentRequest.controller) {
1638 this.currentRequest.controller.abort();
1639 this.currentRequest = null;
1640 }
1641 }
1642
1643 /**
1644 * @private
1645 */
1646 clearTmpEvents() {
1647 this.tmpEvents.forEach(e => e.un());
1648 }
1649
1650 /**
1651 * Display empty waveform.
1652 */
1653 empty() {
1654 if (!this.backend.isPaused()) {
1655 this.stop();
1656 this.backend.disconnectSource();
1657 }
1658 this.isReady = false;
1659 this.cancelAjax();
1660 this.clearTmpEvents();
1661
1662 // empty drawer
1663 this.drawer.progress(0);
1664 this.drawer.setWidth(0);
1665 this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
1666 }
1667
1668 /**
1669 * Remove events, elements and disconnect WebAudio nodes.
1670 *
1671 * @emits WaveSurfer#destroy
1672 */
1673 destroy() {
1674 this.destroyAllPlugins();
1675 this.fireEvent('destroy');
1676 this.cancelAjax();
1677 this.clearTmpEvents();
1678 this.unAll();
1679 if (this.params.responsive !== false) {
1680 window.removeEventListener('resize', this._onResize, true);
1681 window.removeEventListener(
1682 'orientationchange',
1683 this._onResize,
1684 true
1685 );
1686 }
1687 if (this.backend) {
1688 this.backend.destroy();
1689 }
1690 if (this.drawer) {
1691 this.drawer.destroy();
1692 }
1693 this.isDestroyed = true;
1694 this.isReady = false;
1695 this.arraybuffer = null;
1696 }
1697}
Note: See TracBrowser for help on using the repository browser.