howler.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. /*!
  2. * howler.js v1.1.12
  3. * howlerjs.com
  4. *
  5. * (c) 2013, James Simpson of GoldFire Studios
  6. * goldfirestudios.com
  7. *
  8. * MIT License
  9. */
  10. (function() {
  11. // setup
  12. var cache = {};
  13. // setup the audio context
  14. var ctx = null,
  15. usingWebAudio = true,
  16. noAudio = false;
  17. if (typeof AudioContext !== 'undefined') {
  18. ctx = new AudioContext();
  19. } else if (typeof webkitAudioContext !== 'undefined') {
  20. ctx = new webkitAudioContext();
  21. } else if (typeof Audio !== 'undefined') {
  22. usingWebAudio = false;
  23. try {
  24. new Audio();
  25. } catch(e) {
  26. noAudio = true;
  27. }
  28. } else {
  29. usingWebAudio = false;
  30. noAudio = true;
  31. }
  32. // create a master gain node
  33. if (usingWebAudio) {
  34. var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain();
  35. masterGain.gain.value = 1;
  36. masterGain.connect(ctx.destination);
  37. }
  38. // create global controller
  39. var HowlerGlobal = function() {
  40. this._volume = 1;
  41. this._muted = false;
  42. this.usingWebAudio = usingWebAudio;
  43. this._howls = [];
  44. };
  45. HowlerGlobal.prototype = {
  46. /**
  47. * Get/set the global volume for all sounds.
  48. * @param {Float} vol Volume from 0.0 to 1.0.
  49. * @return {Howler/Float} Returns self or current volume.
  50. */
  51. volume: function(vol) {
  52. var self = this;
  53. // make sure volume is a number
  54. vol = parseFloat(vol);
  55. if (vol && vol >= 0 && vol <= 1) {
  56. self._volume = vol;
  57. if (usingWebAudio) {
  58. masterGain.gain.value = vol;
  59. }
  60. // loop through cache and change volume of all nodes that are using HTML5 Audio
  61. for (var key in self._howls) {
  62. if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) {
  63. // loop through the audio nodes
  64. for (var i=0; i<self._howls[key]._audioNode.length; i++) {
  65. self._howls[key]._audioNode[i].volume = self._howls[key]._volume * self._volume;
  66. }
  67. }
  68. }
  69. return self;
  70. }
  71. // return the current global volume
  72. return (usingWebAudio) ? masterGain.gain.value : self._volume;
  73. },
  74. /**
  75. * Mute all sounds.
  76. * @return {Howler}
  77. */
  78. mute: function() {
  79. this._setMuted(true);
  80. return this;
  81. },
  82. /**
  83. * Unmute all sounds.
  84. * @return {Howler}
  85. */
  86. unmute: function() {
  87. this._setMuted(false);
  88. return this;
  89. },
  90. /**
  91. * Handle muting and unmuting globally.
  92. * @param {Boolean} muted Is muted or not.
  93. */
  94. _setMuted: function(muted) {
  95. var self = this;
  96. self._muted = muted;
  97. if (usingWebAudio) {
  98. masterGain.gain.value = muted ? 0 : self._volume;
  99. }
  100. for (var key in self._howls) {
  101. if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) {
  102. // loop through the audio nodes
  103. for (var i=0; i<self._howls[key]._audioNode.length; i++) {
  104. self._howls[key]._audioNode[i].muted = muted;
  105. }
  106. }
  107. }
  108. }
  109. };
  110. // allow access to the global audio controls
  111. var Howler = new HowlerGlobal();
  112. // check for browser codec support
  113. var audioTest = null;
  114. if (!noAudio) {
  115. audioTest = new Audio();
  116. var codecs = {
  117. mp3: !!audioTest.canPlayType('audio/mpeg;').replace(/^no$/,''),
  118. opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,''),
  119. ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,''),
  120. wav: !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/,''),
  121. m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/,''),
  122. webm: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,'')
  123. };
  124. }
  125. // setup the audio object
  126. var Howl = function(o) {
  127. var self = this;
  128. // setup the defaults
  129. self._autoplay = o.autoplay || false;
  130. self._buffer = o.buffer || false;
  131. self._duration = o.duration || 0;
  132. self._format = o.format || null;
  133. self._loop = o.loop || false;
  134. self._loaded = false;
  135. self._sprite = o.sprite || {};
  136. self._src = o.src || '';
  137. self._pos3d = o.pos3d || [0, 0, -0.5];
  138. self._volume = o.volume || 1;
  139. self._urls = o.urls || [];
  140. self._rate = o.rate || 1;
  141. // setup event functions
  142. self._onload = [o.onload || function() {}];
  143. self._onloaderror = [o.onloaderror || function() {}];
  144. self._onend = [o.onend || function() {}];
  145. self._onpause = [o.onpause || function() {}];
  146. self._onplay = [o.onplay || function() {}];
  147. self._onendTimer = [];
  148. // Web Audio or HTML5 Audio?
  149. self._webAudio = usingWebAudio && !self._buffer;
  150. // check if we need to fall back to HTML5 Audio
  151. self._audioNode = [];
  152. if (self._webAudio) {
  153. self._setupAudioNode();
  154. }
  155. // add this to an array of Howl's to allow global control
  156. Howler._howls.push(self);
  157. // load the track
  158. self.load();
  159. };
  160. // setup all of the methods
  161. Howl.prototype = {
  162. /**
  163. * Load an audio file.
  164. * @return {Howl}
  165. */
  166. load: function() {
  167. var self = this,
  168. url = null;
  169. // if no audio is available, quit immediately
  170. if (noAudio) {
  171. self.on('loaderror');
  172. return;
  173. }
  174. var canPlay = {
  175. mp3: codecs.mp3,
  176. opus: codecs.opus,
  177. ogg: codecs.ogg,
  178. wav: codecs.wav,
  179. m4a: codecs.m4a,
  180. weba: codecs.webm
  181. };
  182. // loop through source URLs and pick the first one that is compatible
  183. for (var i=0; i<self._urls.length; i++) {
  184. var ext;
  185. if (self._format) {
  186. // use specified audio format if available
  187. ext = self._format;
  188. } else {
  189. // figure out the filetype (whether an extension or base64 data)
  190. ext = self._urls[i].toLowerCase().match(/.+\.([^?]+)(\?|$)/);
  191. ext = (ext && ext.length >= 2) ? ext[1] : self._urls[i].toLowerCase().match(/data\:audio\/([^?]+);/)[1];
  192. }
  193. if (canPlay[ext]) {
  194. url = self._urls[i];
  195. break;
  196. }
  197. }
  198. if (!url) {
  199. self.on('loaderror');
  200. return;
  201. }
  202. self._src = url;
  203. if (self._webAudio) {
  204. loadBuffer(self, url);
  205. } else {
  206. var newNode = new Audio();
  207. self._audioNode.push(newNode);
  208. // setup the new audio node
  209. newNode.src = url;
  210. newNode._pos = 0;
  211. newNode.preload = 'auto';
  212. newNode.volume = (Howler._muted) ? 0 : self._volume * Howler.volume();
  213. // add this sound to the cache
  214. cache[url] = self;
  215. // setup the event listener to start playing the sound
  216. // as soon as it has buffered enough
  217. var listener = function() {
  218. self._duration = newNode.duration;
  219. // setup a sprite if none is defined
  220. if (Object.getOwnPropertyNames(self._sprite).length === 0) {
  221. self._sprite = {_default: [0, self._duration * 1000]};
  222. }
  223. if (!self._loaded) {
  224. self._loaded = true;
  225. self.on('load');
  226. }
  227. if (self._autoplay) {
  228. self.play();
  229. }
  230. // clear the event listener
  231. newNode.removeEventListener('canplaythrough', listener, false);
  232. };
  233. newNode.addEventListener('canplaythrough', listener, false);
  234. newNode.load();
  235. }
  236. return self;
  237. },
  238. /**
  239. * Get/set the URLs to be pulled from to play in this source.
  240. * @param {Array} urls Arry of URLs to load from
  241. * @return {Howl} Returns self or the current URLs
  242. */
  243. urls: function(urls) {
  244. var self = this;
  245. if (urls) {
  246. self.stop();
  247. self._urls = (typeof urls === 'string') ? [urls] : urls;
  248. self._loaded = false;
  249. self.load();
  250. return self;
  251. } else {
  252. return self._urls;
  253. }
  254. },
  255. /**
  256. * Play a sound from the current time (0 by default).
  257. * @param {String} sprite (optional) Plays from the specified position in the sound sprite definition.
  258. * @param {Function} callback (optional) Returns the unique playback id for this sound instance.
  259. * @return {Howl}
  260. */
  261. play: function(sprite, callback) {
  262. var self = this;
  263. // if no sprite was passed but a callback was, update the variables
  264. if (typeof sprite === 'function') {
  265. callback = sprite;
  266. }
  267. // use the default sprite if none is passed
  268. if (!sprite || typeof sprite === 'function') {
  269. sprite = '_default';
  270. }
  271. // if the sound hasn't been loaded, add it to the event queue
  272. if (!self._loaded) {
  273. self.on('load', function() {
  274. self.play(sprite, callback);
  275. });
  276. return self;
  277. }
  278. // if the sprite doesn't exist, play nothing
  279. if (!self._sprite[sprite]) {
  280. if (typeof callback === 'function') callback();
  281. return self;
  282. }
  283. // get the node to playback
  284. self._inactiveNode(function(node) {
  285. // persist the sprite being played
  286. node._sprite = sprite;
  287. // determine where to start playing from
  288. var pos = (node._pos > 0) ? node._pos : self._sprite[sprite][0] / 1000,
  289. duration = self._sprite[sprite][1] / 1000 - node._pos;
  290. // determine if this sound should be looped
  291. var loop = !!(self._loop || self._sprite[sprite][2]);
  292. // set timer to fire the 'onend' event
  293. var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '',
  294. timerId;
  295. (function() {
  296. var data = {
  297. id: soundId,
  298. sprite: sprite,
  299. loop: loop
  300. };
  301. timerId = setTimeout(function() {
  302. // if looping, restart the track
  303. if (!self._webAudio && loop) {
  304. self.stop(data.id, data.timer).play(sprite, data.id);
  305. }
  306. // set web audio node to paused at end
  307. if (self._webAudio && !loop) {
  308. self._nodeById(data.id).paused = true;
  309. }
  310. // end the track if it is HTML audio and a sprite
  311. if (!self._webAudio && !loop) {
  312. self.stop(data.id, data.timer);
  313. }
  314. // fire ended event
  315. self.on('end', soundId);
  316. }, duration * 1000);
  317. // store the reference to the timer
  318. self._onendTimer.push(timerId);
  319. // remember which timer to cancel
  320. data.timer = self._onendTimer[self._onendTimer.length - 1];
  321. })();
  322. if (self._webAudio) {
  323. var loopStart = self._sprite[sprite][0] / 1000,
  324. loopEnd = self._sprite[sprite][1] / 1000;
  325. // set the play id to this node and load into context
  326. node.id = soundId;
  327. node.paused = false;
  328. refreshBuffer(self, [loop, loopStart, loopEnd], soundId);
  329. self._playStart = ctx.currentTime;
  330. node.gain.value = self._volume;
  331. if (typeof node.bufferSource.start === 'undefined') {
  332. node.bufferSource.noteGrainOn(0, pos, duration);
  333. } else {
  334. node.bufferSource.start(0, pos, duration);
  335. }
  336. } else {
  337. if (node.readyState === 4) {
  338. node.id = soundId;
  339. node.currentTime = pos;
  340. node.muted = Howler._muted;
  341. node.volume = self._volume * Howler.volume();
  342. setTimeout(function() { node.play(); }, 0);
  343. } else {
  344. self._clearEndTimer(timerId);
  345. (function(){
  346. var sound = self,
  347. playSprite = sprite,
  348. fn = callback,
  349. newNode = node;
  350. var listener = function() {
  351. sound.play(playSprite, fn);
  352. // clear the event listener
  353. newNode.removeEventListener('canplaythrough', listener, false);
  354. };
  355. newNode.addEventListener('canplaythrough', listener, false);
  356. })();
  357. return self;
  358. }
  359. }
  360. // fire the play event and send the soundId back in the callback
  361. self.on('play');
  362. if (typeof callback === 'function') callback(soundId);
  363. return self;
  364. });
  365. return self;
  366. },
  367. /**
  368. * Pause playback and save the current position.
  369. * @param {String} id (optional) The play instance ID.
  370. * @param {String} timerId (optional) Clear the correct timeout ID.
  371. * @return {Howl}
  372. */
  373. pause: function(id, timerId) {
  374. var self = this;
  375. // if the sound hasn't been loaded, add it to the event queue
  376. if (!self._loaded) {
  377. self.on('play', function() {
  378. self.pause(id);
  379. });
  380. return self;
  381. }
  382. // clear 'onend' timer
  383. self._clearEndTimer(timerId || 0);
  384. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  385. if (activeNode) {
  386. activeNode._pos = self.pos(null, id);
  387. if (self._webAudio) {
  388. // make sure the sound has been created
  389. if (!activeNode.bufferSource) {
  390. return self;
  391. }
  392. activeNode.paused = true;
  393. if (typeof activeNode.bufferSource.stop === 'undefined') {
  394. activeNode.bufferSource.noteOff(0);
  395. } else {
  396. activeNode.bufferSource.stop(0);
  397. }
  398. } else {
  399. activeNode.pause();
  400. }
  401. }
  402. self.on('pause');
  403. return self;
  404. },
  405. /**
  406. * Stop playback and reset to start.
  407. * @param {String} id (optional) The play instance ID.
  408. * @param {String} timerId (optional) Clear the correct timeout ID.
  409. * @return {Howl}
  410. */
  411. stop: function(id, timerId) {
  412. var self = this;
  413. // if the sound hasn't been loaded, add it to the event queue
  414. if (!self._loaded) {
  415. self.on('play', function() {
  416. self.stop(id);
  417. });
  418. return self;
  419. }
  420. // clear 'onend' timer
  421. self._clearEndTimer(timerId || 0);
  422. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  423. if (activeNode) {
  424. activeNode._pos = 0;
  425. if (self._webAudio) {
  426. // make sure the sound has been created
  427. if (!activeNode.bufferSource) {
  428. return self;
  429. }
  430. activeNode.paused = true;
  431. if (typeof activeNode.bufferSource.stop === 'undefined') {
  432. activeNode.bufferSource.noteOff(0);
  433. } else {
  434. activeNode.bufferSource.stop(0);
  435. }
  436. } else {
  437. activeNode.pause();
  438. activeNode.currentTime = 0;
  439. }
  440. }
  441. return self;
  442. },
  443. /**
  444. * Mute this sound.
  445. * @param {String} id (optional) The play instance ID.
  446. * @return {Howl}
  447. */
  448. mute: function(id) {
  449. var self = this;
  450. // if the sound hasn't been loaded, add it to the event queue
  451. if (!self._loaded) {
  452. self.on('play', function() {
  453. self.mute(id);
  454. });
  455. return self;
  456. }
  457. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  458. if (activeNode) {
  459. if (self._webAudio) {
  460. activeNode.gain.value = 0;
  461. } else {
  462. activeNode.volume = 0;
  463. }
  464. }
  465. return self;
  466. },
  467. /**
  468. * Unmute this sound.
  469. * @param {String} id (optional) The play instance ID.
  470. * @return {Howl}
  471. */
  472. unmute: function(id) {
  473. var self = this;
  474. // if the sound hasn't been loaded, add it to the event queue
  475. if (!self._loaded) {
  476. self.on('play', function() {
  477. self.unmute(id);
  478. });
  479. return self;
  480. }
  481. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  482. if (activeNode) {
  483. if (self._webAudio) {
  484. activeNode.gain.value = self._volume;
  485. } else {
  486. activeNode.volume = self._volume;
  487. }
  488. }
  489. return self;
  490. },
  491. /**
  492. * Get/set volume of this sound.
  493. * @param {Float} vol Volume from 0.0 to 1.0.
  494. * @param {String} id (optional) The play instance ID.
  495. * @return {Howl/Float} Returns self or current volume.
  496. */
  497. volume: function(vol, id) {
  498. var self = this;
  499. // make sure volume is a number
  500. vol = parseFloat(vol);
  501. if (vol >= 0 && vol <= 1) {
  502. self._volume = vol;
  503. // if the sound hasn't been loaded, add it to the event queue
  504. if (!self._loaded) {
  505. self.on('play', function() {
  506. self.volume(vol, id);
  507. });
  508. return self;
  509. }
  510. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  511. if (activeNode) {
  512. if (self._webAudio) {
  513. activeNode.gain.value = vol;
  514. } else {
  515. activeNode.volume = vol * Howler.volume();
  516. }
  517. }
  518. return self;
  519. } else {
  520. return self._volume;
  521. }
  522. },
  523. /**
  524. * Get/set whether to loop the sound.
  525. * @param {Boolean} loop To loop or not to loop, that is the question.
  526. * @return {Howl/Boolean} Returns self or current looping value.
  527. */
  528. loop: function(loop) {
  529. var self = this;
  530. if (typeof loop === 'boolean') {
  531. self._loop = loop;
  532. return self;
  533. } else {
  534. return self._loop;
  535. }
  536. },
  537. /**
  538. * Get/set sound sprite definition.
  539. * @param {Object} sprite Example: {spriteName: [offset, duration, loop]}
  540. * @param {Integer} offset Where to begin playback in milliseconds
  541. * @param {Integer} duration How long to play in milliseconds
  542. * @param {Boolean} loop (optional) Set true to loop this sprite
  543. * @return {Howl} Returns current sprite sheet or self.
  544. */
  545. sprite: function(sprite) {
  546. var self = this;
  547. if (typeof sprite === 'object') {
  548. self._sprite = sprite;
  549. return self;
  550. } else {
  551. return self._sprite;
  552. }
  553. },
  554. /**
  555. * Get/set the position of playback.
  556. * @param {Float} pos The position to move current playback to.
  557. * @param {String} id (optional) The play instance ID.
  558. * @return {Howl/Float} Returns self or current playback position.
  559. */
  560. pos: function(pos, id) {
  561. var self = this;
  562. // if the sound hasn't been loaded, add it to the event queue
  563. if (!self._loaded) {
  564. self.on('load', function() {
  565. self.pos(pos);
  566. });
  567. return typeof pos === 'number' ? self : self._pos || 0;
  568. }
  569. // make sure we are dealing with a number for pos
  570. pos = parseFloat(pos);
  571. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  572. if (activeNode) {
  573. if (self._webAudio) {
  574. if (pos >= 0) {
  575. activeNode._pos = pos;
  576. self.pause(id).play(activeNode._sprite, id);
  577. return self;
  578. } else {
  579. return activeNode._pos + (ctx.currentTime - self._playStart);
  580. }
  581. } else {
  582. if (pos >= 0) {
  583. activeNode.currentTime = pos;
  584. return self;
  585. } else {
  586. return activeNode.currentTime;
  587. }
  588. }
  589. } else if (pos >= 0) {
  590. return self;
  591. } else {
  592. // find the first inactive node to return the pos for
  593. for (var i=0; i<self._audioNode.length; i++) {
  594. if (self._audioNode[i].paused && self._audioNode[i].readyState === 4) {
  595. return (self._webAudio) ? self._audioNode[i]._pos : self._audioNode[i].currentTime;
  596. }
  597. }
  598. }
  599. },
  600. /**
  601. * Get/set the 3D position of the audio source.
  602. * The most common usage is to set the 'x' position
  603. * to affect the left/right ear panning. Setting any value higher than
  604. * 1.0 will begin to decrease the volume of the sound as it moves further away.
  605. * NOTE: This only works with Web Audio API, HTML5 Audio playback
  606. * will not be affected.
  607. * @param {Float} x The x-position of the playback from -1000.0 to 1000.0
  608. * @param {Float} y The y-position of the playback from -1000.0 to 1000.0
  609. * @param {Float} z The z-position of the playback from -1000.0 to 1000.0
  610. * @param {String} id (optional) The play instance ID.
  611. * @return {Howl/Array} Returns self or the current 3D position: [x, y, z]
  612. */
  613. pos3d: function(x, y, z, id) {
  614. var self = this;
  615. // set a default for the optional 'y' & 'z'
  616. y = (typeof y === 'undefined' || !y) ? 0 : y;
  617. z = (typeof z === 'undefined' || !z) ? -0.5 : z;
  618. // if the sound hasn't been loaded, add it to the event queue
  619. if (!self._loaded) {
  620. self.on('play', function() {
  621. self.pos3d(x, y, z, id);
  622. });
  623. return self;
  624. }
  625. if (x >= 0 || x < 0) {
  626. if (self._webAudio) {
  627. var activeNode = (id) ? self._nodeById(id) : self._activeNode();
  628. if (activeNode) {
  629. self._pos3d = [x, y, z];
  630. activeNode.panner.setPosition(x, y, z);
  631. }
  632. }
  633. } else {
  634. return self._pos3d;
  635. }
  636. return self;
  637. },
  638. /**
  639. * Fade a currently playing sound between two volumes.
  640. * @param {Number} from The volume to fade from (0.0 to 1.0).
  641. * @param {Number} to The volume to fade to (0.0 to 1.0).
  642. * @param {Number} len Time in milliseconds to fade.
  643. * @param {Function} callback (optional) Fired when the fade is complete.
  644. * @param {String} id (optional) The play instance ID.
  645. * @return {Howl}
  646. */
  647. fade: function(from, to, len, callback, id) {
  648. var self = this,
  649. diff = Math.abs(from - to),
  650. dir = from > to ? 'down' : 'up',
  651. steps = diff / 0.01,
  652. stepTime = len / steps;
  653. // if the sound hasn't been loaded, add it to the event queue
  654. if (!self._loaded) {
  655. self.on('load', function() {
  656. self.fade(from, to, len, callback, id);
  657. });
  658. return self;
  659. }
  660. // set the volume to the start position
  661. self.volume(from, id);
  662. for (var i=1; i<=steps; i++) {
  663. (function() {
  664. var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i,
  665. vol = Math.round(1000 * change) / 1000,
  666. toVol = to;
  667. setTimeout(function() {
  668. self.volume(vol, id);
  669. if (vol === toVol) {
  670. if (callback) callback();
  671. }
  672. }, stepTime * i);
  673. })();
  674. }
  675. },
  676. /**
  677. * [DEPRECATED] Fade in the current sound.
  678. * @param {Float} to Volume to fade to (0.0 to 1.0).
  679. * @param {Number} len Time in milliseconds to fade.
  680. * @param {Function} callback
  681. * @return {Howl}
  682. */
  683. fadeIn: function(to, len, callback) {
  684. return this.volume(0).play().fade(0, to, len, callback);
  685. },
  686. /**
  687. * [DEPRECATED] Fade out the current sound and pause when finished.
  688. * @param {Float} to Volume to fade to (0.0 to 1.0).
  689. * @param {Number} len Time in milliseconds to fade.
  690. * @param {Function} callback
  691. * @param {String} id (optional) The play instance ID.
  692. * @return {Howl}
  693. */
  694. fadeOut: function(to, len, callback, id) {
  695. var self = this;
  696. return self.fade(self._volume, to, len, function() {
  697. if (callback) callback();
  698. self.pause(id);
  699. // fire ended event
  700. self.on('end');
  701. }, id);
  702. },
  703. /**
  704. * Get an audio node by ID.
  705. * @return {Howl} Audio node.
  706. */
  707. _nodeById: function(id) {
  708. var self = this,
  709. node = self._audioNode[0];
  710. // find the node with this ID
  711. for (var i=0; i<self._audioNode.length; i++) {
  712. if (self._audioNode[i].id === id) {
  713. node = self._audioNode[i];
  714. break;
  715. }
  716. }
  717. return node;
  718. },
  719. /**
  720. * Get the first active audio node.
  721. * @return {Howl} Audio node.
  722. */
  723. _activeNode: function() {
  724. var self = this,
  725. node = null;
  726. // find the first playing node
  727. for (var i=0; i<self._audioNode.length; i++) {
  728. if (!self._audioNode[i].paused) {
  729. node = self._audioNode[i];
  730. break;
  731. }
  732. }
  733. // remove excess inactive nodes
  734. self._drainPool();
  735. return node;
  736. },
  737. /**
  738. * Get the first inactive audio node.
  739. * If there is none, create a new one and add it to the pool.
  740. * @param {Function} callback Function to call when the audio node is ready.
  741. */
  742. _inactiveNode: function(callback) {
  743. var self = this,
  744. node = null;
  745. // find first inactive node to recycle
  746. for (var i=0; i<self._audioNode.length; i++) {
  747. if (self._audioNode[i].paused && self._audioNode[i].readyState === 4) {
  748. callback(self._audioNode[i]);
  749. node = true;
  750. break;
  751. }
  752. }
  753. // remove excess inactive nodes
  754. self._drainPool();
  755. if (node) {
  756. return;
  757. }
  758. // create new node if there are no inactives
  759. var newNode;
  760. if (self._webAudio) {
  761. newNode = self._setupAudioNode();
  762. callback(newNode);
  763. } else {
  764. self.load();
  765. newNode = self._audioNode[self._audioNode.length - 1];
  766. newNode.addEventListener('loadedmetadata', function() {
  767. callback(newNode);
  768. });
  769. }
  770. },
  771. /**
  772. * If there are more than 5 inactive audio nodes in the pool, clear out the rest.
  773. */
  774. _drainPool: function() {
  775. var self = this,
  776. inactive = 0,
  777. i;
  778. // count the number of inactive nodes
  779. for (i=0; i<self._audioNode.length; i++) {
  780. if (self._audioNode[i].paused) {
  781. inactive++;
  782. }
  783. }
  784. // remove excess inactive nodes
  785. for (i=self._audioNode.length-1; i>=0; i--) {
  786. if (inactive <= 5) {
  787. break;
  788. }
  789. if (self._audioNode[i].paused) {
  790. // disconnect the audio source if using Web Audio
  791. if (self._webAudio) {
  792. self._audioNode[i].disconnect(0);
  793. }
  794. inactive--;
  795. self._audioNode.splice(i, 1);
  796. }
  797. }
  798. },
  799. /**
  800. * Clear 'onend' timeout before it ends.
  801. * @param {Number} timerId The ID of the sound to be cancelled.
  802. */
  803. _clearEndTimer: function(timerId) {
  804. var self = this,
  805. timer = self._onendTimer.indexOf(timerId);
  806. // make sure the timer gets cleared
  807. timer = timer >= 0 ? timer : 0;
  808. if (self._onendTimer[timer]) {
  809. clearTimeout(self._onendTimer[timer]);
  810. self._onendTimer.splice(timer, 1);
  811. }
  812. },
  813. /**
  814. * Setup the gain node and panner for a Web Audio instance.
  815. * @return {Object} The new audio node.
  816. */
  817. _setupAudioNode: function() {
  818. var self = this,
  819. node = self._audioNode,
  820. index = self._audioNode.length;
  821. // create gain node
  822. node[index] = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain();
  823. node[index].gain.value = self._volume;
  824. node[index].paused = true;
  825. node[index]._pos = 0;
  826. node[index].readyState = 4;
  827. node[index].connect(masterGain);
  828. // create the panner
  829. node[index].panner = ctx.createPanner();
  830. node[index].panner.setPosition(self._pos3d[0], self._pos3d[1], self._pos3d[2]);
  831. node[index].panner.connect(node[index]);
  832. return node[index];
  833. },
  834. /**
  835. * Call/set custom events.
  836. * @param {String} event Event type.
  837. * @param {Function} fn Function to call.
  838. * @return {Howl}
  839. */
  840. on: function(event, fn) {
  841. var self = this,
  842. events = self['_on' + event];
  843. if (typeof fn === "function") {
  844. events.push(fn);
  845. } else {
  846. for (var i=0; i<events.length; i++) {
  847. if (fn) {
  848. events[i].call(self, fn);
  849. } else {
  850. events[i].call(self);
  851. }
  852. }
  853. }
  854. return self;
  855. },
  856. /**
  857. * Remove a custom event.
  858. * @param {String} event Event type.
  859. * @param {Function} fn Listener to remove.
  860. * @return {Howl}
  861. */
  862. off: function(event, fn) {
  863. var self = this,
  864. events = self['_on' + event],
  865. fnString = fn.toString();
  866. // loop through functions in the event for comparison
  867. for (var i=0; i<events.length; i++) {
  868. if (fnString === events[i].toString()) {
  869. events.splice(i, 1);
  870. break;
  871. }
  872. }
  873. return self;
  874. },
  875. /**
  876. * Unload and destroy the current Howl object.
  877. * This will immediately stop all play instances attached to this sound.
  878. */
  879. unload: function() {
  880. var self = this;
  881. // stop playing any active nodes
  882. var nodes = self._audioNode;
  883. for (var i=0; i<self._audioNode.length; i++) {
  884. self.stop(nodes[i].id);
  885. if (!self._webAudio) {
  886. // remove the source if using HTML5 Audio
  887. nodes[i].src = '';
  888. } else {
  889. // disconnect the output from the master gain
  890. nodes[i].disconnect(0);
  891. }
  892. }
  893. // remove the reference in the global Howler object
  894. var index = Howler._howls.indexOf(self);
  895. if (index) {
  896. Howler._howls.splice(index, 1);
  897. }
  898. // delete this sound from the cache
  899. delete cache[self._src];
  900. self = null;
  901. }
  902. };
  903. // only define these functions when using WebAudio
  904. if (usingWebAudio) {
  905. /**
  906. * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API).
  907. * @param {Object} obj The Howl object for the sound to load.
  908. * @param {String} url The path to the sound file.
  909. */
  910. var loadBuffer = function(obj, url) {
  911. // check if the buffer has already been cached
  912. if (url in cache) {
  913. // set the duration from the cache
  914. obj._duration = cache[url].duration;
  915. // load the sound into this object
  916. loadSound(obj);
  917. } else {
  918. // load the buffer from the URL
  919. var xhr = new XMLHttpRequest();
  920. xhr.open('GET', url, true);
  921. xhr.responseType = 'arraybuffer';
  922. xhr.onload = function() {
  923. // decode the buffer into an audio source
  924. ctx.decodeAudioData(xhr.response, function(buffer) {
  925. if (buffer) {
  926. cache[url] = buffer;
  927. loadSound(obj, buffer);
  928. }
  929. });
  930. };
  931. xhr.onerror = function() {
  932. // if there is an error, switch the sound to HTML Audio
  933. if (obj._webAudio) {
  934. obj._buffer = true;
  935. obj._webAudio = false;
  936. obj._audioNode = [];
  937. delete obj._gainNode;
  938. obj.load();
  939. }
  940. };
  941. try {
  942. xhr.send();
  943. } catch (e) {
  944. xhr.onerror();
  945. }
  946. }
  947. };
  948. /**
  949. * Finishes loading the Web Audio API sound and fires the loaded event
  950. * @param {Object} obj The Howl object for the sound to load.
  951. * @param {Objecct} buffer The decoded buffer sound source.
  952. */
  953. var loadSound = function(obj, buffer) {
  954. // set the duration
  955. obj._duration = (buffer) ? buffer.duration : obj._duration;
  956. // setup a sprite if none is defined
  957. if (Object.getOwnPropertyNames(obj._sprite).length === 0) {
  958. obj._sprite = {_default: [0, obj._duration * 1000]};
  959. }
  960. // fire the loaded event
  961. if (!obj._loaded) {
  962. obj._loaded = true;
  963. obj.on('load');
  964. }
  965. if (obj._autoplay) {
  966. obj.play();
  967. }
  968. };
  969. /**
  970. * Load the sound back into the buffer source.
  971. * @param {Object} obj The sound to load.
  972. * @param {Array} loop Loop boolean, pos, and duration.
  973. * @param {String} id (optional) The play instance ID.
  974. */
  975. var refreshBuffer = function(obj, loop, id) {
  976. // determine which node to connect to
  977. var node = obj._nodeById(id);
  978. // setup the buffer source for playback
  979. node.bufferSource = ctx.createBufferSource();
  980. node.bufferSource.buffer = cache[obj._src];
  981. node.bufferSource.connect(node.panner);
  982. node.bufferSource.loop = loop[0];
  983. if (loop[0]) {
  984. node.bufferSource.loopStart = loop[1];
  985. node.bufferSource.loopEnd = loop[1] + loop[2];
  986. }
  987. node.bufferSource.playbackRate.value = obj._rate;
  988. };
  989. }
  990. /**
  991. * Add support for AMD (Asynchronous Module Definition) libraries such as require.js.
  992. */
  993. if (typeof define === 'function' && define.amd) {
  994. define(function() {
  995. return {
  996. Howler: Howler,
  997. Howl: Howl
  998. };
  999. });
  1000. }
  1001. // define globally in case AMD is not available or available but not used
  1002. window.Howler = Howler;
  1003. window.Howl = Howl;
  1004. })();