not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1353 lines
38 KiB

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