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