wavesurfer.js 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. import * as util from './util';
  2. import MultiCanvas from './drawer.multicanvas';
  3. import WebAudio from './webaudio';
  4. import MediaElement from './mediaelement';
  5. import PeakCache from './peakcache';
  6. /*
  7. * This work is licensed under a BSD-3-Clause License.
  8. */
  9. /** @external {HTMLElement} https://developer.mozilla.org/en/docs/Web/API/HTMLElement */
  10. /** @external {OfflineAudioContext} https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext */
  11. /** @external {File} https://developer.mozilla.org/en-US/docs/Web/API/File */
  12. /** @external {Blob} https://developer.mozilla.org/en-US/docs/Web/API/Blob */
  13. /** @external {CanvasRenderingContext2D} https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D */
  14. /** @external {MediaStreamConstraints} https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints */
  15. /** @external {AudioNode} https://developer.mozilla.org/de/docs/Web/API/AudioNode */
  16. /**
  17. * @typedef {Object} WavesurferParams
  18. * @property {AudioContext} audioContext=null Use your own previously
  19. * initialized AudioContext or leave blank.
  20. * @property {number} audioRate=1 Speed at which to play audio. Lower number is
  21. * slower.
  22. * @property {boolean} autoCenter=true If a scrollbar is present, center the
  23. * waveform around the progress
  24. * @property {string} backend='WebAudio' `'WebAudio'|'MediaElement'` In most cases
  25. * you don't have to set this manually. MediaElement is a fallback for
  26. * unsupported browsers.
  27. * @property {number} barHeight=1 The height of the wave
  28. * @property {boolean} closeAudioContext=false Close and nullify all audio
  29. * contexts when the destroy method is called.
  30. * @property {!string|HTMLElement} container CSS selector or HTML element where
  31. * the waveform should be drawn. This is the only required parameter.
  32. * @property {string} cursorColor='#333' The fill color of the cursor indicating
  33. * the playhead position.
  34. * @property {number} cursorWidth=1 Measured in pixels.
  35. * @property {boolean} fillParent=true Whether to fill the entire container or
  36. * draw only according to `minPxPerSec`.
  37. * @property {boolean} forceDecode=false Force decoding of audio using web audio
  38. * when zooming to get a more detailed waveform.
  39. * @property {number} height=128 The height of the waveform. Measured in
  40. * pixels.
  41. * @property {boolean} hideScrollbar=false Whether to hide the horizontal
  42. * scrollbar when one would normally be shown.
  43. * @property {boolean} interact=true Whether the mouse interaction will be
  44. * enabled at initialization. You can switch this parameter at any time later
  45. * on.
  46. * @property {boolean} loopSelection=true (Use with regions plugin) Enable
  47. * looping of selected regions
  48. * @property {number} maxCanvasWidth=4000 Maximum width of a single canvas in
  49. * pixels, excluding a small overlap (2 * `pixelRatio`, rounded up to the next
  50. * even integer). If the waveform is longer than this value, additional canvases
  51. * will be used to render the waveform, which is useful for very large waveforms
  52. * that may be too wide for browsers to draw on a single canvas.
  53. * @property {boolean} mediaControls=false (Use with backend `MediaElement`)
  54. * this enables the native controls for the media element
  55. * @property {string} mediaType='audio' (Use with backend `MediaElement`)
  56. * `'audio'|'video'`
  57. * @property {number} minPxPerSec=20 Minimum number of pixels per second of
  58. * audio.
  59. * @property {boolean} normalize=false If true, normalize by the maximum peak
  60. * instead of 1.0.
  61. * @property {boolean} partialRender=false Use the PeakCache to improve
  62. * rendering speed of large waveforms
  63. * @property {number} pixelRatio=window.devicePixelRatio The pixel ratio used to
  64. * calculate display
  65. * @property {PluginDefinition[]} plugins=[] An array of plugin definitions to
  66. * register during instantiation, they will be directly initialised unless they
  67. * are added with the `deferInit` property set to true.
  68. * @property {string} progressColor='#555' The fill color of the part of the
  69. * waveform behind the cursor.
  70. * @property {Object} renderer=MultiCanvas Can be used to inject a custom
  71. * renderer.
  72. * @property {boolean|number} responsive=false If set to `true` resize the
  73. * waveform, when the window is resized. This is debounced with a `100ms`
  74. * timeout by default. If this parameter is a number it represents that timeout.
  75. * @property {boolean} scrollParent=false Whether to scroll the container with a
  76. * lengthy waveform. Otherwise the waveform is shrunk to the container width
  77. * (see fillParent).
  78. * @property {number} skipLength=2 Number of seconds to skip with the
  79. * skipForward() and skipBackward() methods.
  80. * @property {boolean} splitChannels=false Render with seperate waveforms for
  81. * the channels of the audio
  82. * @property {string} waveColor='#999' The fill color of the waveform after the
  83. * cursor.
  84. */
  85. /**
  86. * @typedef {Object} PluginDefinition
  87. * @desc The Object used to describe a plugin
  88. * @example wavesurfer.addPlugin(pluginDefinition);
  89. * @property {string} name The name of the plugin, the plugin instance will be
  90. * added as a property to the wavesurfer instance under this name
  91. * @property {?Object} staticProps The properties that should be added to the
  92. * wavesurfer instance as static properties
  93. * @property {?boolean} deferInit Don't initialise plugin
  94. * automatically
  95. * @property {Object} params={} The plugin parameters, they are the first parameter
  96. * passed to the plugin class constructor function
  97. * @property {PluginClass} instance The plugin instance factory, is called with
  98. * the dependency specified in extends. Returns the plugin class.
  99. */
  100. /**
  101. * @interface PluginClass
  102. *
  103. * @desc This is the interface which is implemented by all plugin classes. Note
  104. * that this only turns into an observer after being passed through
  105. * `wavesurfer.addPlugin`.
  106. *
  107. * @extends {Observer}
  108. */
  109. class PluginClass {
  110. /**
  111. * Plugin definition factory
  112. *
  113. * This function must be used to create a plugin definition which can be
  114. * used by wavesurfer to correctly instantiate the plugin.
  115. *
  116. * @param {Object} params={} The plugin params (specific to the plugin)
  117. * @return {PluginDefinition} an object representing the plugin
  118. */
  119. create(params) {}
  120. /**
  121. * Construct the plugin
  122. *
  123. * @param {Object} ws The wavesurfer instance
  124. * @param {Object} params={} The plugin params (specific to the plugin)
  125. */
  126. constructor(ws, params) {}
  127. /**
  128. * Initialise the plugin
  129. *
  130. * Start doing something. This is called by
  131. * `wavesurfer.initPlugin(pluginName)`
  132. */
  133. init() {}
  134. /**
  135. * Destroy the plugin instance
  136. *
  137. * Stop doing something. This is called by
  138. * `wavesurfer.destroyPlugin(pluginName)`
  139. */
  140. destroy() {}
  141. }
  142. /**
  143. * WaveSurfer core library class
  144. *
  145. * @extends {Observer}
  146. * @example
  147. * const params = {
  148. * container: '#waveform',
  149. * waveColor: 'violet',
  150. * progressColor: 'purple'
  151. * };
  152. *
  153. * // initialise like this
  154. * const wavesurfer = WaveSurfer.create(params);
  155. *
  156. * // or like this ...
  157. * const wavesurfer = new WaveSurfer(params);
  158. * wavesurfer.init();
  159. *
  160. * // load audio file
  161. * wavesurfer.load('example/media/demo.wav');
  162. */
  163. export default class WaveSurfer extends util.Observer {
  164. /** @private */
  165. defaultParams = {
  166. audioContext : null,
  167. audioRate : 1,
  168. autoCenter : true,
  169. backend : 'WebAudio',
  170. barHeight : 1,
  171. container : null,
  172. cursorColor : '#333',
  173. cursorWidth : 1,
  174. dragSelection : true,
  175. fillParent : true,
  176. forceDecode : false,
  177. height : 128,
  178. hideScrollbar : false,
  179. interact : true,
  180. loopSelection : true,
  181. maxCanvasWidth: 4000,
  182. mediaContainer: null,
  183. mediaControls : false,
  184. mediaType : 'audio',
  185. minPxPerSec : 20,
  186. normalize : false,
  187. partialRender : false,
  188. pixelRatio : window.devicePixelRatio || screen.deviceXDPI / screen.logicalXDPI,
  189. plugins : [],
  190. progressColor : '#555',
  191. renderer : MultiCanvas,
  192. responsive : false,
  193. scrollParent : false,
  194. skipLength : 2,
  195. splitChannels : false,
  196. waveColor : '#999'
  197. }
  198. /** @private */
  199. backends = {
  200. MediaElement,
  201. WebAudio
  202. }
  203. /**
  204. * Instantiate this class, call its `init` function and returns it
  205. *
  206. * @param {WavesurferParams} params
  207. * @return {Object} WaveSurfer instance
  208. * @example const wavesurfer = WaveSurfer.create(params);
  209. */
  210. static create(params) {
  211. const wavesurfer = new WaveSurfer(params);
  212. return wavesurfer.init();
  213. }
  214. /**
  215. * Functions in the `util` property are available as a prototype property to
  216. * all instances
  217. *
  218. * @type {Object}
  219. * @example
  220. * const wavesurfer = WaveSurfer.create(params);
  221. * wavesurfer.util.style(myElement, { background: 'blue' });
  222. */
  223. util = util
  224. /**
  225. * Functions in the `util` property are available as a static property of the
  226. * WaveSurfer class
  227. *
  228. * @type {Object}
  229. * @example
  230. * WaveSurfer.util.style(myElement, { background: 'blue' });
  231. */
  232. static util = util
  233. /**
  234. * Initialise wavesurfer instance
  235. *
  236. * @param {WavesurferParams} params Instantiation options for wavesurfer
  237. * @example
  238. * const wavesurfer = new WaveSurfer(params);
  239. * @returns {this}
  240. */
  241. constructor(params) {
  242. super();
  243. /**
  244. * Extract relevant parameters (or defaults)
  245. * @private
  246. */
  247. this.params = util.extend({}, this.defaultParams, params);
  248. /** @private */
  249. this.container = 'string' == typeof params.container ?
  250. document.querySelector(this.params.container) :
  251. this.params.container;
  252. if (!this.container) {
  253. throw new Error('Container element not found');
  254. }
  255. if (this.params.mediaContainer == null) {
  256. /** @private */
  257. this.mediaContainer = this.container;
  258. } else if (typeof this.params.mediaContainer == 'string') {
  259. /** @private */
  260. this.mediaContainer = document.querySelector(this.params.mediaContainer);
  261. } else {
  262. /** @private */
  263. this.mediaContainer = this.params.mediaContainer;
  264. }
  265. if (!this.mediaContainer) {
  266. throw new Error('Media Container element not found');
  267. }
  268. if (this.params.maxCanvasWidth <= 1) {
  269. throw new Error('maxCanvasWidth must be greater than 1');
  270. } else if (this.params.maxCanvasWidth % 2 == 1) {
  271. throw new Error('maxCanvasWidth must be an even number');
  272. }
  273. /**
  274. * @private Used to save the current volume when muting so we can
  275. * restore once unmuted
  276. * @type {number}
  277. */
  278. this.savedVolume = 0;
  279. /**
  280. * @private The current muted state
  281. * @type {boolean}
  282. */
  283. this.isMuted = false;
  284. /**
  285. * @private Will hold a list of event descriptors that need to be
  286. * cancelled on subsequent loads of audio
  287. * @type {Object[]}
  288. */
  289. this.tmpEvents = [];
  290. /**
  291. * @private Holds any running audio downloads
  292. * @type {Observer}
  293. */
  294. this.currentAjax = null;
  295. /** @private */
  296. this.arraybuffer = null;
  297. /** @private */
  298. this.drawer = null;
  299. /** @private */
  300. this.backend = null;
  301. /** @private */
  302. this.peakCache = null;
  303. // cache constructor objects
  304. if (typeof this.params.renderer !== 'function') {
  305. throw new Error('Renderer parameter is invalid');
  306. }
  307. /**
  308. * @private The uninitialised Drawer class
  309. */
  310. this.Drawer = this.params.renderer;
  311. /**
  312. * @private The uninitialised Backend class
  313. */
  314. this.Backend = this.backends[this.params.backend];
  315. /**
  316. * @private map of plugin names that are currently initialised
  317. */
  318. this.initialisedPluginList = {};
  319. /** @private */
  320. this.isDestroyed = false;
  321. /** @private */
  322. this.isReady = false;
  323. // responsive debounced event listener. If this.params.responsive is not
  324. // set, this is never called. Use 100ms or this.params.responsive as
  325. // timeout for the debounce function.
  326. let prevWidth = 0;
  327. this._onResize = util.debounce(() => {
  328. if (prevWidth != this.drawer.wrapper.clientWidth) {
  329. prevWidth = this.drawer.wrapper.clientWidth;
  330. this.drawBuffer();
  331. }
  332. }, typeof this.params.responsive === 'number' ? this.params.responsive : 100);
  333. return this;
  334. }
  335. /**
  336. * Initialise the wave
  337. *
  338. * @example
  339. * var wavesurfer = new WaveSurfer(params);
  340. * wavesurfer.init();
  341. * @return {this}
  342. */
  343. init() {
  344. this.registerPlugins(this.params.plugins);
  345. this.createDrawer();
  346. this.createBackend();
  347. this.createPeakCache();
  348. return this;
  349. }
  350. /**
  351. * Add and initialise array of plugins (if `plugin.deferInit` is falsey),
  352. * this function is called in the init function of wavesurfer
  353. *
  354. * @param {PluginDefinition[]} plugins An array of plugin definitions
  355. * @emits {WaveSurfer#plugins-registered} Called with the array of plugin definitions
  356. * @return {this}
  357. */
  358. registerPlugins(plugins) {
  359. // first instantiate all the plugins
  360. plugins.forEach(plugin => this.addPlugin(plugin));
  361. // now run the init functions
  362. plugins.forEach(plugin => {
  363. // call init function of the plugin if deferInit is falsey
  364. // in that case you would manually use initPlugins()
  365. if (!plugin.deferInit) {
  366. this.initPlugin(plugin.name);
  367. }
  368. });
  369. this.fireEvent('plugins-registered', plugins);
  370. return this;
  371. }
  372. /**
  373. * Add a plugin object to wavesurfer
  374. *
  375. * @param {PluginDefinition} plugin A plugin definition
  376. * @emits {WaveSurfer#plugin-added} Called with the name of the plugin that was added
  377. * @example wavesurfer.addPlugin(WaveSurfer.minimap());
  378. * @return {this}
  379. */
  380. addPlugin(plugin) {
  381. if (!plugin.name) {
  382. throw new Error('Plugin does not have a name!');
  383. }
  384. if (!plugin.instance) {
  385. throw new Error(`Plugin ${plugin.name} does not have an instance property!`);
  386. }
  387. // staticProps properties are applied to wavesurfer instance
  388. if (plugin.staticProps) {
  389. Object.keys(plugin.staticProps).forEach(pluginStaticProp => {
  390. /**
  391. * Properties defined in a plugin definition's `staticProps` property are added as
  392. * staticProps properties of the WaveSurfer instance
  393. */
  394. this[pluginStaticProp] = plugin.staticProps[pluginStaticProp];
  395. });
  396. }
  397. const Instance = plugin.instance;
  398. // turn the plugin instance into an observer
  399. const observerPrototypeKeys = Object.getOwnPropertyNames(util.Observer.prototype);
  400. observerPrototypeKeys.forEach(key => {
  401. Instance.prototype[key] = util.Observer.prototype[key];
  402. });
  403. /**
  404. * Instantiated plugin classes are added as a property of the wavesurfer
  405. * instance
  406. * @type {Object}
  407. */
  408. this[plugin.name] = new Instance(plugin.params || {}, this);
  409. this.fireEvent('plugin-added', plugin.name);
  410. return this;
  411. }
  412. /**
  413. * Initialise a plugin
  414. *
  415. * @param {string} name A plugin name
  416. * @emits WaveSurfer#plugin-initialised
  417. * @example wavesurfer.initPlugin('minimap');
  418. * @return {this}
  419. */
  420. initPlugin(name) {
  421. if (!this[name]) {
  422. throw new Error(`Plugin ${name} has not been added yet!`);
  423. }
  424. if (this.initialisedPluginList[name]) {
  425. // destroy any already initialised plugins
  426. this.destroyPlugin(name);
  427. }
  428. this[name].init();
  429. this.initialisedPluginList[name] = true;
  430. this.fireEvent('plugin-initialised', name);
  431. return this;
  432. }
  433. /**
  434. * Destroy a plugin
  435. *
  436. * @param {string} name A plugin name
  437. * @emits WaveSurfer#plugin-destroyed
  438. * @example wavesurfer.destroyPlugin('minimap');
  439. * @returns {this}
  440. */
  441. destroyPlugin(name) {
  442. if (!this[name]) {
  443. throw new Error(`Plugin ${name} has not been added yet and cannot be destroyed!`);
  444. }
  445. if (!this.initialisedPluginList[name]) {
  446. throw new Error(`Plugin ${name} is not active and cannot be destroyed!`);
  447. }
  448. if (typeof this[name].destroy !== 'function') {
  449. throw new Error(`Plugin ${name} does not have a destroy function!`);
  450. }
  451. this[name].destroy();
  452. delete this.initialisedPluginList[name];
  453. this.fireEvent('plugin-destroyed', name);
  454. return this;
  455. }
  456. /**
  457. * Destroy all initialised plugins. Convenience function to use when
  458. * wavesurfer is removed
  459. *
  460. * @private
  461. */
  462. destroyAllPlugins() {
  463. Object.keys(this.initialisedPluginList).forEach(name => this.destroyPlugin(name));
  464. }
  465. /**
  466. * Create the drawer and draw the waveform
  467. *
  468. * @private
  469. * @emits WaveSurfer#drawer-created
  470. */
  471. createDrawer() {
  472. this.drawer = new this.Drawer(this.container, this.params);
  473. this.drawer.init();
  474. this.fireEvent('drawer-created', this.drawer);
  475. if (this.params.responsive) {
  476. window.addEventListener('resize', this._onResize, true);
  477. }
  478. this.drawer.on('redraw', () => {
  479. this.drawBuffer();
  480. this.drawer.progress(this.backend.getPlayedPercents());
  481. });
  482. // Click-to-seek
  483. this.drawer.on('click', (e, progress) => {
  484. setTimeout(() => this.seekTo(progress), 0);
  485. });
  486. // Relay the scroll event from the drawer
  487. this.drawer.on('scroll', e => {
  488. if (this.params.partialRender) {
  489. this.drawBuffer();
  490. }
  491. this.fireEvent('scroll', e);
  492. });
  493. }
  494. /**
  495. * Create the backend
  496. *
  497. * @private
  498. * @emits WaveSurfer#backend-created
  499. */
  500. createBackend() {
  501. if (this.backend) {
  502. this.backend.destroy();
  503. }
  504. // Back compat
  505. if (this.params.backend == 'AudioElement') {
  506. this.params.backend = 'MediaElement';
  507. }
  508. if (this.params.backend == 'WebAudio' && !this.Backend.prototype.supportsWebAudio.call(null)) {
  509. this.params.backend = 'MediaElement';
  510. }
  511. this.backend = new this.Backend(this.params);
  512. this.backend.init();
  513. this.fireEvent('backend-created', this.backend);
  514. this.backend.on('finish', () => this.fireEvent('finish'));
  515. this.backend.on('play', () => this.fireEvent('play'));
  516. this.backend.on('pause', () => this.fireEvent('pause'));
  517. this.backend.on('audioprocess', time => {
  518. this.drawer.progress(this.backend.getPlayedPercents());
  519. this.fireEvent('audioprocess', time);
  520. });
  521. }
  522. /**
  523. * Create the peak cache
  524. *
  525. * @private
  526. */
  527. createPeakCache() {
  528. if (this.params.partialRender) {
  529. this.peakCache = new PeakCache();
  530. }
  531. }
  532. /**
  533. * Get the duration of the audio clip
  534. *
  535. * @example const duration = wavesurfer.getDuration();
  536. * @return {number} Duration in seconds
  537. */
  538. getDuration() {
  539. return this.backend.getDuration();
  540. }
  541. /**
  542. * Get the current playback position
  543. *
  544. * @example const currentTime = wavesurfer.getCurrentTime();
  545. * @return {number} Playback position in seconds
  546. */
  547. getCurrentTime() {
  548. return this.backend.getCurrentTime();
  549. }
  550. /**
  551. * Set the current play time in seconds.
  552. *
  553. * @param {Number} seconds A positive number in seconds. E.g. 10 means 10
  554. * seconds, 60 means 1 minute
  555. */
  556. setCurrentTime(seconds) {
  557. if (this.getDuration() >= seconds) {
  558. this.seekTo(1);
  559. } else {
  560. this.seekTo(seconds/this.getDuration());
  561. }
  562. }
  563. /**
  564. * Starts playback from the current position. Optional start and end
  565. * measured in seconds can be used to set the range of audio to play.
  566. *
  567. * @param {?number} start Position to start at
  568. * @param {?number} end Position to end at
  569. * @emits WaveSurfer#interaction
  570. * @example
  571. * // play from second 1 to 5
  572. * wavesurfer.play(1, 5);
  573. */
  574. play(start, end) {
  575. this.fireEvent('interaction', () => this.play(start, end));
  576. this.backend.play(start, end);
  577. }
  578. /**
  579. * Stops playback
  580. *
  581. * @example wavesurfer.pause();
  582. */
  583. pause() {
  584. this.backend.isPaused() || this.backend.pause();
  585. }
  586. /**
  587. * Toggle playback
  588. *
  589. * @example wavesurfer.playPause();
  590. */
  591. playPause() {
  592. this.backend.isPaused() ? this.play() : this.pause();
  593. }
  594. /**
  595. * Get the current playback state
  596. *
  597. * @example const isPlaying = wavesurfer.isPlaying();
  598. * @return {boolean} False if paused, true if playing
  599. */
  600. isPlaying() {
  601. return !this.backend.isPaused();
  602. }
  603. /**
  604. * Skip backward
  605. *
  606. * @param {?number} seconds Amount to skip back, if not specified `skipLength`
  607. * is used
  608. * @example wavesurfer.skipBackward();
  609. */
  610. skipBackward(seconds) {
  611. this.skip(-seconds || -this.params.skipLength);
  612. }
  613. /**
  614. * Skip forward
  615. *
  616. * @param {?number} seconds Amount to skip back, if not specified `skipLength`
  617. * is used
  618. * @example wavesurfer.skipForward();
  619. */
  620. skipForward(seconds) {
  621. this.skip(seconds || this.params.skipLength);
  622. }
  623. /**
  624. * Skip a number of seconds from the current position (use a negative value
  625. * to go backwards).
  626. *
  627. * @param {number} offset Amount to skip back or forwards
  628. * @example
  629. * // go back 2 seconds
  630. * wavesurfer.skip(-2);
  631. */
  632. skip(offset) {
  633. const duration = this.getDuration() || 1;
  634. let position = this.getCurrentTime() || 0;
  635. position = Math.max(0, Math.min(duration, position + (offset || 0)));
  636. this.seekAndCenter(position / duration);
  637. }
  638. /**
  639. * Seeks to a position and centers the view
  640. *
  641. * @param {number} progress Between 0 (=beginning) and 1 (=end)
  642. * @example
  643. * // seek and go to the middle of the audio
  644. * wavesurfer.seekTo(0.5);
  645. */
  646. seekAndCenter(progress) {
  647. this.seekTo(progress);
  648. this.drawer.recenter(progress);
  649. }
  650. /**
  651. * Seeks to a position
  652. *
  653. * @param {number} progress Between 0 (=beginning) and 1 (=end)
  654. * @emits WaveSurfer#interaction
  655. * @emits WaveSurfer#seek
  656. * @example
  657. * // seek to the middle of the audio
  658. * wavesurfer.seekTo(0.5);
  659. */
  660. seekTo(progress) {
  661. this.fireEvent('interaction', () => this.seekTo(progress));
  662. const paused = this.backend.isPaused();
  663. // avoid draw wrong position while playing backward seeking
  664. if (!paused) {
  665. this.backend.pause();
  666. }
  667. // avoid small scrolls while paused seeking
  668. const oldScrollParent = this.params.scrollParent;
  669. this.params.scrollParent = false;
  670. this.backend.seekTo(progress * this.getDuration());
  671. this.drawer.progress(this.backend.getPlayedPercents());
  672. if (!paused) {
  673. this.backend.play();
  674. }
  675. this.params.scrollParent = oldScrollParent;
  676. this.fireEvent('seek', progress);
  677. }
  678. /**
  679. * Stops and goes to the beginning.
  680. *
  681. * @example wavesurfer.stop();
  682. */
  683. stop() {
  684. this.pause();
  685. this.seekTo(0);
  686. this.drawer.progress(0);
  687. }
  688. /**
  689. * Set the playback volume.
  690. *
  691. * @param {number} newVolume A value between 0 and 1, 0 being no
  692. * volume and 1 being full volume.
  693. */
  694. setVolume(newVolume) {
  695. this.backend.setVolume(newVolume);
  696. }
  697. /**
  698. * Get the playback volume.
  699. *
  700. * @return {number} A value between 0 and 1, 0 being no
  701. * volume and 1 being full volume.
  702. */
  703. getVolume () {
  704. return this.backend.getVolume();
  705. }
  706. /**
  707. * Set the playback rate.
  708. *
  709. * @param {number} rate A positive number. E.g. 0.5 means half the normal
  710. * speed, 2 means double speed and so on.
  711. * @example wavesurfer.setPlaybackRate(2);
  712. */
  713. setPlaybackRate(rate) {
  714. this.backend.setPlaybackRate(rate);
  715. }
  716. /**
  717. * Get the playback rate.
  718. *
  719. * @return {number}
  720. */
  721. getPlaybackRate() {
  722. return this.backend.getPlaybackRate();
  723. }
  724. /**
  725. * Toggle the volume on and off. It not currenly muted it will save the
  726. * current volume value and turn the volume off. If currently muted then it
  727. * will restore the volume to the saved value, and then rest the saved
  728. * value.
  729. *
  730. * @example wavesurfer.toggleMute();
  731. */
  732. toggleMute() {
  733. this.setMute(!this.isMuted);
  734. }
  735. /**
  736. * Enable or disable muted audio
  737. *
  738. * @param {boolean} mute
  739. * @example
  740. * // unmute
  741. * wavesurfer.setMute(false);
  742. */
  743. setMute(mute) {
  744. // ignore all muting requests if the audio is already in that state
  745. if (mute === this.isMuted) {
  746. return;
  747. }
  748. if (mute) {
  749. // If currently not muted then save current volume,
  750. // turn off the volume and update the mute properties
  751. this.savedVolume = this.backend.getVolume();
  752. this.backend.setVolume(0);
  753. this.isMuted = true;
  754. } else {
  755. // If currently muted then restore to the saved volume
  756. // and update the mute properties
  757. this.backend.setVolume(this.savedVolume);
  758. this.isMuted = false;
  759. }
  760. }
  761. /**
  762. * Get the current mute status.
  763. *
  764. * @example const isMuted = wavesurfer.getMute();
  765. * @return {boolean}
  766. */
  767. getMute() {
  768. return this.isMuted;
  769. }
  770. /**
  771. * Get the list of current set filters as an array.
  772. *
  773. * Filters must be set with setFilters method first
  774. *
  775. * @return {array}
  776. */
  777. getFilters() {
  778. return this.backend.filters || [];
  779. }
  780. /**
  781. * Toggles `scrollParent` and redraws
  782. *
  783. * @example wavesurfer.toggleScroll();
  784. */
  785. toggleScroll() {
  786. this.params.scrollParent = !this.params.scrollParent;
  787. this.drawBuffer();
  788. }
  789. /**
  790. * Toggle mouse interaction
  791. *
  792. * @example wavesurfer.toggleInteraction();
  793. */
  794. toggleInteraction() {
  795. this.params.interact = !this.params.interact;
  796. }
  797. /**
  798. * Get the correct peaks for current wave viewport and render wave
  799. *
  800. * @private
  801. * @emits WaveSurfer#redraw
  802. */
  803. drawBuffer() {
  804. const nominalWidth = Math.round(
  805. this.getDuration() * this.params.minPxPerSec * this.params.pixelRatio
  806. );
  807. const parentWidth = this.drawer.getWidth();
  808. let width = nominalWidth;
  809. let start = this.drawer.getScrollX();
  810. let end = Math.max(start + parentWidth, width);
  811. // Fill container
  812. if (this.params.fillParent && (!this.params.scrollParent || nominalWidth < parentWidth)) {
  813. width = parentWidth;
  814. start = 0;
  815. end = width;
  816. }
  817. let peaks;
  818. if (this.params.partialRender) {
  819. const newRanges = this.peakCache.addRangeToPeakCache(width, start, end);
  820. let i;
  821. for (i = 0; i < newRanges.length; i++) {
  822. peaks = this.backend.getPeaks(width, newRanges[i][0], newRanges[i][1]);
  823. this.drawer.drawPeaks(peaks, width, newRanges[i][0], newRanges[i][1]);
  824. }
  825. } else {
  826. peaks = this.backend.getPeaks(width, start, end);
  827. this.drawer.drawPeaks(peaks, width, start, end);
  828. }
  829. this.fireEvent('redraw', peaks, width);
  830. }
  831. /**
  832. * Horizontally zooms the waveform in and out. It also changes the parameter
  833. * `minPxPerSec` and enables the `scrollParent` option. Calling the function
  834. * with a falsey parameter will reset the zoom state.
  835. *
  836. * @param {?number} pxPerSec Number of horizontal pixels per second of
  837. * audio, if none is set the waveform returns to unzoomed state
  838. * @emits WaveSurfer#zoom
  839. * @example wavesurfer.zoom(20);
  840. */
  841. zoom(pxPerSec) {
  842. if (!pxPerSec) {
  843. this.params.minPxPerSec = this.defaultParams.minPxPerSec;
  844. this.params.scrollParent = false;
  845. } else {
  846. this.params.minPxPerSec = pxPerSec;
  847. this.params.scrollParent = true;
  848. }
  849. this.drawBuffer();
  850. this.drawer.progress(this.backend.getPlayedPercents());
  851. this.drawer.recenter(
  852. this.getCurrentTime() / this.getDuration()
  853. );
  854. this.fireEvent('zoom', pxPerSec);
  855. }
  856. /**
  857. * Decode buffer and load
  858. *
  859. * @private
  860. * @param {ArrayBuffer} arraybuffer
  861. */
  862. loadArrayBuffer(arraybuffer) {
  863. this.decodeArrayBuffer(arraybuffer, data => {
  864. if (!this.isDestroyed) {
  865. this.loadDecodedBuffer(data);
  866. }
  867. });
  868. }
  869. /**
  870. * Directly load an externally decoded AudioBuffer
  871. *
  872. * @private
  873. * @param {AudioBuffer} buffer
  874. * @emits WaveSurfer#ready
  875. */
  876. loadDecodedBuffer(buffer) {
  877. this.backend.load(buffer);
  878. this.drawBuffer();
  879. this.fireEvent('ready');
  880. this.isReady = true;
  881. }
  882. /**
  883. * Loads audio data from a Blob or File object
  884. *
  885. * @param {Blob|File} blob Audio data
  886. * @example
  887. */
  888. loadBlob(blob) {
  889. // Create file reader
  890. const reader = new FileReader();
  891. reader.addEventListener('progress', e => this.onProgress(e));
  892. reader.addEventListener('load', e => this.loadArrayBuffer(e.target.result));
  893. reader.addEventListener('error', () => this.fireEvent('error', 'Error reading file'));
  894. reader.readAsArrayBuffer(blob);
  895. this.empty();
  896. }
  897. /**
  898. * Loads audio and re-renders the waveform.
  899. *
  900. * @param {string} url The url of the audio file
  901. * @param {?number[]|number[][]} peaks Wavesurfer does not have to decode the audio to
  902. * render the waveform if this is specified
  903. * @param {?string} preload (Use with backend `MediaElement`)
  904. * `'none'|'metadata'|'auto'` Preload attribute for the media element
  905. * @example
  906. * // using ajax or media element to load (depending on backend)
  907. * wavesurfer.load('http://example.com/demo.wav');
  908. *
  909. * // setting preload attribute with media element backend and supplying
  910. * peaks wavesurfer.load(
  911. * 'http://example.com/demo.wav',
  912. * [0.0218, 0.0183, 0.0165, 0.0198, 0.2137, 0.2888],
  913. * true,
  914. * );
  915. */
  916. load(url, peaks, preload) {
  917. this.empty();
  918. this.isMuted = false;
  919. switch (this.params.backend) {
  920. case 'WebAudio': return this.loadBuffer(url, peaks);
  921. case 'MediaElement': return this.loadMediaElement(url, peaks, preload);
  922. }
  923. }
  924. /**
  925. * Loads audio using Web Audio buffer backend.
  926. *
  927. * @private
  928. * @param {string} url
  929. * @param {?number[]|number[][]} peaks
  930. */
  931. loadBuffer(url, peaks) {
  932. const load = action => {
  933. if (action) {
  934. this.tmpEvents.push(this.once('ready', action));
  935. }
  936. return this.getArrayBuffer(url, (data) => this.loadArrayBuffer(data));
  937. };
  938. if (peaks) {
  939. this.backend.setPeaks(peaks);
  940. this.drawBuffer();
  941. this.tmpEvents.push(this.once('interaction', load));
  942. } else {
  943. return load();
  944. }
  945. }
  946. /**
  947. * Either create a media element, or load an existing media element.
  948. *
  949. * @private
  950. * @param {string|HTMLElement} urlOrElt Either a path to a media file, or an
  951. * existing HTML5 Audio/Video Element
  952. * @param {number[]|number[][]} peaks Array of peaks. Required to bypass web audio
  953. * dependency
  954. * @param {?boolean} preload Set to true if the preload attribute of the
  955. * audio element should be enabled
  956. */
  957. loadMediaElement(urlOrElt, peaks, preload) {
  958. let url = urlOrElt;
  959. if (typeof urlOrElt === 'string') {
  960. this.backend.load(url, this.mediaContainer, peaks, preload);
  961. } else {
  962. const elt = urlOrElt;
  963. this.backend.loadElt(elt, peaks);
  964. // If peaks are not provided,
  965. // url = element.src so we can get peaks with web audio
  966. url = elt.src;
  967. }
  968. this.tmpEvents.push(
  969. this.backend.once('canplay', () => {
  970. this.drawBuffer();
  971. this.fireEvent('ready');
  972. this.isReady = true;
  973. }),
  974. this.backend.once('error', err => this.fireEvent('error', err))
  975. );
  976. // If no pre-decoded peaks provided or pre-decoded peaks are
  977. // provided with forceDecode flag, attempt to download the
  978. // audio file and decode it with Web Audio.
  979. if (peaks) {
  980. this.backend.setPeaks(peaks);
  981. }
  982. if ((!peaks || this.params.forceDecode) && this.backend.supportsWebAudio()) {
  983. this.getArrayBuffer(url, arraybuffer => {
  984. this.decodeArrayBuffer(arraybuffer, buffer => {
  985. this.backend.buffer = buffer;
  986. this.backend.setPeaks(null);
  987. this.drawBuffer();
  988. this.fireEvent('waveform-ready');
  989. });
  990. });
  991. }
  992. }
  993. /**
  994. * Decode an array buffer and pass data to a callback
  995. *
  996. * @private
  997. * @param {Object} arraybuffer
  998. * @param {function} callback
  999. */
  1000. decodeArrayBuffer(arraybuffer, callback) {
  1001. this.arraybuffer = arraybuffer;
  1002. this.backend.decodeArrayBuffer(
  1003. arraybuffer,
  1004. data => {
  1005. // Only use the decoded data if we haven't been destroyed or
  1006. // another decode started in the meantime
  1007. if (!this.isDestroyed && this.arraybuffer == arraybuffer) {
  1008. callback(data);
  1009. this.arraybuffer = null;
  1010. }
  1011. },
  1012. () => this.fireEvent('error', 'Error decoding audiobuffer')
  1013. );
  1014. }
  1015. /**
  1016. * Load an array buffer by ajax and pass to a callback
  1017. *
  1018. * @param {string} url
  1019. * @param {function} callback
  1020. * @private
  1021. */
  1022. getArrayBuffer(url, callback) {
  1023. const ajax = util.ajax({
  1024. url: url,
  1025. responseType: 'arraybuffer'
  1026. });
  1027. this.currentAjax = ajax;
  1028. this.tmpEvents.push(
  1029. ajax.on('progress', e => {
  1030. this.onProgress(e);
  1031. }),
  1032. ajax.on('success', (data, e) => {
  1033. callback(data);
  1034. this.currentAjax = null;
  1035. }),
  1036. ajax.on('error', e => {
  1037. this.fireEvent('error', 'XHR error: ' + e.target.statusText);
  1038. this.currentAjax = null;
  1039. })
  1040. );
  1041. return ajax;
  1042. }
  1043. /**
  1044. * Called while the audio file is loading
  1045. *
  1046. * @private
  1047. * @param {Event} e
  1048. * @emits WaveSurfer#loading
  1049. */
  1050. onProgress(e) {
  1051. let percentComplete;
  1052. if (e.lengthComputable) {
  1053. percentComplete = e.loaded / e.total;
  1054. } else {
  1055. // Approximate progress with an asymptotic
  1056. // function, and assume downloads in the 1-3 MB range.
  1057. percentComplete = e.loaded / (e.loaded + 1000000);
  1058. }
  1059. this.fireEvent('loading', Math.round(percentComplete * 100), e.target);
  1060. }
  1061. /**
  1062. * Exports PCM data into a JSON array and opens in a new window.
  1063. *
  1064. * @param {number} length=1024 The scale in which to export the peaks. (Integer)
  1065. * @param {number} accuracy=10000 (Integer)
  1066. * @param {?boolean} noWindow Set to true to disable opening a new
  1067. * window with the JSON
  1068. * @param {number} start
  1069. * @todo Update exportPCM to work with new getPeaks signature
  1070. * @return {JSON} JSON of peaks
  1071. */
  1072. exportPCM(length, accuracy, noWindow, start) {
  1073. length = length || 1024;
  1074. start = start || 0;
  1075. accuracy = accuracy || 10000;
  1076. noWindow = noWindow || false;
  1077. const peaks = this.backend.getPeaks(length, start);
  1078. const arr = [].map.call(peaks, val => Math.round(val * accuracy) / accuracy);
  1079. const json = JSON.stringify(arr);
  1080. if (!noWindow) {
  1081. window.open('data:application/json;charset=utf-8,' +
  1082. encodeURIComponent(json));
  1083. }
  1084. return json;
  1085. }
  1086. /**
  1087. * Save waveform image as data URI.
  1088. *
  1089. * The default format is `image/png`. Other supported types are
  1090. * `image/jpeg` and `image/webp`.
  1091. *
  1092. * @param {string} format='image/png'
  1093. * @param {number} quality=1
  1094. * @return {string} data URI of image
  1095. */
  1096. exportImage(format, quality) {
  1097. if (!format) {
  1098. format = 'image/png';
  1099. }
  1100. if (!quality) {
  1101. quality = 1;
  1102. }
  1103. return this.drawer.getImage(format, quality);
  1104. }
  1105. /**
  1106. * Cancel any ajax request currently in progress
  1107. */
  1108. cancelAjax() {
  1109. if (this.currentAjax) {
  1110. this.currentAjax.xhr.abort();
  1111. this.currentAjax = null;
  1112. }
  1113. }
  1114. /**
  1115. * @private
  1116. */
  1117. clearTmpEvents() {
  1118. this.tmpEvents.forEach(e => e.un());
  1119. }
  1120. /**
  1121. * Display empty waveform.
  1122. */
  1123. empty() {
  1124. if (!this.backend.isPaused()) {
  1125. this.stop();
  1126. this.backend.disconnectSource();
  1127. }
  1128. this.cancelAjax();
  1129. this.clearTmpEvents();
  1130. this.drawer.progress(0);
  1131. this.drawer.setWidth(0);
  1132. this.drawer.drawPeaks({ length: this.drawer.getWidth() }, 0);
  1133. }
  1134. /**
  1135. * Remove events, elements and disconnect WebAudio nodes.
  1136. *
  1137. * @emits WaveSurfer#destroy
  1138. */
  1139. destroy() {
  1140. this.destroyAllPlugins();
  1141. this.fireEvent('destroy');
  1142. this.cancelAjax();
  1143. this.clearTmpEvents();
  1144. this.unAll();
  1145. if (this.params.responsive) {
  1146. window.removeEventListener('resize', this._onResize, true);
  1147. }
  1148. this.backend.destroy();
  1149. this.drawer.destroy();
  1150. this.isDestroyed = true;
  1151. this.arraybuffer = null;
  1152. }
  1153. }