PageSourceSearch

https://harleydikkinson.com/wp-content/themes/archee/assets/vendors/js/howler.js?ver=1.1.0

js harleydikkinson.com collected 2026-10-07 11:26:57 UTC 107,970 bytes, 3,248 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.