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