vendor: 107,970 bytes, lines 1-3248
1/*! 2 * howler.js v2.2.4 3 * howlerjs.com 4 * 5 * (c) 2013-2020, James Simpson of GoldFire Studios 6 * goldfirestudios.com 7 * 8 * MIT License 9 */ 10 11(function() { 12 13 'use strict'; 14 15 /** Global Methods **/ 16 /***************************************************************************/ 17 18 /** 19 * Create the global controller. All contained methods and properties apply 20 * to all sounds that are currently playing or will be in the future. 21 */ 22 var HowlerGlobal = function() { 23 this.init(); 24 }; 25 HowlerGlobal.prototype = { 26 /** 27 * Initialize the global Howler object. 28 * @return {Howler} 29 */ 30 init: function() { 31 var self = this || Howler; 32 33 // Create a global ID counter. 34 self._counter = 1000; 35 36 // Pool of unlocked HTML5 Audio objects. 37 self._html5AudioPool = []; 38 self.html5PoolSize = 10; 39 40 // Internal properties. 41 self._codecs = {}; 42 self._howls = []; 43 self._muted = false; 44 self._volume = 1; 45 self._canPlayEvent = 'canplaythrough'; 46 self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null; 47 48 // Public properties. 49 self.masterGain = null; 50 self.noAudio = false; 51 self.usingWebAudio = true; 52 self.autoSuspend = true; 53 self.ctx = null; 54 55 // Set to false to disable the auto audio unlocker. 56 self.autoUnlock = true; 57 58 // Setup the various state values for global tracking. 59 self._setup(); 60 61 return self; 62 }, 63 64 /** 65 * Get/set the global volume for all sounds. 66 * @param {Float} vol Volume from 0.0 to 1.0. 67 * @return {Howler/Float} Returns self or current volume. 68 */ 69 volume: function(vol) { 70 var self = this || Howler; 71 vol = parseFloat(vol); 72 73 // If we don't have an AudioContext created yet, run the setup. 74 if (!self.ctx) { 75 setupAudioContext(); 76 } 77 78 if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { 79 self._volume = vol; 80 81 // Don't update any of the nodes if we are muted. 82 if (self._muted) { 83 return self; 84 } 85 86 // When using Web Audio, we just need to adjust the master gain. 87 if (self.usingWebAudio) { 88 self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); 89 } 90 91 // Loop through and change volume for all HTML5 audio nodes. 92 for (var i=0; i<self._howls.length; i++) { 93 if (!self._howls[i]._webAudio) { 94 // Get all of the sounds in this Howl group. 95 var ids = self._howls[i]._getSoundIds(); 96 97 // Loop through all sounds and change the volumes. 98 for (var j=0; j<ids.length; j++) { 99 var sound = self._howls[i]._soundById(ids[j]); 100 101 if (sound && sound._node) { 102 sound._node.volume = sound._volume * vol; 103 } 104 } 105 } 106 } 107 108 return self; 109 } 110 111 return self._volume; 112 }, 113 114 /** 115 * Handle muting and unmuting globally. 116 * @param {Boolean} muted Is muted or not. 117 */ 118 mute: function(muted) { 119 var self = this || Howler; 120 121 // If we don't have an AudioContext created yet, run the setup. 122 if (!self.ctx) { 123 setupAudioContext(); 124 } 125 126 self._muted = muted; 127 128 // With Web Audio, we just need to mute the master gain. 129 if (self.usingWebAudio) { 130 self.masterGain.gain.setValueAtTime(muted ? 0 : self._volume, Howler.ctx.currentTime); 131 } 132 133 // Loop through and mute all HTML5 Audio nodes. 134 for (var i=0; i<self._howls.length; i++) { 135 if (!self._howls[i]._webAudio) { 136 // Get all of the sounds in this Howl group. 137 var ids = self._howls[i]._getSoundIds(); 138 139 // Loop through all sounds and mark the audio node as muted. 140 for (var j=0; j<ids.length; j++) { 141 var sound = self._howls[i]._soundById(ids[j]); 142 143 if (sound && sound._node) { 144 sound._node.muted = (muted) ? true : sound._muted; 145 } 146 } 147 } 148 } 149 150 return self; 151 }, 152 153 /** 154 * Handle stopping all sounds globally. 155 */ 156 stop: function() { 157 var self = this || Howler; 158 159 // Loop through all Howls and stop them. 160 for (var i=0; i<self._howls.length; i++) { 161 self._howls[i].stop(); 162 } 163 164 return self; 165 }, 166 167 /** 168 * Unload and destroy all currently loaded Howl objects. 169 * @return {Howler} 170 */ 171 unload: function() { 172 var self = this || Howler; 173 174 for (var i=self._howls.length-1; i>=0; i--) { 175 self._howls[i].unload(); 176 } 177 178 // Create a new AudioContext to make sure it is fully reset. 179 if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { 180 self.ctx.close(); 181 self.ctx = null; 182 setupAudioContext(); 183 } 184 185 return self; 186 }, 187 188 /** 189 * Check for codec support of specific extension. 190 * @param {String} ext Audio file extention. 191 * @return {Boolean} 192 */ 193 codecs: function(ext) { 194 return (this || Howler)._codecs[ext.replace(/^x-/, '')]; 195 }, 196 197 /** 198 * Setup various state values for global tracking. 199 * @return {Howler} 200 */ 201 _setup: function() { 202 var self = this || Howler; 203 204 // Keeps track of the suspend/resume state of the AudioContext. 205 self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; 206 207 // Automatically begin the 30-second suspend process 208 self._autoSuspend(); 209 210 // Check if audio is available. 211 if (!self.usingWebAudio) { 212 // No audio is available on this system if noAudio is set to true. 213 if (typeof Audio !== 'undefined') { 214 try { 215 var test = new Audio(); 216 217 // Check if the canplaythrough event is available. 218 if (typeof test.oncanplaythrough === 'undefined') { 219 self._canPlayEvent = 'canplay'; 220 } 221 } catch(e) { 222 self.noAudio = true; 223 } 224 } else { 225 self.noAudio = true; 226 } 227 } 228 229 // Test to make sure audio isn't disabled in Internet Explorer. 230 try { 231 var test = new Audio(); 232 if (test.muted) { 233 self.noAudio = true; 234 } 235 } catch (e) {} 236 237 // Check for supported codecs. 238 if (!self.noAudio) { 239 self._setupCodecs(); 240 } 241 242 return self; 243 }, 244 245 /** 246 * Check for browser support for various codecs and cache the results. 247 * @return {Howler} 248 */ 249 _setupCodecs: function() { 250 var self = this || Howler; 251 var audioTest = null; 252 253 // Must wrap in a try/catch because IE11 in server mode throws an error. 254 try { 255 audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; 256 } catch (err) { 257 return self; 258 } 259 260 if (!audioTest || typeof audioTest.canPlayType !== 'function') { 261 return self; 262 } 263 264 var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); 265 266 // Opera version <33 has mixed MP3 support, so we need to check for and block it. 267 var ua = self._navigator ? self._navigator.userAgent : ''; 268 var checkOpera = ua.match(/OPR\/(\d+)/g); 269 var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); 270 var checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; 271 var safariVersion = ua.match(/Version\/(.*?) /); 272 var isOldSafari = (checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15); 273 274 self._codecs = { 275 mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), 276 mpeg: !!mpegTest, 277 opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), 278 ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), 279 oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), 280 wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType('audio/wav')).replace(/^no$/, ''), 281 aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), 282 caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), 283 m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), 284 m4b: !!(audioTest.canPlayType('audio/x-m4b;') || audioTest.canPlayType('audio/m4b;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), 285 mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), 286 weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), 287 webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), 288 dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), 289 flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') 290 }; 291 292 return self; 293 }, 294 295 /** 296 * Some browsers/devices will only allow audio to be played after a user interaction. 297 * Attempt to automatically unlock audio on the first user interaction. 298 * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ 299 * @return {Howler} 300 */ 301 _unlockAudio: function() { 302 var self = this || Howler; 303 304 // Only run this if Web Audio is supported and it hasn't already been unlocked. 305 if (self._audioUnlocked || !self.ctx) { 306 return; 307 } 308 309 self._audioUnlocked = false; 310 self.autoUnlock = false; 311 312 // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. 313 // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. 314 // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. 315 if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { 316 self._mobileUnloaded = true; 317 self.unload(); 318 } 319 320 // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: 321 // http://stackoverflow.com/questions/24119684 322 self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); 323 324 // Call this method on touch start to create and play a buffer, 325 // then check if the audio actually played to determine if 326 // audio has now been unlocked on iOS, Android, etc. 327 var unlock = function(e) { 328 // Create a pool of unlocked HTML5 Audio objects that can 329 // be used for playing sounds without user interaction. HTML5 330 // Audio objects must be individually unlocked, as opposed 331 // to the WebAudio API which only needs a single activation. 332 // This must occur before WebAudio setup or the source.onended 333 // event will not fire. 334 while (self._html5AudioPool.length < self.html5PoolSize) { 335 try { 336 var audioNode = new Audio(); 337 338 // Mark this Audio object as unlocked to ensure it can get returned 339 // to the unlocked pool when released. 340 audioNode._unlocked = true; 341 342 // Add the audio node to the pool. 343 self._releaseHtml5Audio(audioNode); 344 } catch (e) { 345 self.noAudio = true; 346 break; 347 } 348 } 349 350 // Loop through any assigned audio nodes and unlock them. 351 for (var i=0; i<self._howls.length; i++) { 352 if (!self._howls[i]._webAudio) { 353 // Get all of the sounds in this Howl group. 354 var ids = self._howls[i]._getSoundIds(); 355 356 // Loop through all sounds and unlock the audio nodes. 357 for (var j=0; j<ids.length; j++) { 358 var sound = self._howls[i]._soundById(ids[j]); 359 360 if (sound && sound._node && !sound._node._unlocked) { 361 sound._node._unlocked = true; 362 sound._node.load(); 363 } 364 } 365 } 366 } 367 368 // Fix Android can not play in suspend state. 369 self._autoResume(); 370 371 // Create an empty buffer. 372 var source = self.ctx.createBufferSource(); 373 source.buffer = self._scratchBuffer; 374 source.connect(self.ctx.destination); 375 376 // Play the empty buffer. 377 if (typeof source.start === 'undefined') { 378 source.noteOn(0); 379 } else { 380 source.start(0); 381 } 382 383 // Calling resume() on a stack initiated by user gesture is what actually unlocks the audio on Android Chrome >= 55. 384 if (typeof self.ctx.resume === 'function') { 385 self.ctx.resume(); 386 } 387 388 // Setup a timeout to check that we are unlocked on the next event loop. 389 source.onended = function() { 390 source.disconnect(0); 391 392 // Update the unlocked state and prevent this check from happening again. 393 self._audioUnlocked = true; 394 395 // Remove the touch start listener. 396 document.removeEventListener('touchstart', unlock, true); 397 document.removeEventListener('touchend', unlock, true); 398 document.removeEventListener('click', unlock, true); 399 document.removeEventListener('keydown', unlock, true); 400 401 // Let all sounds know that audio has been unlocked. 402 for (var i=0; i<self._howls.length; i++) { 403 self._howls[i]._emit('unlock'); 404 } 405 }; 406 }; 407 408 // Setup a touch start listener to attempt an unlock in. 409 document.addEventListener('touchstart', unlock, true); 410 document.addEventListener('touchend', unlock, true); 411 document.addEventListener('click', unlock, true); 412 document.addEventListener('keydown', unlock, true); 413 414 return self; 415 }, 416 417 /** 418 * Get an unlocked HTML5 Audio object from the pool. If none are left, 419 * return a new Audio object and throw a warning. 420 * @return {Audio} HTML5 Audio object. 421 */ 422 _obtainHtml5Audio: function() { 423 var self = this || Howler; 424 425 // Return the next object from the pool if one exists. 426 if (self._html5AudioPool.length) { 427 return self._html5AudioPool.pop(); 428 } 429 430 //.Check if the audio is locked and throw a warning. 431 var testPlay = new Audio().play(); 432 if (testPlay && typeof Promise !== 'undefined' && (testPlay instanceof Promise || typeof testPlay.then === 'function')) { 433 testPlay.catch(function() { 434 console.warn('HTML5 Audio pool exhausted, returning potentially locked audio object.'); 435 }); 436 } 437 438 return new Audio(); 439 }, 440 441 /** 442 * Return an activated HTML5 Audio object to the pool. 443 * @return {Howler} 444 */ 445 _releaseHtml5Audio: function(audio) { 446 var self = this || Howler; 447 448 // Don't add audio to the pool if we don't know if it has been unlocked. 449 if (audio._unlocked) { 450 self._html5AudioPool.push(audio); 451 } 452 453 return self; 454 }, 455 456 /** 457 * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. 458 * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. 459 * @return {Howler} 460 */ 461 _autoSuspend: function() { 462 var self = this; 463 464 if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) { 465 return; 466 } 467 468 // Check if any sounds are playing. 469 for (var i=0; i<self._howls.length; i++) { 470 if (self._howls[i]._webAudio) { 471 for (var j=0; j<self._howls[i]._sounds.length; j++) { 472 if (!self._howls[i]._sounds[j]._paused) { 473 return self; 474 } 475 } 476 } 477 } 478 479 if (self._suspendTimer) { 480 clearTimeout(self._suspendTimer); 481 } 482 483 // If no sound has played after 30 seconds, suspend the context. 484 self._suspendTimer = setTimeout(function() { 485 if (!self.autoSuspend) { 486 return; 487 } 488 489 self._suspendTimer = null; 490 self.state = 'suspending'; 491 492 // Handle updating the state of the audio context after suspending. 493 var handleSuspension = function() { 494 self.state = 'suspended'; 495 496 if (self._resumeAfterSuspend) { 497 delete self._resumeAfterSuspend; 498 self._autoResume(); 499 } 500 }; 501 502 // Either the state gets suspended or it is interrupted. 503 // Either way, we need to update the state to suspended. 504 self.ctx.suspend().then(handleSuspension, handleSuspension); 505 }, 30000); 506 507 return self; 508 }, 509 510 /** 511 * Automatically resume the Web Audio AudioContext when a new sound is played. 512 * @return {Howler} 513 */ 514 _autoResume: function() { 515 var self = this; 516 517 if (!self.ctx || typeof self.ctx.resume === 'undefined' || !Howler.usingWebAudio) { 518 return; 519 } 520 521 if (self.state === 'running' && self.ctx.state !== 'interrupted' && self._suspendTimer) { 522 clearTimeout(self._suspendTimer); 523 self._suspendTimer = null; 524 } else if (self.state === 'suspended' || self.state === 'running' && self.ctx.state === 'interrupted') { 525 self.ctx.resume().then(function() { 526 self.state = 'running'; 527 528 // Emit to all Howls that the audio has resumed. 529 for (var i=0; i<self._howls.length; i++) { 530 self._howls[i]._emit('resume'); 531 } 532 }); 533 534 if (self._suspendTimer) { 535 clearTimeout(self._suspendTimer); 536 self._suspendTimer = null; 537 } 538 } else if (self.state === 'suspending') { 539 self._resumeAfterSuspend = true; 540 } 541 542 return self; 543 } 544 }; 545 546 // Setup the global audio controller. 547 var Howler = new HowlerGlobal(); 548 549 /** Group Methods **/ 550 /***************************************************************************/ 551 552 /** 553 * Create an audio group controller. 554 * @param {Object} o Passed in properties for this group. 555 */ 556 var Howl = function(o) { 557 var self = this; 558 559 // Throw an error if no source is provided. 560 if (!o.src || o.src.length === 0) { 561 console.error('An array of source files must be passed with any new Howl.'); 562 return; 563 } 564 565 self.init(o); 566 }; 567 Howl.prototype = { 568 /** 569 * Initialize a new Howl group object. 570 * @param {Object} o Passed in properties for this group. 571 * @return {Howl} 572 */ 573 init: function(o) { 574 var self = this; 575 576 // If we don't have an AudioContext created yet, run the setup. 577 if (!Howler.ctx) { 578 setupAudioContext(); 579 } 580 581 // Setup user-defined default properties. 582 self._autoplay = o.autoplay || false; 583 self._format = (typeof o.format !== 'string') ? o.format : [o.format]; 584 self._html5 = o.html5 || false; 585 self._muted = o.mute || false; 586 self._loop = o.loop || false; 587 self._pool = o.pool || 5; 588 self._preload = (typeof o.preload === 'boolean' || o.preload === 'metadata') ? o.preload : true; 589 self._rate = o.rate || 1; 590 self._sprite = o.sprite || {}; 591 self._src = (typeof o.src !== 'string') ? o.src : [o.src]; 592 self._volume = o.volume !== undefined ? o.volume : 1; 593 self._xhr = { 594 method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', 595 headers: o.xhr && o.xhr.headers ? o.xhr.headers : null, 596 withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false, 597 }; 598 599 // Setup all other default properties. 600 self._duration = 0; 601 self._state = 'unloaded'; 602 self._sounds = []; 603 self._endTimers = {}; 604 self._queue = []; 605 self._playLock = false; 606 607 // Setup event listeners. 608 self._onend = o.onend ? [{fn: o.onend}] : []; 609 self._onfade = o.onfade ? [{fn: o.onfade}] : []; 610 self._onload = o.onload ? [{fn: o.onload}] : []; 611 self._onloaderror = o.onloaderror ? [{fn: o.onloaderror}] : []; 612 self._onplayerror = o.onplayerror ? [{fn: o.onplayerror}] : []; 613 self._onpause = o.onpause ? [{fn: o.onpause}] : []; 614 self._onplay = o.onplay ? [{fn: o.onplay}] : []; 615 self._onstop = o.onstop ? [{fn: o.onstop}] : []; 616 self._onmute = o.onmute ? [{fn: o.onmute}] : []; 617 self._onvolume = o.onvolume ? [{fn: o.onvolume}] : []; 618 self._onrate = o.onrate ? [{fn: o.onrate}] : []; 619 self._onseek = o.onseek ? [{fn: o.onseek}] : []; 620 self._onunlock = o.onunlock ? [{fn: o.onunlock}] : []; 621 self._onresume = []; 622 623 // Web Audio or HTML5 Audio? 624 self._webAudio = Howler.usingWebAudio && !self._html5; 625 626 // Automatically try to enable audio. 627 if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { 628 Howler._unlockAudio(); 629 } 630 631 // Keep track of this Howl group in the global controller. 632 Howler._howls.push(self); 633 634 // If they selected autoplay, add a play event to the load queue. 635 if (self._autoplay) { 636 self._queue.push({ 637 event: 'play', 638 action: function() { 639 self.play(); 640 } 641 }); 642 } 643 644 // Load the source file unless otherwise specified. 645 if (self._preload && self._preload !== 'none') { 646 self.load(); 647 } 648 649 return self; 650 }, 651 652 /** 653 * Load the audio file. 654 * @return {Howler} 655 */ 656 load: function() { 657 var self = this; 658 var url = null; 659 660 // If no audio is available, quit immediately. 661 if (Howler.noAudio) { 662 self._emit('loaderror', null, 'No audio support.'); 663 return; 664 } 665 666 // Make sure our source is in an array. 667 if (typeof self._src === 'string') { 668 self._src = [self._src]; 669 } 670 671 // Loop through the sources and pick the first one that is compatible. 672 for (var i=0; i<self._src.length; i++) { 673 var ext, str; 674 675 if (self._format && self._format[i]) { 676 // If an extension was specified, use that instead. 677 ext = self._format[i]; 678 } else { 679 // Make sure the source is a string. 680 str = self._src[i]; 681 if (typeof str !== 'string') { 682 self._emit('loaderror', null, 'Non-string found in selected audio sources - ignoring.'); 683 continue; 684 } 685 686 // Extract the file extension from the URL or base64 data URI. 687 ext = /^data:audio\/([^;,]+);/i.exec(str); 688 if (!ext) { 689 ext = /\.([^.]+)$/.exec(str.split('?', 1)[0]); 690 } 691 692 if (ext) { 693 ext = ext[1].toLowerCase(); 694 } 695 } 696 697 // Log a warning if no extension was found. 698 if (!ext) { 699 console.warn('No file extension was found. Consider using the "format" property or specify an extension.'); 700 } 701 702 // Check if this extension is available. 703 if (ext && Howler.codecs(ext)) { 704 url = self._src[i]; 705 break; 706 } 707 } 708 709 if (!url) { 710 self._emit('loaderror', null, 'No codec support for selected audio sources.'); 711 return; 712 } 713 714 self._src = url; 715 self._state = 'loading'; 716 717 // If the hosting page is HTTPS and the source isn't, 718 // drop down to HTML5 Audio to avoid Mixed Content errors. 719 if (window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { 720 self._html5 = true; 721 self._webAudio = false; 722 } 723 724 // Create a new sound object and add it to the pool. 725 new Sound(self); 726 727 // Load and decode the audio data for playback. 728 if (self._webAudio) { 729 loadBuffer(self); 730 } 731 732 return self; 733 }, 734 735 /** 736 * Play a sound or resume previous playback. 737 * @param {String/Number} sprite Sprite name for sprite playback or sound id to continue previous. 738 * @param {Boolean} internal Internal Use: true prevents event firing. 739 * @return {Number} Sound ID. 740 */ 741 play: function(sprite, internal) { 742 var self = this; 743 var id = null; 744 745 // Determine if a sprite, sound id or nothing was passed 746 if (typeof sprite === 'number') { 747 id = sprite; 748 sprite = null; 749 } else if (typeof sprite === 'string' && self._state === 'loaded' && !self._sprite[sprite]) { 750 // If the passed sprite doesn't exist, do nothing. 751 return null; 752 } else if (typeof sprite === 'undefined') { 753 // Use the default sound sprite (plays the full audio length). 754 sprite = '__default'; 755 756 // Check if there is a single paused sound that isn't ended. 757 // If there is, play that sound. If not, continue as usual. 758 if (!self._playLock) { 759 var num = 0; 760 for (var i=0; i<self._sounds.length; i++) { 761 if (self._sounds[i]._paused && !self._sounds[i]._ended) { 762 num++; 763 id = self._sounds[i]._id; 764 } 765 } 766 767 if (num === 1) { 768 sprite = null; 769 } else { 770 id = null; 771 } 772 } 773 } 774 775 // Get the selected node, or get one from the pool. 776 var sound = id ? self._soundById(id) : self._inactiveSound(); 777 778 // If the sound doesn't exist, do nothing. 779 if (!sound) { 780 return null; 781 } 782 783 // Select the sprite definition. 784 if (id && !sprite) { 785 sprite = sound._sprite || '__default'; 786 } 787 788 // If the sound hasn't loaded, we must wait to get the audio's duration. 789 // We also need to wait to make sure we don't run into race conditions with 790 // the order of function calls. 791 if (self._state !== 'loaded') { 792 // Set the sprite value on this sound. 793 sound._sprite = sprite; 794 795 // Mark this sound as not ended in case another sound is played before this one loads. 796 sound._ended = false; 797 798 // Add the sound to the queue to be played on load. 799 var soundId = sound._id; 800 self._queue.push({ 801 event: 'play', 802 action: function() { 803 self.play(soundId); 804 } 805 }); 806 807 return soundId; 808 } 809 810 // Don't play the sound if an id was passed and it is already playing. 811 if (id && !sound._paused) { 812 // Trigger the play event, in order to keep iterating through queue. 813 if (!internal) { 814 self._loadQueue('play'); 815 } 816 817 return sound._id; 818 } 819 820 // Make sure the AudioContext isn't suspended, and resume it if it is. 821 if (self._webAudio) { 822 Howler._autoResume(); 823 } 824 825 // Determine how long to play for and where to start playing. 826 var seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite][0] / 1000); 827 var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); 828 var timeout = (duration * 1000) / Math.abs(sound._rate); 829 var start = self._sprite[sprite][0] / 1000; 830 var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; 831 sound._sprite = sprite; 832 833 // Mark the sound as ended instantly so that this async playback 834 // doesn't get grabbed by another call to play while this one waits to start. 835 sound._ended = false; 836 837 // Update the parameters of the sound. 838 var setParams = function() { 839 sound._paused = false; 840 sound._seek = seek; 841 sound._start = start; 842 sound._stop = stop; 843 sound._loop = !!(sound._loop || self._sprite[sprite][2]); 844 }; 845 846 // End the sound instantly if seek is at the end. 847 if (seek >= stop) { 848 self._ended(sound); 849 return; 850 } 851 852 // Begin the actual playback. 853 var node = sound._node; 854 if (self._webAudio) { 855 // Fire this when the sound is ready to play to begin Web Audio playback. 856 var playWebAudio = function() { 857 self._playLock = false; 858 setParams(); 859 self._refreshBuffer(sound); 860 861 // Setup the playback params. 862 var vol = (sound._muted || self._muted) ? 0 : sound._volume; 863 node.gain.setValueAtTime(vol, Howler.ctx.currentTime); 864 sound._playStart = Howler.ctx.currentTime; 865 866 // Play the sound using the supported method. 867 if (typeof node.bufferSource.start === 'undefined') { 868 sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); 869 } else { 870 sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); 871 } 872 873 // Start a new timer if none is present. 874 if (timeout !== Infinity) { 875 self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); 876 } 877 878 if (!internal) { 879 setTimeout(function() { 880 self._emit('play', sound._id); 881 self._loadQueue(); 882 }, 0); 883 } 884 }; 885 886 if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { 887 playWebAudio(); 888 } else { 889 self._playLock = true; 890 891 // Wait for the audio context to resume before playing. 892 self.once('resume', playWebAudio); 893 894 // Cancel the end timer. 895 self._clearTimer(sound._id); 896 } 897 } else { 898 // Fire this when the sound is ready to play to begin HTML5 Audio playback. 899 var playHtml5 = function() { 900 node.currentTime = seek; 901 node.muted = sound._muted || self._muted || Howler._muted || node.muted; 902 node.volume = sound._volume * Howler.volume(); 903 node.playbackRate = sound._rate; 904 905 // Some browsers will throw an error if this is called without user interaction. 906 try { 907 var play = node.play(); 908 909 // Support older browsers that don't support promises, and thus don't have this issue. 910 if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) { 911 // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). 912 self._playLock = true; 913 914 // Set param values immediately. 915 setParams(); 916 917 // Releases the lock and executes queued actions. 918 play 919 .then(function() { 920 self._playLock = false; 921 node._unlocked = true; 922 if (!internal) { 923 self._emit('play', sound._id); 924 } else { 925 self._loadQueue(); 926 } 927 }) 928 .catch(function() { 929 self._playLock = false; 930 self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + 931 'on mobile devices and Chrome where playback was not within a user interaction.'); 932 933 // Reset the ended and paused values. 934 sound._ended = true; 935 sound._paused = true; 936 }); 937 } else if (!internal) { 938 self._playLock = false; 939 setParams(); 940 self._emit('play', sound._id); 941 } 942 943 // Setting rate before playing won't work in IE, so we set it again here. 944 node.playbackRate = sound._rate; 945 946 // If the node is still paused, then we can assume there was a playback issue. 947 if (node.paused) { 948 self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + 949 'on mobile devices and Chrome where playback was not within a user interaction.'); 950 return; 951 } 952 953 // Setup the end timer on sprites or listen for the ended event. 954 if (sprite !== '__default' || sound._loop) { 955 self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); 956 } else { 957 self._endTimers[sound._id] = function() { 958 // Fire ended on this audio node. 959 self._ended(sound); 960 961 // Clear this listener. 962 node.removeEventListener('ended', self._endTimers[sound._id], false); 963 }; 964 node.addEventListener('ended', self._endTimers[sound._id], false); 965 } 966 } catch (err) { 967 self._emit('playerror', sound._id, err); 968 } 969 }; 970 971 // If this is streaming audio, make sure the src is set and load again. 972 if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { 973 node.src = self._src; 974 node.load(); 975 } 976 977 // Play immediately if ready, or wait for the 'canplaythrough'e vent. 978 var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); 979 if (node.readyState >= 3 || loadedNoReadyState) { 980 playHtml5(); 981 } else { 982 self._playLock = true; 983 self._state = 'loading'; 984 985 var listener = function() { 986 self._state = 'loaded'; 987 988 // Begin playback. 989 playHtml5(); 990 991 // Clear this listener. 992 node.removeEventListener(Howler._canPlayEvent, listener, false); 993 }; 994 node.addEventListener(Howler._canPlayEvent, listener, false); 995 996 // Cancel the end timer. 997 self._clearTimer(sound._id); 998 } 999 } 1000 1001 return sound._id; 1002 }, 1003 1004 /** 1005 * Pause playback and save current position. 1006 * @param {Number} id The sound ID (empty to pause all in group). 1007 * @return {Howl} 1008 */ 1009 pause: function(id) { 1010 var self = this; 1011 1012 // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. 1013 if (self._state !== 'loaded' || self._playLock) { 1014 self._queue.push({ 1015 event: 'pause', 1016 action: function() { 1017 self.pause(id); 1018 } 1019 }); 1020 1021 return self; 1022 } 1023 1024 // If no id is passed, get all ID's to be paused. 1025 var ids = self._getSoundIds(id); 1026 1027 for (var i=0; i<ids.length; i++) { 1028 // Clear the end timer. 1029 self._clearTimer(ids[i]); 1030 1031 // Get the sound. 1032 var sound = self._soundById(ids[i]); 1033 1034 if (sound && !sound._paused) { 1035 // Reset the seek position. 1036 sound._seek = self.seek(ids[i]); 1037 sound._rateSeek = 0; 1038 sound._paused = true; 1039 1040 // Stop currently running fades. 1041 self._stopFade(ids[i]); 1042 1043 if (sound._node) { 1044 if (self._webAudio) { 1045 // Make sure the sound has been created. 1046 if (!sound._node.bufferSource) { 1047 continue; 1048 } 1049 1050 if (typeof sound._node.bufferSource.stop === 'undefined') { 1051 sound._node.bufferSource.noteOff(0); 1052 } else { 1053 sound._node.bufferSource.stop(0); 1054 } 1055 1056 // Clean up the buffer source. 1057 self._cleanBuffer(sound._node); 1058 } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { 1059 sound._node.pause(); 1060 } 1061 } 1062 } 1063 1064 // Fire the pause event, unless `true` is passed as the 2nd argument. 1065 if (!arguments[1]) { 1066 self._emit('pause', sound ? sound._id : null); 1067 } 1068 } 1069 1070 return self; 1071 }, 1072 1073 /** 1074 * Stop playback and reset to start. 1075 * @param {Number} id The sound ID (empty to stop all in group). 1076 * @param {Boolean} internal Internal Use: true prevents event firing. 1077 * @return {Howl} 1078 */ 1079 stop: function(id, internal) { 1080 var self = this; 1081 1082 // If the sound hasn't loaded, add it to the load queue to stop when capable. 1083 if (self._state !== 'loaded' || self._playLock) { 1084 self._queue.push({ 1085 event: 'stop', 1086 action: function() { 1087 self.stop(id); 1088 } 1089 }); 1090 1091 return self; 1092 } 1093 1094 // If no id is passed, get all ID's to be stopped. 1095 var ids = self._getSoundIds(id); 1096 1097 for (var i=0; i<ids.length; i++) { 1098 // Clear the end timer. 1099 self._clearTimer(ids[i]); 1100 1101 // Get the sound. 1102 var sound = self._soundById(ids[i]); 1103 1104 if (sound) { 1105 // Reset the seek position. 1106 sound._seek = sound._start || 0; 1107 sound._rateSeek = 0; 1108 sound._paused = true; 1109 sound._ended = true; 1110 1111 // Stop currently running fades. 1112 self._stopFade(ids[i]); 1113 1114 if (sound._node) { 1115 if (self._webAudio) { 1116 // Make sure the sound's AudioBufferSourceNode has been created. 1117 if (sound._node.bufferSource) { 1118 if (typeof sound._node.bufferSource.stop === 'undefined') { 1119 sound._node.bufferSource.noteOff(0); 1120 } else { 1121 sound._node.bufferSource.stop(0); 1122 } 1123 1124 // Clean up the buffer source. 1125 self._cleanBuffer(sound._node); 1126 } 1127 } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { 1128 sound._node.currentTime = sound._start || 0; 1129 sound._node.pause(); 1130 1131 // If this is a live stream, stop download once the audio is stopped. 1132 if (sound._node.duration === Infinity) { 1133 self._clearSound(sound._node); 1134 } 1135 } 1136 } 1137 1138 if (!internal) { 1139 self._emit('stop', sound._id); 1140 } 1141 } 1142 } 1143 1144 return self; 1145 }, 1146 1147 /** 1148 * Mute/unmute a single sound or all sounds in this Howl group. 1149 * @param {Boolean} muted Set to true to mute and false to unmute. 1150 * @param {Number} id The sound ID to update (omit to mute/unmute all). 1151 * @return {Howl} 1152 */ 1153 mute: function(muted, id) { 1154 var self = this; 1155 1156 // If the sound hasn't loaded, add it to the load queue to mute when capable. 1157 if (self._state !== 'loaded'|| self._playLock) { 1158 self._queue.push({ 1159 event: 'mute', 1160 action: function() { 1161 self.mute(muted, id); 1162 } 1163 }); 1164 1165 return self; 1166 } 1167 1168 // If applying mute/unmute to all sounds, update the group's value. 1169 if (typeof id === 'undefined') { 1170 if (typeof muted === 'boolean') { 1171 self._muted = muted; 1172 } else { 1173 return self._muted; 1174 } 1175 } 1176 1177 // If no id is passed, get all ID's to be muted. 1178 var ids = self._getSoundIds(id); 1179 1180 for (var i=0; i<ids.length; i++) { 1181 // Get the sound. 1182 var sound = self._soundById(ids[i]); 1183 1184 if (sound) { 1185 sound._muted = muted; 1186 1187 // Cancel active fade and set the volume to the end value. 1188 if (sound._interval) { 1189 self._stopFade(sound._id); 1190 } 1191 1192 if (self._webAudio && sound._node) { 1193 sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, Howler.ctx.currentTime); 1194 } else if (sound._node) { 1195 sound._node.muted = Howler._muted ? true : muted; 1196 } 1197 1198 self._emit('mute', sound._id); 1199 } 1200 } 1201 1202 return self; 1203 }, 1204 1205 /** 1206 * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments. 1207 * volume() -> Returns the group's volume value. 1208 * volume(id) -> Returns the sound id's current volume. 1209 * volume(vol) -> Sets the volume of all sounds in this Howl group. 1210 * volume(vol, id) -> Sets the volume of passed sound id. 1211 * @return {Howl/Number} Returns self or current volume. 1212 */ 1213 volume: function() { 1214 var self = this; 1215 var args = arguments; 1216 var vol, id; 1217 1218 // Determine the values based on arguments. 1219 if (args.length === 0) { 1220 // Return the value of the groups' volume. 1221 return self._volume; 1222 } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { 1223 // First check if this is an ID, and if not, assume it is a new volume. 1224 var ids = self._getSoundIds(); 1225 var index = ids.indexOf(args[0]); 1226 if (index >= 0) { 1227 id = parseInt(args[0], 10); 1228 } else { 1229 vol = parseFloat(args[0]); 1230 } 1231 } else if (args.length >= 2) { 1232 vol = parseFloat(args[0]); 1233 id = parseInt(args[1], 10); 1234 } 1235 1236 // Update the volume or return the current volume. 1237 var sound; 1238 if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { 1239 // If the sound hasn't loaded, add it to the load queue to change volume when capable. 1240 if (self._state !== 'loaded'|| self._playLock) { 1241 self._queue.push({ 1242 event: 'volume', 1243 action: function() { 1244 self.volume.apply(self, args); 1245 } 1246 }); 1247 1248 return self; 1249 } 1250 1251 // Set the group volume. 1252 if (typeof id === 'undefined') { 1253 self._volume = vol; 1254 } 1255 1256 // Update one or all volumes. 1257 id = self._getSoundIds(id); 1258 for (var i=0; i<id.length; i++) { 1259 // Get the sound. 1260 sound = self._soundById(id[i]); 1261 1262 if (sound) { 1263 sound._volume = vol; 1264 1265 // Stop currently running fades. 1266 if (!args[2]) { 1267 self._stopFade(id[i]); 1268 } 1269 1270 if (self._webAudio && sound._node && !sound._muted) { 1271 sound._node.gain.setValueAtTime(vol, Howler.ctx.currentTime); 1272 } else if (sound._node && !sound._muted) { 1273 sound._node.volume = vol * Howler.volume(); 1274 } 1275 1276 self._emit('volume', sound._id); 1277 } 1278 } 1279 } else { 1280 sound = id ? self._soundById(id) : self._sounds[0]; 1281 return sound ? sound._volume : 0; 1282 } 1283 1284 return self; 1285 }, 1286 1287 /** 1288 * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). 1289 * @param {Number} from The value to fade from (0.0 to 1.0). 1290 * @param {Number} to The volume to fade to (0.0 to 1.0). 1291 * @param {Number} len Time in milliseconds to fade. 1292 * @param {Number} id The sound id (omit to fade all sounds). 1293 * @return {Howl} 1294 */ 1295 fade: function(from, to, len, id) { 1296 var self = this; 1297 1298 // If the sound hasn't loaded, add it to the load queue to fade when capable. 1299 if (self._state !== 'loaded' || self._playLock) { 1300 self._queue.push({ 1301 event: 'fade', 1302 action: function() { 1303 self.fade(from, to, len, id); 1304 } 1305 }); 1306 1307 return self; 1308 } 1309 1310 // Make sure the to/from/len values are numbers. 1311 from = Math.min(Math.max(0, parseFloat(from)), 1); 1312 to = Math.min(Math.max(0, parseFloat(to)), 1); 1313 len = parseFloat(len); 1314 1315 // Set the volume to the start position. 1316 self.volume(from, id); 1317 1318 // Fade the volume of one or all sounds. 1319 var ids = self._getSoundIds(id); 1320 for (var i=0; i<ids.length; i++) { 1321 // Get the sound. 1322 var sound = self._soundById(ids[i]); 1323 1324 // Create a linear fade or fall back to timeouts with HTML5 Audio. 1325 if (sound) { 1326 // Stop the previous fade if no sprite is being used (otherwise, volume handles this). 1327 if (!id) { 1328 self._stopFade(ids[i]); 1329 } 1330 1331 // If we are using Web Audio, let the native methods do the actual fade. 1332 if (self._webAudio && !sound._muted) { 1333 var currentTime = Howler.ctx.currentTime; 1334 var end = currentTime + (len / 1000); 1335 sound._volume = from; 1336 sound._node.gain.setValueAtTime(from, currentTime); 1337 sound._node.gain.linearRampToValueAtTime(to, end); 1338 } 1339 1340 self._startFadeInterval(sound, from, to, len, ids[i], typeof id === 'undefined'); 1341 } 1342 } 1343 1344 return self; 1345 }, 1346 1347 /** 1348 * Starts the internal interval to fade a sound. 1349 * @param {Object} sound Reference to sound to fade. 1350 * @param {Number} from The value to fade from (0.0 to 1.0). 1351 * @param {Number} to The volume to fade to (0.0 to 1.0). 1352 * @param {Number} len Time in milliseconds to fade. 1353 * @param {Number} id The sound id to fade. 1354 * @param {Boolean} isGroup If true, set the volume on the group. 1355 */ 1356 _startFadeInterval: function(sound, from, to, len, id, isGroup) { 1357 var self = this; 1358 var vol = from; 1359 var diff = to - from; 1360 var steps = Math.abs(diff / 0.01); 1361 var stepLen = Math.max(4, (steps > 0) ? len / steps : len); 1362 var lastTick = Date.now(); 1363 1364 // Store the value being faded to. 1365 sound._fadeTo = to; 1366 1367 // Update the volume value on each interval tick. 1368 sound._interval = setInterval(function() { 1369 // Update the volume based on the time since the last tick. 1370 var tick = (Date.now() - lastTick) / len; 1371 lastTick = Date.now(); 1372 vol += diff * tick; 1373 1374 // Round to within 2 decimal points. 1375 vol = Math.round(vol * 100) / 100; 1376 1377 // Make sure the volume is in the right bounds. 1378 if (diff < 0) { 1379 vol = Math.max(to, vol); 1380 } else { 1381 vol = Math.min(to, vol); 1382 } 1383 1384 // Change the volume. 1385 if (self._webAudio) { 1386 sound._volume = vol; 1387 } else { 1388 self.volume(vol, sound._id, true); 1389 } 1390 1391 // Set the group's volume. 1392 if (isGroup) { 1393 self._volume = vol; 1394 } 1395 1396 // When the fade is complete, stop it and fire event. 1397 if ((to < from && vol <= to) || (to > from && vol >= to)) { 1398 clearInterval(sound._interval); 1399 sound._interval = null; 1400 sound._fadeTo = null; 1401 self.volume(to, sound._id); 1402 self._emit('fade', sound._id); 1403 } 1404 }, stepLen); 1405 }, 1406 1407 /** 1408 * Internal method that stops the currently playing fade when 1409 * a new fade starts, volume is changed or the sound is stopped. 1410 * @param {Number} id The sound id. 1411 * @return {Howl} 1412 */ 1413 _stopFade: function(id) { 1414 var self = this; 1415 var sound = self._soundById(id); 1416 1417 if (sound && sound._interval) { 1418 if (self._webAudio) { 1419 sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); 1420 } 1421 1422 clearInterval(sound._interval); 1423 sound._interval = null; 1424 self.volume(sound._fadeTo, id); 1425 sound._fadeTo = null; 1426 self._emit('fade', id); 1427 } 1428 1429 return self; 1430 }, 1431 1432 /** 1433 * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. 1434 * loop() -> Returns the group's loop value. 1435 * loop(id) -> Returns the sound id's loop value. 1436 * loop(loop) -> Sets the loop value for all sounds in this Howl group. 1437 * loop(loop, id) -> Sets the loop value of passed sound id. 1438 * @return {Howl/Boolean} Returns self or current loop value. 1439 */ 1440 loop: function() { 1441 var self = this; 1442 var args = arguments; 1443 var loop, id, sound; 1444 1445 // Determine the values for loop and id. 1446 if (args.length === 0) { 1447 // Return the grou's loop value. 1448 return self._loop; 1449 } else if (args.length === 1) { 1450 if (typeof args[0] === 'boolean') { 1451 loop = args[0]; 1452 self._loop = loop; 1453 } else { 1454 // Return this sound's loop value. 1455 sound = self._soundById(parseInt(args[0], 10)); 1456 return sound ? sound._loop : false; 1457 } 1458 } else if (args.length === 2) { 1459 loop = args[0]; 1460 id = parseInt(args[1], 10); 1461 } 1462 1463 // If no id is passed, get all ID's to be looped. 1464 var ids = self._getSoundIds(id); 1465 for (var i=0; i<ids.length; i++) { 1466 sound = self._soundById(ids[i]); 1467 1468 if (sound) { 1469 sound._loop = loop; 1470 if (self._webAudio && sound._node && sound._node.bufferSource) { 1471 sound._node.bufferSource.loop = loop; 1472 if (loop) { 1473 sound._node.bufferSource.loopStart = sound._start || 0; 1474 sound._node.bufferSource.loopEnd = sound._stop; 1475 1476 // If playing, restart playback to ensure looping updates. 1477 if (self.playing(ids[i])) { 1478 self.pause(ids[i], true); 1479 self.play(ids[i], true); 1480 } 1481 } 1482 } 1483 } 1484 } 1485 1486 return self; 1487 }, 1488 1489 /** 1490 * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments. 1491 * rate() -> Returns the first sound node's current playback rate. 1492 * rate(id) -> Returns the sound id's current playback rate. 1493 * rate(rate) -> Sets the playback rate of all sounds in this Howl group. 1494 * rate(rate, id) -> Sets the playback rate of passed sound id. 1495 * @return {Howl/Number} Returns self or the current playback rate. 1496 */ 1497 rate: function() { 1498 var self = this; 1499 var args = arguments; 1500 var rate, id; 1501 1502 // Determine the values based on arguments. 1503 if (args.length === 0) { 1504 // We will simply return the current rate of the first node. 1505 id = self._sounds[0]._id; 1506 } else if (args.length === 1) { 1507 // First check if this is an ID, and if not, assume it is a new rate value. 1508 var ids = self._getSoundIds(); 1509 var index = ids.indexOf(args[0]); 1510 if (index >= 0) { 1511 id = parseInt(args[0], 10); 1512 } else { 1513 rate = parseFloat(args[0]); 1514 } 1515 } else if (args.length === 2) { 1516 rate = parseFloat(args[0]); 1517 id = parseInt(args[1], 10); 1518 } 1519 1520 // Update the playback rate or return the current value. 1521 var sound; 1522 if (typeof rate === 'number') { 1523 // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. 1524 if (self._state !== 'loaded' || self._playLock) { 1525 self._queue.push({ 1526 event: 'rate', 1527 action: function() { 1528 self.rate.apply(self, args); 1529 } 1530 }); 1531 1532 return self; 1533 } 1534 1535 // Set the group rate. 1536 if (typeof id === 'undefined') { 1537 self._rate = rate; 1538 } 1539 1540 // Update one or all volumes. 1541 id = self._getSoundIds(id); 1542 for (var i=0; i<id.length; i++) { 1543 // Get the sound. 1544 sound = self._soundById(id[i]); 1545 1546 if (sound) { 1547 // Keep track of our position when the rate changed and update the playback 1548 // start position so we can properly adjust the seek position for time elapsed. 1549 if (self.playing(id[i])) { 1550 sound._rateSeek = self.seek(id[i]); 1551 sound._playStart = self._webAudio ? Howler.ctx.currentTime : sound._playStart; 1552 } 1553 sound._rate = rate; 1554 1555 // Change the playback rate. 1556 if (self._webAudio && sound._node && sound._node.bufferSource) { 1557 sound._node.bufferSource.playbackRate.setValueAtTime(rate, Howler.ctx.currentTime); 1558 } else if (sound._node) { 1559 sound._node.playbackRate = rate; 1560 } 1561 1562 // Reset the timers. 1563 var seek = self.seek(id[i]); 1564 var duration = ((self._sprite[sound._sprite][0] + self._sprite[sound._sprite][1]) / 1000) - seek; 1565 var timeout = (duration * 1000) / Math.abs(sound._rate); 1566 1567 // Start a new end timer if sound is already playing. 1568 if (self._endTimers[id[i]] || !sound._paused) { 1569 self._clearTimer(id[i]); 1570 self._endTimers[id[i]] = setTimeout(self._ended.bind(self, sound), timeout); 1571 } 1572 1573 self._emit('rate', sound._id); 1574 } 1575 } 1576 } else { 1577 sound = self._soundById(id); 1578 return sound ? sound._rate : self._rate; 1579 } 1580 1581 return self; 1582 }, 1583 1584 /** 1585 * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments. 1586 * seek() -> Returns the first sound node's current seek position. 1587 * seek(id) -> Returns the sound id's current seek position. 1588 * seek(seek) -> Sets the seek position of the first sound node. 1589 * seek(seek, id) -> Sets the seek position of passed sound id. 1590 * @return {Howl/Number} Returns self or the current seek position. 1591 */ 1592 seek: function() { 1593 var self = this; 1594 var args = arguments; 1595 var seek, id; 1596 1597 // Determine the values based on arguments. 1598 if (args.length === 0) { 1599 // We will simply return the current position of the first node. 1600 if (self._sounds.length) { 1601 id = self._sounds[0]._id; 1602 } 1603 } else if (args.length === 1) { 1604 // First check if this is an ID, and if not, assume it is a new seek position. 1605 var ids = self._getSoundIds(); 1606 var index = ids.indexOf(args[0]); 1607 if (index >= 0) { 1608 id = parseInt(args[0], 10); 1609 } else if (self._sounds.length) { 1610 id = self._sounds[0]._id; 1611 seek = parseFloat(args[0]); 1612 } 1613 } else if (args.length === 2) { 1614 seek = parseFloat(args[0]); 1615 id = parseInt(args[1], 10); 1616 } 1617 1618 // If there is no ID, bail out. 1619 if (typeof id === 'undefined') { 1620 return 0; 1621 } 1622 1623 // If the sound hasn't loaded, add it to the load queue to seek when capable. 1624 if (typeof seek === 'number' && (self._state !== 'loaded' || self._playLock)) { 1625 self._queue.push({ 1626 event: 'seek', 1627 action: function() { 1628 self.seek.apply(self, args); 1629 } 1630 }); 1631 1632 return self; 1633 } 1634 1635 // Get the sound. 1636 var sound = self._soundById(id); 1637 1638 if (sound) { 1639 if (typeof seek === 'number' && seek >= 0) { 1640 // Pause the sound and update position for restarting playback. 1641 var playing = self.playing(id); 1642 if (playing) { 1643 self.pause(id, true); 1644 } 1645 1646 // Move the position of the track and cancel timer. 1647 sound._seek = seek; 1648 sound._ended = false; 1649 self._clearTimer(id); 1650 1651 // Update the seek position for HTML5 Audio. 1652 if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { 1653 sound._node.currentTime = seek; 1654 } 1655 1656 // Seek and emit when ready. 1657 var seekAndEmit = function() { 1658 // Restart the playback if the sound was playing. 1659 if (playing) { 1660 self.play(id, true); 1661 } 1662 1663 self._emit('seek', id); 1664 }; 1665 1666 // Wait for the play lock to be unset before emitting (HTML5 Audio). 1667 if (playing && !self._webAudio) { 1668 var emitSeek = function() { 1669 if (!self._playLock) { 1670 seekAndEmit(); 1671 } else { 1672 setTimeout(emitSeek, 0); 1673 } 1674 }; 1675 setTimeout(emitSeek, 0); 1676 } else { 1677 seekAndEmit(); 1678 } 1679 } else { 1680 if (self._webAudio) { 1681 var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; 1682 var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; 1683 return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); 1684 } else { 1685 return sound._node.currentTime; 1686 } 1687 } 1688 } 1689 1690 return self; 1691 }, 1692 1693 /** 1694 * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. 1695 * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. 1696 * @return {Boolean} True if playing and false if not. 1697 */ 1698 playing: function(id) { 1699 var self = this; 1700 1701 // Check the passed sound ID (if any). 1702 if (typeof id === 'number') { 1703 var sound = self._soundById(id); 1704 return sound ? !sound._paused : false; 1705 } 1706 1707 // Otherwise, loop through all sounds and check if any are playing. 1708 for (var i=0; i<self._sounds.length; i++) { 1709 if (!self._sounds[i]._paused) { 1710 return true; 1711 } 1712 } 1713 1714 return false; 1715 }, 1716 1717 /** 1718 * Get the duration of this sound. Passing a sound id will return the sprite duration. 1719 * @param {Number} id The sound id to check. If none is passed, return full source duration. 1720 * @return {Number} Audio duration in seconds. 1721 */ 1722 duration: function(id) { 1723 var self = this; 1724 var duration = self._duration; 1725 1726 // If we pass an ID, get the sound and return the sprite length. 1727 var sound = self._soundById(id); 1728 if (sound) { 1729 duration = self._sprite[sound._sprite][1] / 1000; 1730 } 1731 1732 return duration; 1733 }, 1734 1735 /** 1736 * Returns the current loaded state of this Howl. 1737 * @return {String} 'unloaded', 'loading', 'loaded' 1738 */ 1739 state: function() { 1740 return this._state; 1741 }, 1742 1743 /** 1744 * Unload and destroy the current Howl object. 1745 * This will immediately stop all sound instances attached to this group. 1746 */ 1747 unload: function() { 1748 var self = this; 1749 1750 // Stop playing any active sounds. 1751 var sounds = self._sounds; 1752 for (var i=0; i<sounds.length; i++) { 1753 // Stop the sound if it is currently playing. 1754 if (!sounds[i]._paused) { 1755 self.stop(sounds[i]._id); 1756 } 1757 1758 // Remove the source or disconnect. 1759 if (!self._webAudio) { 1760 // Set the source to 0-second silence to stop any downloading (except in IE). 1761 self._clearSound(sounds[i]._node); 1762 1763 // Remove any event listeners. 1764 sounds[i]._node.removeEventListener('error', sounds[i]._errorFn, false); 1765 sounds[i]._node.removeEventListener(Howler._canPlayEvent, sounds[i]._loadFn, false); 1766 sounds[i]._node.removeEventListener('ended', sounds[i]._endFn, false); 1767 1768 // Release the Audio object back to the pool. 1769 Howler._releaseHtml5Audio(sounds[i]._node); 1770 } 1771 1772 // Empty out all of the nodes. 1773 delete sounds[i]._node; 1774 1775 // Make sure all timers are cleared out. 1776 self._clearTimer(sounds[i]._id); 1777 } 1778 1779 // Remove the references in the global Howler object. 1780 var index = Howler._howls.indexOf(self); 1781 if (index >= 0) { 1782 Howler._howls.splice(index, 1); 1783 } 1784 1785 // Delete this sound from the cache (if no other Howl is using it). 1786 var remCache = true; 1787 for (i=0; i<Howler._howls.length; i++) { 1788 if (Howler._howls[i]._src === self._src || self._src.indexOf(Howler._howls[i]._src) >= 0) { 1789 remCache = false; 1790 break; 1791 } 1792 } 1793 1794 if (cache && remCache) { 1795 delete cache[self._src]; 1796 } 1797 1798 // Clear global errors. 1799 Howler.noAudio = false; 1800 1801 // Clear out `self`. 1802 self._state = 'unloaded'; 1803 self._sounds = []; 1804 self = null; 1805 1806 return null; 1807 }, 1808 1809 /** 1810 * Listen to a custom event. 1811 * @param {String} event Event name. 1812 * @param {Function} fn Listener to call. 1813 * @param {Number} id (optional) Only listen to events for this sound. 1814 * @param {Number} once (INTERNAL) Marks event to fire only once. 1815 * @return {Howl} 1816 */ 1817 on: function(event, fn, id, once) { 1818 var self = this; 1819 var events = self['_on' + event]; 1820 1821 if (typeof fn === 'function') { 1822 events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn}); 1823 } 1824 1825 return self; 1826 }, 1827 1828 /** 1829 * Remove a custom event. Call without parameters to remove all events. 1830 * @param {String} event Event name. 1831 * @param {Function} fn Listener to remove. Leave empty to remove all. 1832 * @param {Number} id (optional) Only remove events for this sound. 1833 * @return {Howl} 1834 */ 1835 off: function(event, fn, id) { 1836 var self = this; 1837 var events = self['_on' + event]; 1838 var i = 0; 1839 1840 // Allow passing just an event and ID. 1841 if (typeof fn === 'number') { 1842 id = fn; 1843 fn = null; 1844 } 1845 1846 if (fn || id) { 1847 // Loop through event store and remove the passed function. 1848 for (i=0; i<events.length; i++) { 1849 var isId = (id === events[i].id); 1850 if (fn === events[i].fn && isId || !fn && isId) { 1851 events.splice(i, 1); 1852 break; 1853 } 1854 } 1855 } else if (event) { 1856 // Clear out all events of this type. 1857 self['_on' + event] = []; 1858 } else { 1859 // Clear out all events of every type. 1860 var keys = Object.keys(self); 1861 for (i=0; i<keys.length; i++) { 1862 if ((keys[i].indexOf('_on') === 0) && Array.isArray(self[keys[i]])) { 1863 self[keys[i]] = []; 1864 } 1865 } 1866 } 1867 1868 return self; 1869 }, 1870 1871 /** 1872 * Listen to a custom event and remove it once fired. 1873 * @param {String} event Event name. 1874 * @param {Function} fn Listener to call. 1875 * @param {Number} id (optional) Only listen to events for this sound. 1876 * @return {Howl} 1877 */ 1878 once: function(event, fn, id) { 1879 var self = this; 1880 1881 // Setup the event listener. 1882 self.on(event, fn, id, 1); 1883 1884 return self; 1885 }, 1886 1887 /** 1888 * Emit all events of a specific type and pass the sound id. 1889 * @param {String} event Event name. 1890 * @param {Number} id Sound ID. 1891 * @param {Number} msg Message to go with event. 1892 * @return {Howl} 1893 */ 1894 _emit: function(event, id, msg) { 1895 var self = this; 1896 var events = self['_on' + event]; 1897 1898 // Loop through event store and fire all functions. 1899 for (var i=events.length-1; i>=0; i--) { 1900 // Only fire the listener if the correct ID is used. 1901 if (!events[i].id || events[i].id === id || event === 'load') { 1902 setTimeout(function(fn) { 1903 fn.call(this, id, msg); 1904 }.bind(self, events[i].fn), 0); 1905 1906 // If this event was setup with `once`, remove it. 1907 if (events[i].once) { 1908 self.off(event, events[i].fn, events[i].id); 1909 } 1910 } 1911 } 1912 1913 // Pass the event type into load queue so that it can continue stepping. 1914 self._loadQueue(event); 1915 1916 return self; 1917 }, 1918 1919 /** 1920 * Queue of actions initiated before the sound has loaded. 1921 * These will be called in sequence, with the next only firing 1922 * after the previous has finished executing (even if async like play). 1923 * @return {Howl} 1924 */ 1925 _loadQueue: function(event) { 1926 var self = this; 1927 1928 if (self._queue.length > 0) { 1929 var task = self._queue[0]; 1930 1931 // Remove this task if a matching event was passed. 1932 if (task.event === event) { 1933 self._queue.shift(); 1934 self._loadQueue(); 1935 } 1936 1937 // Run the task if no event type is passed. 1938 if (!event) { 1939 task.action(); 1940 } 1941 } 1942 1943 return self; 1944 }, 1945 1946 /** 1947 * Fired when playback ends at the end of the duration. 1948 * @param {Sound} sound The sound object to work with. 1949 * @return {Howl} 1950 */ 1951 _ended: function(sound) { 1952 var self = this; 1953 var sprite = sound._sprite; 1954 1955 // If we are using IE and there was network latency we may be clipping 1956 // audio before it completes playing. Lets check the node to make sure it 1957 // believes it has completed, before ending the playback. 1958 if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { 1959 setTimeout(self._ended.bind(self, sound), 100); 1960 return self; 1961 } 1962 1963 // Should this sound loop? 1964 var loop = !!(sound._loop || self._sprite[sprite][2]); 1965 1966 // Fire the ended event. 1967 self._emit('end', sound._id); 1968 1969 // Restart the playback for HTML5 Audio loop. 1970 if (!self._webAudio && loop) { 1971 self.stop(sound._id, true).play(sound._id); 1972 } 1973 1974 // Restart this timer if on a Web Audio loop. 1975 if (self._webAudio && loop) { 1976 self._emit('play', sound._id); 1977 sound._seek = sound._start || 0; 1978 sound._rateSeek = 0; 1979 sound._playStart = Howler.ctx.currentTime; 1980 1981 var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); 1982 self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); 1983 } 1984 1985 // Mark the node as paused. 1986 if (self._webAudio && !loop) { 1987 sound._paused = true; 1988 sound._ended = true; 1989 sound._seek = sound._start || 0; 1990 sound._rateSeek = 0; 1991 self._clearTimer(sound._id); 1992 1993 // Clean up the buffer source. 1994 self._cleanBuffer(sound._node); 1995 1996 // Attempt to auto-suspend AudioContext if no sounds are still playing. 1997 Howler._autoSuspend(); 1998 } 1999 2000 // When using a sprite, end the track. 2001 if (!self._webAudio && !loop) { 2002 self.stop(sound._id, true); 2003 } 2004 2005 return self; 2006 }, 2007 2008 /** 2009 * Clear the end timer for a sound playback. 2010 * @param {Number} id The sound ID. 2011 * @return {Howl} 2012 */ 2013 _clearTimer: function(id) { 2014 var self = this; 2015 2016 if (self._endTimers[id]) { 2017 // Clear the timeout or remove the ended listener. 2018 if (typeof self._endTimers[id] !== 'function') { 2019 clearTimeout(self._endTimers[id]); 2020 } else { 2021 var sound = self._soundById(id); 2022 if (sound && sound._node) { 2023 sound._node.removeEventListener('ended', self._endTimers[id], false); 2024 } 2025 } 2026 2027 delete self._endTimers[id]; 2028 } 2029 2030 return self; 2031 }, 2032 2033 /** 2034 * Return the sound identified by this ID, or return null. 2035 * @param {Number} id Sound ID 2036 * @return {Object} Sound object or null. 2037 */ 2038 _soundById: function(id) { 2039 var self = this; 2040 2041 // Loop through all sounds and find the one with this ID. 2042 for (var i=0; i<self._sounds.length; i++) { 2043 if (id === self._sounds[i]._id) { 2044 return self._sounds[i]; 2045 } 2046 } 2047 2048 return null; 2049 }, 2050 2051 /** 2052 * Return an inactive sound from the pool or create a new one. 2053 * @return {Sound} Sound playback object. 2054 */ 2055 _inactiveSound: function() { 2056 var self = this; 2057 2058 self._drain(); 2059 2060 // Find the first inactive node to recycle. 2061 for (var i=0; i<self._sounds.length; i++) { 2062 if (self._sounds[i]._ended) { 2063 return self._sounds[i].reset(); 2064 } 2065 } 2066 2067 // If no inactive node was found, create a new one. 2068 return new Sound(self); 2069 }, 2070 2071 /** 2072 * Drain excess inactive sounds from the pool. 2073 */ 2074 _drain: function() { 2075 var self = this; 2076 var limit = self._pool; 2077 var cnt = 0; 2078 var i = 0; 2079 2080 // If there are less sounds than the max pool size, we are done. 2081 if (self._sounds.length < limit) { 2082 return; 2083 } 2084 2085 // Count the number of inactive sounds. 2086 for (i=0; i<self._sounds.length; i++) { 2087 if (self._sounds[i]._ended) { 2088 cnt++; 2089 } 2090 } 2091 2092 // Remove excess inactive sounds, going in reverse order. 2093 for (i=self._sounds.length - 1; i>=0; i--) { 2094 if (cnt <= limit) { 2095 return; 2096 } 2097 2098 if (self._sounds[i]._ended) { 2099 // Disconnect the audio source when using Web Audio. 2100 if (self._webAudio && self._sounds[i]._node) { 2101 self._sounds[i]._node.disconnect(0); 2102 } 2103 2104 // Remove sounds until we have the pool size. 2105 self._sounds.splice(i, 1); 2106 cnt--; 2107 } 2108 } 2109 }, 2110 2111 /** 2112 * Get all ID's from the sounds pool. 2113 * @param {Number} id Only return one ID if one is passed. 2114 * @return {Array} Array of IDs. 2115 */ 2116 _getSoundIds: function(id) { 2117 var self = this; 2118 2119 if (typeof id === 'undefined') { 2120 var ids = []; 2121 for (var i=0; i<self._sounds.length; i++) { 2122 ids.push(self._sounds[i]._id); 2123 } 2124 2125 return ids; 2126 } else { 2127 return [id]; 2128 } 2129 }, 2130 2131 /** 2132 * Load the sound back into the buffer source. 2133 * @param {Sound} sound The sound object to work with. 2134 * @return {Howl} 2135 */ 2136 _refreshBuffer: function(sound) { 2137 var self = this; 2138 2139 // Setup the buffer source for playback. 2140 sound._node.bufferSource = Howler.ctx.createBufferSource(); 2141 sound._node.bufferSource.buffer = cache[self._src]; 2142 2143 // Connect to the correct node. 2144 if (sound._panner) { 2145 sound._node.bufferSource.connect(sound._panner); 2146 } else { 2147 sound._node.bufferSource.connect(sound._node); 2148 } 2149 2150 // Setup looping and playback rate. 2151 sound._node.bufferSource.loop = sound._loop; 2152 if (sound._loop) { 2153 sound._node.bufferSource.loopStart = sound._start || 0; 2154 sound._node.bufferSource.loopEnd = sound._stop || 0; 2155 } 2156 sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, Howler.ctx.currentTime); 2157 2158 return self; 2159 }, 2160 2161 /** 2162 * Prevent memory leaks by cleaning up the buffer source after playback. 2163 * @param {Object} node Sound's audio node containing the buffer source. 2164 * @return {Howl} 2165 */ 2166 _cleanBuffer: function(node) { 2167 var self = this; 2168 var isIOS = Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; 2169 2170 if (!node.bufferSource) { 2171 return self; 2172 } 2173 2174 if (Howler._scratchBuffer && node.bufferSource) { 2175 node.bufferSource.onended = null; 2176 node.bufferSource.disconnect(0); 2177 if (isIOS) { 2178 try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} 2179 } 2180 } 2181 node.bufferSource = null; 2182 2183 return self; 2184 }, 2185 2186 /** 2187 * Set the source to a 0-second silence to stop any downloading (except in IE). 2188 * @param {Object} node Audio node to clear. 2189 */ 2190 _clearSound: function(node) { 2191 var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); 2192 if (!checkIE) { 2193 node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; 2194 } 2195 } 2196 }; 2197 2198 /** Single Sound Methods **/ 2199 /***************************************************************************/ 2200 2201 /** 2202 * Setup the sound object, which each node attached to a Howl group is contained in. 2203 * @param {Object} howl The Howl parent group. 2204 */ 2205 var Sound = function(howl) { 2206 this._parent = howl; 2207 this.init(); 2208 }; 2209 Sound.prototype = { 2210 /** 2211 * Initialize a new Sound object. 2212 * @return {Sound} 2213 */ 2214 init: function() { 2215 var self = this; 2216 var parent = self._parent; 2217 2218 // Setup the default parameters. 2219 self._muted = parent._muted; 2220 self._loop = parent._loop; 2221 self._volume = parent._volume; 2222 self._rate = parent._rate; 2223 self._seek = 0; 2224 self._paused = true; 2225 self._ended = true; 2226 self._sprite = '__default'; 2227 2228 // Generate a unique ID for this sound. 2229 self._id = ++Howler._counter; 2230 2231 // Add itself to the parent's pool. 2232 parent._sounds.push(self); 2233 2234 // Create the new node. 2235 self.create(); 2236 2237 return self; 2238 }, 2239 2240 /** 2241 * Create and setup a new sound object, whether HTML5 Audio or Web Audio. 2242 * @return {Sound} 2243 */ 2244 create: function() { 2245 var self = this; 2246 var parent = self._parent; 2247 var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; 2248 2249 if (parent._webAudio) { 2250 // Create the gain node for controlling volume (the source will connect to this). 2251 self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); 2252 self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); 2253 self._node.paused = true; 2254 self._node.connect(Howler.masterGain); 2255 } else if (!Howler.noAudio) { 2256 // Get an unlocked Audio object from the pool. 2257 self._node = Howler._obtainHtml5Audio(); 2258 2259 // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). 2260 self._errorFn = self._errorListener.bind(self); 2261 self._node.addEventListener('error', self._errorFn, false); 2262 2263 // Listen for 'canplaythrough' event to let us know the sound is ready. 2264 self._loadFn = self._loadListener.bind(self); 2265 self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); 2266 2267 // Listen for the 'ended' event on the sound to account for edge-case where 2268 // a finite sound has a duration of Infinity. 2269 self._endFn = self._endListener.bind(self); 2270 self._node.addEventListener('ended', self._endFn, false); 2271 2272 // Setup the new audio node. 2273 self._node.src = parent._src; 2274 self._node.preload = parent._preload === true ? 'auto' : parent._preload; 2275 self._node.volume = volume * Howler.volume(); 2276 2277 // Begin loading the source. 2278 self._node.load(); 2279 } 2280 2281 return self; 2282 }, 2283 2284 /** 2285 * Reset the parameters of this sound to the original state (for recycle). 2286 * @return {Sound} 2287 */ 2288 reset: function() { 2289 var self = this; 2290 var parent = self._parent; 2291 2292 // Reset all of the parameters of this sound. 2293 self._muted = parent._muted; 2294 self._loop = parent._loop; 2295 self._volume = parent._volume; 2296 self._rate = parent._rate; 2297 self._seek = 0; 2298 self._rateSeek = 0; 2299 self._paused = true; 2300 self._ended = true; 2301 self._sprite = '__default'; 2302 2303 // Generate a new ID so that it isn't confused with the previous sound. 2304 self._id = ++Howler._counter; 2305 2306 return self; 2307 }, 2308 2309 /** 2310 * HTML5 Audio error listener callback. 2311 */ 2312 _errorListener: function() { 2313 var self = this; 2314 2315 // Fire an error event and pass back the code. 2316 self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); 2317 2318 // Clear the event listener. 2319 self._node.removeEventListener('error', self._errorFn, false); 2320 }, 2321 2322 /** 2323 * HTML5 Audio canplaythrough listener callback. 2324 */ 2325 _loadListener: function() { 2326 var self = this; 2327 var parent = self._parent; 2328 2329 // Round up the duration to account for the lower precision in HTML5 Audio. 2330 parent._duration = Math.ceil(self._node.duration * 10) / 10; 2331 2332 // Setup a sprite if none is defined. 2333 if (Object.keys(parent._sprite).length === 0) { 2334 parent._sprite = {__default: [0, parent._duration * 1000]}; 2335 } 2336 2337 if (parent._state !== 'loaded') { 2338 parent._state = 'loaded'; 2339 parent._emit('load'); 2340 parent._loadQueue(); 2341 } 2342 2343 // Clear the event listener. 2344 self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); 2345 }, 2346 2347 /** 2348 * HTML5 Audio ended listener callback. 2349 */ 2350 _endListener: function() { 2351 var self = this; 2352 var parent = self._parent; 2353 2354 // Only handle the `ended`` event if the duration is Infinity. 2355 if (parent._duration === Infinity) { 2356 // Update the parent duration to match the real audio duration. 2357 // Round up the duration to account for the lower precision in HTML5 Audio. 2358 parent._duration = Math.ceil(self._node.duration * 10) / 10; 2359 2360 // Update the sprite that corresponds to the real duration. 2361 if (parent._sprite.__default[1] === Infinity) { 2362 parent._sprite.__default[1] = parent._duration * 1000; 2363 } 2364 2365 // Run the regular ended method. 2366 parent._ended(self); 2367 } 2368 2369 // Clear the event listener since the duration is now correct. 2370 self._node.removeEventListener('ended', self._endFn, false); 2371 } 2372 }; 2373 2374 /** Helper Methods **/ 2375 /***************************************************************************/ 2376 2377 var cache = {}; 2378 2379 /** 2380 * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). 2381 * @param {Howl} self 2382 */ 2383 var loadBuffer = function(self) { 2384 var url = self._src; 2385 2386 // Check if the buffer has already been cached and use it instead. 2387 if (cache[url]) { 2388 // Set the duration from the cache. 2389 self._duration = cache[url].duration; 2390 2391 // Load the sound into this Howl. 2392 loadSound(self); 2393 2394 return; 2395 } 2396 2397 if (/^data:[^;]+;base64,/.test(url)) { 2398 // Decode the base64 data URI without XHR, since some browsers don't support it. 2399 var data = atob(url.split(',')[1]); 2400 var dataView = new Uint8Array(data.length); 2401 for (var i=0; i<data.length; ++i) { 2402 dataView[i] = data.charCodeAt(i); 2403 } 2404 2405 decodeAudioData(dataView.buffer, self); 2406 } else { 2407 // Load the buffer from the URL. 2408 var xhr = new XMLHttpRequest(); 2409 xhr.open(self._xhr.method, url, true); 2410 xhr.withCredentials = self._xhr.withCredentials; 2411 xhr.responseType = 'arraybuffer'; 2412 2413 // Apply any custom headers to the request. 2414 if (self._xhr.headers) { 2415 Object.keys(self._xhr.headers).forEach(function(key) { 2416 xhr.setRequestHeader(key, self._xhr.headers[key]); 2417 }); 2418 } 2419 2420 xhr.onload = function() { 2421 // Make sure we get a successful response back. 2422 var code = (xhr.status + '')[0]; 2423 if (code !== '0' && code !== '2' && code !== '3') { 2424 self._emit('loaderror', null, 'Failed loading audio file with status: ' + xhr.status + '.'); 2425 return; 2426 } 2427 2428 decodeAudioData(xhr.response, self); 2429 }; 2430 xhr.onerror = function() { 2431 // If there is an error, switch to HTML5 Audio. 2432 if (self._webAudio) { 2433 self._html5 = true; 2434 self._webAudio = false; 2435 self._sounds = []; 2436 delete cache[url]; 2437 self.load(); 2438 } 2439 }; 2440 safeXhrSend(xhr); 2441 } 2442 }; 2443 2444 /** 2445 * Send the XHR request wrapped in a try/catch. 2446 * @param {Object} xhr XHR to send. 2447 */ 2448 var safeXhrSend = function(xhr) { 2449 try { 2450 xhr.send(); 2451 } catch (e) { 2452 xhr.onerror(); 2453 } 2454 }; 2455 2456 /** 2457 * Decode audio data from an array buffer. 2458 * @param {ArrayBuffer} arraybuffer The audio data. 2459 * @param {Howl} self 2460 */ 2461 var decodeAudioData = function(arraybuffer, self) { 2462 // Fire a load error if something broke. 2463 var error = function() { 2464 self._emit('loaderror', null, 'Decoding audio data failed.'); 2465 }; 2466 2467 // Load the sound on success. 2468 var success = function(buffer) { 2469 if (buffer && self._sounds.length > 0) { 2470 cache[self._src] = buffer; 2471 loadSound(self, buffer); 2472 } else { 2473 error(); 2474 } 2475 }; 2476 2477 // Decode the buffer into an audio source. 2478 if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { 2479 Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); 2480 } else { 2481 Howler.ctx.decodeAudioData(arraybuffer, success, error); 2482 } 2483 } 2484 2485 /** 2486 * Sound is now loaded, so finish setting everything up and fire the loaded event. 2487 * @param {Howl} self 2488 * @param {Object} buffer The decoded buffer sound source. 2489 */ 2490 var loadSound = function(self, buffer) { 2491 // Set the duration. 2492 if (buffer && !self._duration) { 2493 self._duration = buffer.duration; 2494 } 2495 2496 // Setup a sprite if none is defined. 2497 if (Object.keys(self._sprite).length === 0) { 2498 self._sprite = {__default: [0, self._duration * 1000]}; 2499 } 2500 2501 // Fire the loaded event. 2502 if (self._state !== 'loaded') { 2503 self._state = 'loaded'; 2504 self._emit('load'); 2505 self._loadQueue(); 2506 } 2507 }; 2508 2509 /** 2510 * Setup the audio context when available, or switch to HTML5 Audio mode. 2511 */ 2512 var setupAudioContext = function() { 2513 // If we have already detected that Web Audio isn't supported, don't run this step again. 2514 if (!Howler.usingWebAudio) { 2515 return; 2516 } 2517 2518 // Check if we are using Web Audio and setup the AudioContext if we are. 2519 try { 2520 if (typeof AudioContext !== 'undefined') { 2521 Howler.ctx = new AudioContext(); 2522 } else if (typeof webkitAudioContext !== 'undefined') { 2523 Howler.ctx = new webkitAudioContext(); 2524 } else { 2525 Howler.usingWebAudio = false; 2526 } 2527 } catch(e) { 2528 Howler.usingWebAudio = false; 2529 } 2530 2531 // If the audio context creation still failed, set using web audio to false. 2532 if (!Howler.ctx) { 2533 Howler.usingWebAudio = false; 2534 } 2535 2536 // Check if a webview is being used on iOS8 or earlier (rather than the browser). 2537 // If it is, disable Web Audio as it causes crashing. 2538 var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); 2539 var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); 2540 var version = appVersion ? parseInt(appVersion[1], 10) : null; 2541 if (iOS && version && version < 9) { 2542 var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); 2543 if (Howler._navigator && !safari) { 2544 Howler.usingWebAudio = false; 2545 } 2546 } 2547 2548 // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). 2549 if (Howler.usingWebAudio) { 2550 Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); 2551 Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); 2552 Howler.masterGain.connect(Howler.ctx.destination); 2553 } 2554 2555 // Re-run the setup on Howler. 2556 Howler._setup(); 2557 }; 2558 2559 // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. 2560 if (typeof define === 'function' && define.amd) { 2561 define([], function() { 2562 return { 2563 Howler: Howler, 2564 Howl: Howl 2565 }; 2566 }); 2567 } 2568 2569 // Add support for CommonJS libraries such as browserify. 2570 if (typeof exports !== 'undefined') { 2571 exports.Howler = Howler; 2572 exports.Howl = Howl; 2573 } 2574 2575 // Add to global in Node.js (for testing, etc). 2576 if (typeof global !== 'undefined') { 2577 global.HowlerGlobal = HowlerGlobal; 2578 global.Howler = Howler; 2579 global.Howl = Howl; 2580 global.Sound = Sound; 2581 } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. 2582 window.HowlerGlobal = HowlerGlobal; 2583 window.Howler = Howler; 2584 window.Howl = Howl; 2585 window.Sound = Sound; 2586 } 2587})(); 2588 2589 2590/*! 2591 * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. 2592 * 2593 * howler.js v2.2.4 2594 * howlerjs.com 2595 * 2596 * (c) 2013-2020, James Simpson of GoldFire Studios 2597 * goldfirestudios.com 2598 * 2599 * MIT License 2600 */ 2601 2602(function() { 2603 2604 'use strict'; 2605 2606 // Setup default properties. 2607 HowlerGlobal.prototype._pos = [0, 0, 0]; 2608 HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; 2609 2610 /** Global Methods **/ 2611 /***************************************************************************/ 2612 2613 /** 2614 * Helper method to update the stereo panning position of all current Howls. 2615 * Future Howls will not use this value unless explicitly set. 2616 * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. 2617 * @return {Howler/Number} Self or current stereo panning value. 2618 */ 2619 HowlerGlobal.prototype.stereo = function(pan) { 2620 var self = this; 2621 2622 // Stop right here if not using Web Audio. 2623 if (!self.ctx || !self.ctx.listener) { 2624 return self; 2625 } 2626 2627 // Loop through all Howls and update their stereo panning. 2628 for (var i=self._howls.length-1; i>=0; i--) { 2629 self._howls[i].stereo(pan); 2630 } 2631 2632 return self; 2633 }; 2634 2635 /** 2636 * Get/set the position of the listener in 3D cartesian space. Sounds using 2637 * 3D position will be relative to the listener's position. 2638 * @param {Number} x The x-position of the listener. 2639 * @param {Number} y The y-position of the listener. 2640 * @param {Number} z The z-position of the listener. 2641 * @return {Howler/Array} Self or current listener position. 2642 */ 2643 HowlerGlobal.prototype.pos = function(x, y, z) { 2644 var self = this; 2645 2646 // Stop right here if not using Web Audio. 2647 if (!self.ctx || !self.ctx.listener) { 2648 return self; 2649 } 2650 2651 // Set the defaults for optional 'y' & 'z'. 2652 y = (typeof y !== 'number') ? self._pos[1] : y; 2653 z = (typeof z !== 'number') ? self._pos[2] : z; 2654 2655 if (typeof x === 'number') { 2656 self._pos = [x, y, z]; 2657 2658 if (typeof self.ctx.listener.positionX !== 'undefined') { 2659 self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1); 2660 self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1); 2661 self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1); 2662 } else { 2663 self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); 2664 } 2665 } else { 2666 return self._pos; 2667 } 2668 2669 return self; 2670 }; 2671 2672 /** 2673 * Get/set the direction the listener is pointing in the 3D cartesian space. 2674 * A front and up vector must be provided. The front is the direction the 2675 * face of the listener is pointing, and up is the direction the top of the 2676 * listener is pointing. Thus, these values are expected to be at right angles 2677 * from each other. 2678 * @param {Number} x The x-orientation of the listener. 2679 * @param {Number} y The y-orientation of the listener. 2680 * @param {Number} z The z-orientation of the listener. 2681 * @param {Number} xUp The x-orientation of the top of the listener. 2682 * @param {Number} yUp The y-orientation of the top of the listener. 2683 * @param {Number} zUp The z-orientation of the top of the listener. 2684 * @return {Howler/Array} Returns self or the current orientation vectors. 2685 */ 2686 HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { 2687 var self = this; 2688 2689 // Stop right here if not using Web Audio. 2690 if (!self.ctx || !self.ctx.listener) { 2691 return self; 2692 } 2693 2694 // Set the defaults for optional 'y' & 'z'. 2695 var or = self._orientation; 2696 y = (typeof y !== 'number') ? or[1] : y; 2697 z = (typeof z !== 'number') ? or[2] : z; 2698 xUp = (typeof xUp !== 'number') ? or[3] : xUp; 2699 yUp = (typeof yUp !== 'number') ? or[4] : yUp; 2700 zUp = (typeof zUp !== 'number') ? or[5] : zUp; 2701 2702 if (typeof x === 'number') { 2703 self._orientation = [x, y, z, xUp, yUp, zUp]; 2704 2705 if (typeof self.ctx.listener.forwardX !== 'undefined') { 2706 self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); 2707 self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); 2708 self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); 2709 self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1); 2710 self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1); 2711 self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1); 2712 } else { 2713 self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); 2714 } 2715 } else { 2716 return or; 2717 } 2718 2719 return self; 2720 }; 2721 2722 /** Group Methods **/ 2723 /***************************************************************************/ 2724 2725 /** 2726 * Add new properties to the core init. 2727 * @param {Function} _super Core init method. 2728 * @return {Howl} 2729 */ 2730 Howl.prototype.init = (function(_super) { 2731 return function(o) { 2732 var self = this; 2733 2734 // Setup user-defined default properties. 2735 self._orientation = o.orientation || [1, 0, 0]; 2736 self._stereo = o.stereo || null; 2737 self._pos = o.pos || null; 2738 self._pannerAttr = { 2739 coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, 2740 coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, 2741 coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, 2742 distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', 2743 maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, 2744 panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', 2745 refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, 2746 rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 2747 }; 2748 2749 // Setup event listeners. 2750 self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; 2751 self._onpos = o.onpos ? [{fn: o.onpos}] : []; 2752 self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; 2753 2754 // Complete initilization with howler.js core's init function. 2755 return _super.call(this, o); 2756 }; 2757 })(Howl.prototype.init); 2758 2759 /** 2760 * Get/set the stereo panning of the audio source for this sound or all in the group. 2761 * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. 2762 * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. 2763 * @return {Howl/Number} Returns self or the current stereo panning value. 2764 */ 2765 Howl.prototype.stereo = function(pan, id) { 2766 var self = this; 2767 2768 // Stop right here if not using Web Audio. 2769 if (!self._webAudio) { 2770 return self; 2771 } 2772 2773 // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. 2774 if (self._state !== 'loaded') { 2775 self._queue.push({ 2776 event: 'stereo', 2777 action: function() { 2778 self.stereo(pan, id); 2779 } 2780 }); 2781 2782 return self; 2783 } 2784 2785 // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. 2786 var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; 2787 2788 // Setup the group's stereo panning if no ID is passed. 2789 if (typeof id === 'undefined') { 2790 // Return the group's stereo panning if no parameters are passed. 2791 if (typeof pan === 'number') { 2792 self._stereo = pan; 2793 self._pos = [pan, 0, 0]; 2794 } else { 2795 return self._stereo; 2796 } 2797 } 2798 2799 // Change the streo panning of one or all sounds in group. 2800 var ids = self._getSoundIds(id); 2801 for (var i=0; i<ids.length; i++) { 2802 // Get the sound. 2803 var sound = self._soundById(ids[i]); 2804 2805 if (sound) { 2806 if (typeof pan === 'number') { 2807 sound._stereo = pan; 2808 sound._pos = [pan, 0, 0]; 2809 2810 if (sound._node) { 2811 // If we are falling back, make sure the panningModel is equalpower. 2812 sound._pannerAttr.panningModel = 'equalpower'; 2813 2814 // Check if there is a panner setup and create a new one if not. 2815 if (!sound._panner || !sound._panner.pan) { 2816 setupPanner(sound, pannerType); 2817 } 2818 2819 if (pannerType === 'spatial') { 2820 if (typeof sound._panner.positionX !== 'undefined') { 2821 sound._panner.positionX.setValueAtTime(pan, Howler.ctx.currentTime); 2822 sound._panner.positionY.setValueAtTime(0, Howler.ctx.currentTime); 2823 sound._panner.positionZ.setValueAtTime(0, Howler.ctx.currentTime); 2824 } else { 2825 sound._panner.setPosition(pan, 0, 0); 2826 } 2827 } else { 2828 sound._panner.pan.setValueAtTime(pan, Howler.ctx.currentTime); 2829 } 2830 } 2831 2832 self._emit('stereo', sound._id); 2833 } else { 2834 return sound._stereo; 2835 } 2836 } 2837 } 2838 2839 return self; 2840 }; 2841 2842 /** 2843 * Get/set the 3D spatial position of the audio source for this sound or group relative to the global listener. 2844 * @param {Number} x The x-position of the audio source. 2845 * @param {Number} y The y-position of the audio source. 2846 * @param {Number} z The z-position of the audio source. 2847 * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. 2848 * @return {Howl/Array} Returns self or the current 3D spatial position: [x, y, z]. 2849 */ 2850 Howl.prototype.pos = function(x, y, z, id) { 2851 var self = this; 2852 2853 // Stop right here if not using Web Audio. 2854 if (!self._webAudio) { 2855 return self; 2856 } 2857 2858 // If the sound hasn't loaded, add it to the load queue to change position when capable. 2859 if (self._state !== 'loaded') { 2860 self._queue.push({ 2861 event: 'pos', 2862 action: function() { 2863 self.pos(x, y, z, id); 2864 } 2865 }); 2866 2867 return self; 2868 } 2869 2870 // Set the defaults for optional 'y' & 'z'. 2871 y = (typeof y !== 'number') ? 0 : y; 2872 z = (typeof z !== 'number') ? -0.5 : z; 2873 2874 // Setup the group's spatial position if no ID is passed. 2875 if (typeof id === 'undefined') { 2876 // Return the group's spatial position if no parameters are passed. 2877 if (typeof x === 'number') { 2878 self._pos = [x, y, z]; 2879 } else { 2880 return self._pos; 2881 } 2882 } 2883 2884 // Change the spatial position of one or all sounds in group. 2885 var ids = self._getSoundIds(id); 2886 for (var i=0; i<ids.length; i++) { 2887 // Get the sound. 2888 var sound = self._soundById(ids[i]); 2889 2890 if (sound) { 2891 if (typeof x === 'number') { 2892 sound._pos = [x, y, z]; 2893 2894 if (sound._node) { 2895 // Check if there is a panner setup and create a new one if not. 2896 if (!sound._panner || sound._panner.pan) { 2897 setupPanner(sound, 'spatial'); 2898 } 2899 2900 if (typeof sound._panner.positionX !== 'undefined') { 2901 sound._panner.positionX.setValueAtTime(x, Howler.ctx.currentTime); 2902 sound._panner.positionY.setValueAtTime(y, Howler.ctx.currentTime); 2903 sound._panner.positionZ.setValueAtTime(z, Howler.ctx.currentTime); 2904 } else { 2905 sound._panner.setPosition(x, y, z); 2906 } 2907 } 2908 2909 self._emit('pos', sound._id); 2910 } else { 2911 return sound._pos; 2912 } 2913 } 2914 } 2915 2916 return self; 2917 }; 2918 2919 /** 2920 * Get/set the direction the audio source is pointing in the 3D cartesian coordinate 2921 * space. Depending on how direction the sound is, based on the `cone` attributes, 2922 * a sound pointing away from the listener can be quiet or silent. 2923 * @param {Number} x The x-orientation of the source. 2924 * @param {Number} y The y-orientation of the source. 2925 * @param {Number} z The z-orientation of the source. 2926 * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. 2927 * @return {Howl/Array} Returns self or the current 3D spatial orientation: [x, y, z]. 2928 */ 2929 Howl.prototype.orientation = function(x, y, z, id) { 2930 var self = this; 2931 2932 // Stop right here if not using Web Audio. 2933 if (!self._webAudio) { 2934 return self; 2935 } 2936 2937 // If the sound hasn't loaded, add it to the load queue to change orientation when capable. 2938 if (self._state !== 'loaded') { 2939 self._queue.push({ 2940 event: 'orientation', 2941 action: function() { 2942 self.orientation(x, y, z, id); 2943 } 2944 }); 2945 2946 return self; 2947 } 2948 2949 // Set the defaults for optional 'y' & 'z'. 2950 y = (typeof y !== 'number') ? self._orientation[1] : y; 2951 z = (typeof z !== 'number') ? self._orientation[2] : z; 2952 2953 // Setup the group's spatial orientation if no ID is passed. 2954 if (typeof id === 'undefined') { 2955 // Return the group's spatial orientation if no parameters are passed. 2956 if (typeof x === 'number') { 2957 self._orientation = [x, y, z]; 2958 } else { 2959 return self._orientation; 2960 } 2961 } 2962 2963 // Change the spatial orientation of one or all sounds in group. 2964 var ids = self._getSoundIds(id); 2965 for (var i=0; i<ids.length; i++) { 2966 // Get the sound. 2967 var sound = self._soundById(ids[i]); 2968 2969 if (sound) { 2970 if (typeof x === 'number') { 2971 sound._orientation = [x, y, z]; 2972 2973 if (sound._node) { 2974 // Check if there is a panner setup and create a new one if not. 2975 if (!sound._panner) { 2976 // Make sure we have a position to setup the node with. 2977 if (!sound._pos) { 2978 sound._pos = self._pos || [0, 0, -0.5]; 2979 } 2980 2981 setupPanner(sound, 'spatial'); 2982 } 2983 2984 if (typeof sound._panner.orientationX !== 'undefined') { 2985 sound._panner.orientationX.setValueAtTime(x, Howler.ctx.currentTime); 2986 sound._panner.orientationY.setValueAtTime(y, Howler.ctx.currentTime); 2987 sound._panner.orientationZ.setValueAtTime(z, Howler.ctx.currentTime); 2988 } else { 2989 sound._panner.setOrientation(x, y, z); 2990 } 2991 } 2992 2993 self._emit('orientation', sound._id); 2994 } else { 2995 return sound._orientation; 2996 } 2997 } 2998 } 2999 3000 return self; 3001 }; 3002 3003 /** 3004 * Get/set the panner node's attributes for a sound or group of sounds. 3005 * This method can optionall take 0, 1 or 2 arguments. 3006 * pannerAttr() -> Returns the group's values. 3007 * pannerAttr(id) -> Returns the sound id's values. 3008 * pannerAttr(o) -> Set's the values of all sounds in this Howl group. 3009 * pannerAttr(o, id) -> Set's the values of passed sound id. 3010 * 3011 * Attributes: 3012 * coneInnerAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, 3013 * inside of which there will be no volume reduction. 3014 * coneOuterAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, 3015 * outside of which the volume will be reduced to a constant value of `coneOuterGain`. 3016 * coneOuterGain - (0 by default) A parameter for directional audio sources, this is the gain outside of the 3017 * `coneOuterAngle`. It is a linear value in the range `[0, 1]`. 3018 * distanceModel - ('inverse' by default) Determines algorithm used to reduce volume as audio moves away from 3019 * listener. Can be `linear`, `inverse` or `exponential. 3020 * maxDistance - (10000 by default) The maximum distance between source and listener, after which the volume 3021 * will not be reduced any further. 3022 * refDistance - (1 by default) A reference distance for reducing volume as source moves further from the listener. 3023 * This is simply a variable of the distance model and has a different effect depending on which model 3024 * is used and the scale of your coordinates. Generally, volume will be equal to 1 at this distance. 3025 * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. This is simply a 3026 * variable of the distance model and can be in the range of `[0, 1]` with `linear` and `[0, â]` 3027 * with `inverse` and `exponential`. 3028 * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. 3029 * Can be `HRTF` or `equalpower`. 3030 * 3031 * @return {Howl/Object} Returns self or current panner attributes. 3032 */ 3033 Howl.prototype.pannerAttr = function() { 3034 var self = this; 3035 var args = arguments; 3036 var o, id, sound; 3037 3038 // Stop right here if not using Web Audio. 3039 if (!self._webAudio) { 3040 return self; 3041 } 3042 3043 // Determine the values based on arguments. 3044 if (args.length === 0) { 3045 // Return the group's panner attribute values. 3046 return self._pannerAttr; 3047 } else if (args.length === 1) { 3048 if (typeof args[0] === 'object') { 3049 o = args[0]; 3050 3051 // Set the grou's panner attribute values. 3052 if (typeof id === 'undefined') { 3053 if (!o.pannerAttr) { 3054 o.pannerAttr = { 3055 coneInnerAngle: o.coneInnerAngle, 3056 coneOuterAngle: o.coneOuterAngle, 3057 coneOuterGain: o.coneOuterGain, 3058 distanceModel: o.distanceModel, 3059 maxDistance: o.maxDistance, 3060 refDistance: o.refDistance, 3061 rolloffFactor: o.rolloffFactor, 3062 panningModel: o.panningModel 3063 }; 3064 } 3065 3066 self._pannerAttr = { 3067 coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== 'undefined' ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle, 3068 coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== 'undefined' ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle, 3069 coneOuterGain: typeof o.pannerAttr.coneOuterGain !== 'undefined' ? o.pannerAttr.coneOuterGain : self._coneOuterGain, 3070 distanceModel: typeof o.pannerAttr.distanceModel !== 'undefined' ? o.pannerAttr.distanceModel : self._distanceModel, 3071 maxDistance: typeof o.pannerAttr.maxDistance !== 'undefined' ? o.pannerAttr.maxDistance : self._maxDistance, 3072 refDistance: typeof o.pannerAttr.refDistance !== 'undefined' ? o.pannerAttr.refDistance : self._refDistance, 3073 rolloffFactor: typeof o.pannerAttr.rolloffFactor !== 'undefined' ? o.pannerAttr.rolloffFactor : self._rolloffFactor, 3074 panningModel: typeof o.pannerAttr.panningModel !== 'undefined' ? o.pannerAttr.panningModel : self._panningModel 3075 }; 3076 } 3077 } else { 3078 // Return this sound's panner attribute values. 3079 sound = self._soundById(parseInt(args[0], 10)); 3080 return sound ? sound._pannerAttr : self._pannerAttr; 3081 } 3082 } else if (args.length === 2) { 3083 o = args[0]; 3084 id = parseInt(args[1], 10); 3085 } 3086 3087 // Update the values of the specified sounds. 3088 var ids = self._getSoundIds(id); 3089 for (var i=0; i<ids.length; i++) { 3090 sound = self._soundById(ids[i]); 3091 3092 if (sound) { 3093 // Merge the new values into the sound. 3094 var pa = sound._pannerAttr; 3095 pa = { 3096 coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : pa.coneInnerAngle, 3097 coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : pa.coneOuterAngle, 3098 coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : pa.coneOuterGain, 3099 distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : pa.distanceModel, 3100 maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : pa.maxDistance, 3101 refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : pa.refDistance, 3102 rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : pa.rolloffFactor, 3103 panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : pa.panningModel 3104 }; 3105 3106 // Create a new panner node if one doesn't already exist. 3107 var panner = sound._panner; 3108 if (!panner) { 3109 // Make sure we have a position to setup the node with. 3110 if (!sound._pos) { 3111 sound._pos = self._pos || [0, 0, -0.5]; 3112 } 3113 3114 // Create a new panner node. 3115 setupPanner(sound, 'spatial'); 3116 panner = sound._panner 3117 } 3118 3119 // Update the panner values or create a new panner if none exists. 3120 panner.coneInnerAngle = pa.coneInnerAngle; 3121 panner.coneOuterAngle = pa.coneOuterAngle; 3122 panner.coneOuterGain = pa.coneOuterGain; 3123 panner.distanceModel = pa.distanceModel; 3124 panner.maxDistance = pa.maxDistance; 3125 panner.refDistance = pa.refDistance; 3126 panner.rolloffFactor = pa.rolloffFactor; 3127 panner.panningModel = pa.panningModel; 3128 } 3129 } 3130 3131 return self; 3132 }; 3133 3134 /** Single Sound Methods **/ 3135 /***************************************************************************/ 3136 3137 /** 3138 * Add new properties to the core Sound init. 3139 * @param {Function} _super Core Sound init method. 3140 * @return {Sound} 3141 */ 3142 Sound.prototype.init = (function(_super) { 3143 return function() { 3144 var self = this; 3145 var parent = self._parent; 3146 3147 // Setup user-defined default properties. 3148 self._orientation = parent._orientation; 3149 self._stereo = parent._stereo; 3150 self._pos = parent._pos; 3151 self._pannerAttr = parent._pannerAttr; 3152 3153 // Complete initilization with howler.js core Sound's init function. 3154 _super.call(this); 3155 3156 // If a stereo or position was specified, set it up. 3157 if (self._stereo) { 3158 parent.stereo(self._stereo); 3159 } else if (self._pos) { 3160 parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id); 3161 } 3162 }; 3163 })(Sound.prototype.init); 3164 3165 /** 3166 * Override the Sound.reset method to clean up properties from the spatial plugin. 3167 * @param {Function} _super Sound reset method. 3168 * @return {Sound} 3169 */ 3170 Sound.prototype.reset = (function(_super) { 3171 return function() { 3172 var self = this; 3173 var parent = self._parent; 3174 3175 // Reset all spatial plugin properties on this sound. 3176 self._orientation = parent._orientation; 3177 self._stereo = parent._stereo; 3178 self._pos = parent._pos; 3179 self._pannerAttr = parent._pannerAttr; 3180 3181 // If a stereo or position was specified, set it up. 3182 if (self._stereo) { 3183 parent.stereo(self._stereo); 3184 } else if (self._pos) { 3185 parent.pos(self._pos[0], self._pos[1], self._pos[2], self._id); 3186 } else if (self._panner) { 3187 // Disconnect the panner. 3188 self._panner.disconnect(0); 3189 self._panner = undefined; 3190 parent._refreshBuffer(self); 3191 } 3192 3193 // Complete resetting of the sound. 3194 return _super.call(this); 3195 }; 3196 })(Sound.prototype.reset); 3197 3198 /** Helper Methods **/ 3199 /***************************************************************************/ 3200 3201 /** 3202 * Create a new panner node and save it on the sound. 3203 * @param {Sound} sound Specific sound to setup panning on. 3204 * @param {String} type Type of panner to create: 'stereo' or 'spatial'. 3205 */ 3206 var setupPanner = function(sound, type) { 3207 type = type || 'spatial'; 3208 3209 // Create the new panner node. 3210 if (type === 'spatial') { 3211 sound._panner = Howler.ctx.createPanner(); 3212 sound._panner.coneInnerAngle = sound._pannerAttr.coneInnerAngle; 3213 sound._panner.coneOuterAngle = sound._pannerAttr.coneOuterAngle; 3214 sound._panner.coneOuterGain = sound._pannerAttr.coneOuterGain; 3215 sound._panner.distanceModel = sound._pannerAttr.distanceModel; 3216 sound._panner.maxDistance = sound._pannerAttr.maxDistance; 3217 sound._panner.refDistance = sound._pannerAttr.refDistance; 3218 sound._panner.rolloffFactor = sound._pannerAttr.rolloffFactor; 3219 sound._panner.panningModel = sound._pannerAttr.panningModel; 3220 3221 if (typeof sound._panner.positionX !== 'undefined') { 3222 sound._panner.positionX.setValueAtTime(sound._pos[0], Howler.ctx.currentTime); 3223 sound._panner.positionY.setValueAtTime(sound._pos[1], Howler.ctx.currentTime); 3224 sound._panner.positionZ.setValueAtTime(sound._pos[2], Howler.ctx.currentTime); 3225 } else { 3226 sound._panner.setPosition(sound._pos[0], sound._pos[1], sound._pos[2]); 3227 } 3228 3229 if (typeof sound._panner.orientationX !== 'undefined') { 3230 sound._panner.orientationX.setValueAtTime(sound._orientation[0], Howler.ctx.currentTime); 3231 sound._panner.orientationY.setValueAtTime(sound._orientation[1], Howler.ctx.currentTime); 3232 sound._panner.orientationZ.setValueAtTime(sound._orientation[2], Howler.ctx.currentTime); 3233 } else { 3234 sound._panner.setOrientation(sound._orientation[0], sound._orientation[1], sound._orientation[2]); 3235 } 3236 } else { 3237 sound._panner = Howler.ctx.createStereoPanner(); 3238 sound._panner.pan.setValueAtTime(sound._stereo, Howler.ctx.currentTime); 3239 } 3240 3241 sound._panner.connect(sound._node); 3242 3243 // Update the connections. 3244 if (!sound._paused) { 3245 sound._parent.pause(sound._id, true).play(sound._id, true); 3246 } 3247 }; 3248})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.