text: /*!
   *  howler.js v1.1.4
   *  howlerjs.com
   *
   *  (c) 2013, James Simpson of GoldFire Studios
   *  goldfirestudios.com
   *
   *  MIT License
   */
  
  (function() {
    // setup
    var cache = {};
  
    // setup the audio context
    var ctx = null,
      usingWebAudio = true,
      noAudio = false;
    if (typeof AudioContext !== 'undefined') {
      ctx = new AudioContext();
    } else if (typeof webkitAudioContext !== 'undefined') {
      ctx = new webkitAudioContext();
    } else if (typeof Audio !== 'undefined') {
      usingWebAudio = false;
    } else {
      usingWebAudio = false;
      noAudio = true;
    }
  
    // create a master gain node
    if (usingWebAudio) {
      var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain();
      masterGain.gain.value = 1;
      masterGain.connect(ctx.destination);
    }
  
    // create global controller
    var HowlerGlobal = function() {
      this._volume = 1;
      this._muted = false;
      this.usingWebAudio = usingWebAudio;
      this._howls = [];
    };
    HowlerGlobal.prototype = {
      /**
       * Get/set the global volume for all sounds.
       * @param  {Float} vol Volume from 0.0 to 1.0.
       * @return {Object/Float}     Returns self or current volume.
       */
      volume: function(vol) {
        var self = this;
  
        // make sure volume is a number
        vol = parseFloat(vol);
  
        if (vol && vol >= 0 && vol <= 1) {
          self._volume = vol;
  
          if (usingWebAudio) {
            masterGain.gain.value = vol;
          }
  
          // loop through cache and change volume of all nodes that are using HTML5 Audio
          for (var key in self._howls) {
            if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) {
              // loop through the audio nodes
              for (var i=0; i<self._howls[key]._audioNode.length; i++) {
                self._howls[key]._audioNode[i].volume = self._howls[key]._volume * self._volume;
              }
            }
          }
  
          return self;
        }
  
        // return the current global volume
        return (usingWebAudio) ? masterGain.gain.value : self._volume;
      },
  
      /**
       * Mute all sounds.
       * @return {Object}
       */
      mute: function() {
        var self = this;
  
        self._muted = true;
  
        if (usingWebAudio) {
          masterGain.gain.value = 0;
        }
  
        for (var key in self._howls) {
          if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) {
            // loop through the audio nodes
            for (var i=0; i<self._howls[key]._audioNode.length; i++) {
              self._howls[key]._audioNode[i].muted = true;
            }
          }
        }
  
        return self;
      },
  
      /**
       * Unmute all sounds.
       * @return {Object}
       */
      unmute: function() {
        var self = this;
  
        self._muted = false;
  
        if (usingWebAudio) {
          masterGain.gain.value = self._volume;
        }
  
        for (var key in self._howls) {
          if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) {
            // loop through the audio nodes
            for (var i=0; i<self._howls[key]._audioNode.length; i++) {
              //self._howls[key]._audioNode[i].volume = self._howls[key]._volume * self._volume;
              self._howls[key]._audioNode[i].muted = false;
            }
          }
        }
  
        return self;
      }
    };
  
    // allow access to the global audio controls
    var Howler = new HowlerGlobal();
  
    // check for browser codec support
    var audioTest = null;
    if (!noAudio) {
      audioTest = new Audio();
      var codecs = {
        mp3: !!audioTest.canPlayType('audio/mpeg;').replace(/^no$/,''),
        ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,''),
        wav: !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/,''),
        m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/,''),
        webm: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,'')
      };
    }
  
    // setup the audio object
    var Howl = function(o) {
      var self = this;
  
      // setup the defaults
      self._autoplay = o.autoplay || false;
      self._buffer = o.buffer || false;
      self._duration = o.duration || 0;
      self._format = o.format || null;
      self._loop = o.loop || false;
      self._loaded = false;
      self._sprite = o.sprite || {};
      self._src = o.src || '';
      self._pos3d = o.pos3d || [0, 0, -0.5];
      self._volume = o.volume || 1;
      self._urls = o.urls || [];
  
      // setup event functions
      self._onload = [o.onload || function() {}];
      self._onloaderror = [o.onloaderror || function() {}];
      self._onend = [o.onend || function() {}];
      self._onpause = [o.onpause || function() {}];
      self._onplay = [o.onplay || function() {}];
  
      self._onendTimer = [];
  
      // Web Audio or HTML5 Audio?
      self._webAudio = usingWebAudio && !self._buffer;
  
      // check if we need to fall back to HTML5 Audio
      self._audioNode = [];
      if (self._webAudio) {
        self._setupAudioNode();
      }
  
      // add this to an array of Howl's to allow global control
      Howler._howls.push(self);
  
      // load the track
      self.load();
    };
  
    // setup all of the methods
    Howl.prototype = {
      /**
       * Load an audio file.
       * @return {Object}
       */
      load: function() {
        var self = this,
          url = null;
  
        // if no audio is available, quit immediately
        if (noAudio) {
          self.on('loaderror');
          return;
        }
  
        // loop through source URLs and pick the first one that is compatible
        for (var i=0; i<self._urls.length; i++) {
          var ext = self._urls[i].toLowerCase().match(/.+\.([^?]+)(\?|$)/),
            canPlay = false;
  
          // figure out the filetype (whether an extension or base64 data)
          ext = (ext && ext.length >= 2) ? ext[1] : self._urls[i].toLowerCase().match(/data\:audio\/([^?]+);/)[1];
  
          // set audio file format if specified
          if (self._format) {
            ext = self._format;
          }
  
          switch (ext) {
            case 'mp3':
              canPlay = codecs.mp3;
              break;
  
            case 'ogg':
              canPlay = codecs.ogg;
              break;
  
            case 'wav':
              canPlay = codecs.wav;
              break;
  
            case 'm4a':
              canPlay = codecs.m4a;
              break;
  
            case 'weba':
              canPlay = codecs.webm;
              break;
          }
  
          if (canPlay === true) {
            url = self._urls[i];
            break;
          }
        }
  
        if (!url) {
          self.on('loaderror');
          return;
        }
  
        self._src = url;
  
        if (self._webAudio) {
          loadBuffer(self, url);
        } else {
          var newNode = new Audio();
          self._audioNode.push(newNode);
  
          // setup the new audio node
          newNode.src = url;
          newNode._pos = 0;
          newNode.preload = 'auto';
          newNode.volume = (Howler._muted) ? 0 : self._volume * Howler.volume();
  
          // add this sound to the cache
          cache[url] = self;
  
          // setup the event listener to start playing the sound
          // as soon as it has buffered enough
          var listener = function() {
            self._duration = newNode.duration;
  
            // setup a sprite if none is defined
            if (Object.getOwnPropertyNames(self._sprite).length === 0) {
              self._sprite = {_default: [0, self._duration * 1000]};
            }
  
            if (!self._loaded) {
              self._loaded = true;
              self.on('load');
            }
  
            if (self._autoplay) {
              self.play();
            }
  
            // clear the event listener
            newNode.removeEventListener('canplaythrough', listener, false);
          };
          newNode.addEventListener('canplaythrough', listener, false);
          newNode.load();
        }
  
        return self;
      },
  
      /**
       * Get/set the URLs to be pulled from to play in this source.
       * @param  {Array} urls  Arry of URLs to load from
       * @return {Object}      Returns self or the current URLs
       */
      urls: function(urls) {
        var self = this;
  
        if (urls) {
          self._urls = urls;
          self._loaded = false;
          self.stop();
          self.load();
  
          return self;
        } else {
          return self._urls;
        }
      },
  
      /**
       * Play a sound from the current time (0 by default).
       * @param  {String}   sprite   (optional) Plays from the specified position in the sound sprite definition.
       * @param  {Function} callback (optional) Returns the unique playback id for this sound instance.
       * @return {Object}
       */
      play: function(sprite, callback) {
        var self = this;
  
        // use the default sprite if none is passed
        if (!sprite) {
          sprite = '_default';
        }
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('load', function() {
            self.play(sprite, callback);
          });
  
          return self;
        }
  
        // if the sprite doesn't exist, play nothing
        if (!self._sprite[sprite]) {
          if (typeof callback === 'function') callback();
          return self;
        }
  
        // get the node to playback
        self._inactiveNode(function(node) {
          // persist the sprite being played
          node._sprite = sprite;
  
          // determine where to start playing from
          var pos = (node._pos > 0) ? node._pos : self._sprite[sprite][0] / 1000,
            duration = self._sprite[sprite][1] / 1000 - node._pos;
  
          // determine if this sound should be looped
          var loop = !!(self._loop || self._sprite[sprite][2]);
  
          // set timer to fire the 'onend' event
          var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '',
            timerId;
          (function() {
            var data = {
              id: soundId,
              sprite: sprite,
              loop: loop
            };
            timerId = setTimeout(function() {
              // if looping, restart the track
              if (!self._webAudio && loop) {
                self.stop(data.id, data.timer).play(sprite, data.id);
              }
  
              // set web audio node to paused at end
              if (self._webAudio && !loop) {
                self._nodeById(data.id).paused = true;
              }
  
              // end the track if it is HTML audio and a sprite
              if (!self._webAudio && !loop) {
                self.stop(data.id, data.timer);
              }
  
              // fire ended event
              self.on('end');
            }, duration * 1000);
  
            // store the reference to the timer
            self._onendTimer.push(timerId);
  
            // remember which timer to cancel
            data.timer = self._onendTimer[self._onendTimer.length - 1];
          })();
  
          if (self._webAudio) {
            // set the play id to this node and load into context
            node.id = soundId;
            node.paused = false;
            refreshBuffer(self, [loop, pos, duration], soundId);
            self._playStart = ctx.currentTime;
            node.gain.value = self._volume;
  
            if (typeof node.bufferSource.start === 'undefined') {
              node.bufferSource.noteGrainOn(0, pos, duration);
            } else {
              node.bufferSource.start(0, pos, duration);
            }
          } else {
            if (node.readyState === 4) {
              node.id = soundId;
              node.currentTime = pos;
              node.muted = Howler._muted;
              node.volume = self._volume * Howler.volume();
              node.play();
            } else {
              self._clearEndTimer(timerId);
  
              (function(){
                var sound = self,
                  playSprite = sprite,
                  fn = callback,
                  newNode = node;
                var listener = function() {
                  sound.play(playSprite, fn);
  
                  // clear the event listener
                  newNode.removeEventListener('canplaythrough', listener, false);
                };
                newNode.addEventListener('canplaythrough', listener, false);
              })();
  
              return self;
            }
          }
  
          // fire the play event and send the soundId back in the callback
          self.on('play');
          if (typeof callback === 'function') callback(soundId);
  
          return self;
        });
  
        return self;
      },
  
      /**
       * Pause playback and save the current position.
       * @param {String} id (optional) The play instance ID.
       * @param {String} id (optional) Clear the correct timeout ID.
       * @return {Object}
       */
      pause: function(id, timerId) {
        var self = this;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.pause(id);
          });
  
          return self;
        }
  
        // clear 'onend' timer
        self._clearEndTimer(timerId || 0);
  
        var activeNode = (id) ? self._nodeById(id) : self._activeNode();
        if (activeNode) {
          if (self._webAudio) {
            // make sure the sound has been created
            if (!activeNode.bufferSource) {
              return self;
            }
  
            activeNode.paused = true;
            activeNode._pos += ctx.currentTime - self._playStart;
            if (typeof activeNode.bufferSource.stop === 'undefined') {
              activeNode.bufferSource.noteOff(0);
            } else {
              activeNode.bufferSource.stop(0);
            }
          } else {
            activeNode._pos = activeNode.currentTime;
            activeNode.pause();
          }
        }
  
        self.on('pause');
  
        return self;
      },
  
      /**
       * Stop playback and reset to start.
       * @param  {String} id  (optional) The play instance ID.
       * @param  {String} id  (optional) Clear the correct timeout ID.
       * @return {Object}
       */
      stop: function(id, timerId) {
        var self = this;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.stop(id);
          });
  
          return self;
        }
  
        // clear 'onend' timer
        self._clearEndTimer(timerId || 0);
  
        var activeNode = (id) ? self._nodeById(id) : self._activeNode();
        if (activeNode) {
          activeNode._pos = 0;
  
          if (self._webAudio) {
            // make sure the sound has been created
            if (!activeNode.bufferSource) {
              return self;
            }
  
            activeNode.paused = true;
  
            if (typeof activeNode.bufferSource.stop === 'undefined') {
              activeNode.bufferSource.noteOff(0);
            } else {
              activeNode.bufferSource.stop(0);
            }
          } else {
            activeNode.pause();
            activeNode.currentTime = 0;
          }
        }
  
        return self;
      },
  
      /**
       * Mute this sound.
       * @param  {String} id (optional) The play instance ID.
       * @return {Object}
       */
      mute: function(id) {
        var self = this;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.mute(id);
          });
  
          return self;
        }
  
        var activeNode = (id) ? self._nodeById(id) : self._activeNode();
        if (activeNode) {
          if (self._webAudio) {
            activeNode.gain.value = 0;
          } else {
            activeNode.volume = 0;
          }
        }
  
        return self;
      },
  
      /**
       * Unmute this sound.
       * @param  {String} id (optional) The play instance ID.
       * @return {Object}
       */
      unmute: function(id) {
        var self = this;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.unmute(id);
          });
  
          return self;
        }
  
        var activeNode = (id) ? self._nodeById(id) : self._activeNode();
        if (activeNode) {
          if (self._webAudio) {
            activeNode.gain.value = self._volume;
          } else {
            activeNode.volume = self._volume;
          }
        }
  
        return self;
      },
  
      /**
       * Get/set volume of this sound.
       * @param  {Float}  vol Volume from 0.0 to 1.0.
       * @param  {String} id  (optional) The play instance ID.
       * @return {Object/Float}     Returns self or current volume.
       */
      volume: function(vol, id) {
        var self = this;
  
        // make sure volume is a number
        vol = parseFloat(vol);
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.volume(vol, id);
          });
  
          return self;
        }
  
        if (vol >= 0 && vol <= 1) {
          self._volume = vol;
  
          var activeNode = (id) ? self._nodeById(id) : self._activeNode();
          if (activeNode) {
            if (self._webAudio) {
              activeNode.gain.value = vol;
            } else {
              activeNode.volume = vol * Howler.volume();
            }
          }
  
          return self;
        } else {
          return self._volume;
        }
      },
  
      /**
       * Get/set whether to loop the sound.
       * @param  {Boolean} loop To loop or not to loop, that is the question.
       * @return {Object/Boolean}      Returns self or current looping value.
       */
      loop: function(loop) {
        var self = this;
  
        if (typeof loop === 'boolean') {
          self._loop = loop;
  
          return self;
        } else {
          return self._loop;
        }
      },
  
      /**
       * Get/set sound sprite definition.
       * @param  {Object} sprite Example: {spriteName: [offset, duration, loop]}
       *                @param {Integer} offset   Where to begin playback in milliseconds
       *                @param {Integer} duration How long to play in milliseconds
       *                @param {Boolean} loop     (optional) Set true to loop this sprite
       * @return {Object}        Returns current sprite sheet or self.
       */
      sprite: function(sprite) {
        var self = this;
  
        if (typeof sprite === 'object') {
          self._sprite = sprite;
  
          return self;
        } else {
          return self._sprite;
        }
      },
  
      /**
       * Get/set the position of playback.
       * @param  {Float}  pos The position to move current playback to.
       * @param  {String} id  (optional) The play instance ID.
       * @return {Object/Float}      Returns self or current playback position.
       */
      pos: function(pos, id) {
        var self = this;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('load', function() {
            self.pos(pos);
          });
  
          return self;
        }
  
        var activeNode = (id) ? self._nodeById(id) : self._activeNode();
        if (activeNode) {
          if (self._webAudio) {
            if (pos >= 0) {
              activeNode._pos = pos;
              self.pause(id).play(activeNode._sprite, id);
  
              return self;
            } else {
              return activeNode._pos + (ctx.currentTime - self._playStart);
            }
          } else {
            if (pos >= 0) {
              activeNode.currentTime = pos;
  
              return self;
            } else {
              return activeNode.currentTime;
            }
          }
        }
      },
  
      /**
       * Get/set the 3D position of the audio source.
       * The most common usage is to set the 'x' position
       * to affect the left/right ear panning. Setting any value higher than
       * 1.0 will begin to decrease the volume of the sound as it moves further away.
       * NOTE: This only works with Web Audio API, HTML5 Audio playback
       * will not be affected.
       * @param  {Float}  x  The x-position of the playback from -1000.0 to 1000.0
       * @param  {Float}  y  The y-position of the playback from -1000.0 to 1000.0
       * @param  {Float}  z  The z-position of the playback from -1000.0 to 1000.0
       * @param  {String} id (optional) The play instance ID.
       * @return {Object/Array}   Returns self or the current 3D position: [x, y, z]
       */
      pos3d: function(x, y, z, id) {
        var self = this;
  
        // set a default for the optional 'y' & 'z'
        y = (typeof y === 'undefined' || !y) ? 0 : y;
        z = (typeof z === 'undefined' || !z) ? -0.5 : z;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.pos3d(x, y, z, id);
          });
  
          return self;
        }
  
        if (x >= 0 || x < 0) {
          if (self._webAudio) {
            var activeNode = (id) ? self._nodeById(id) : self._activeNode();
            if (activeNode) {
              self._pos3d = [x, y, z];
              activeNode.panner.setPosition(x, y, z);
            }
          }
        } else {
          return self._pos3d;
        }
  
        return self;
      },
  
      /**
       * Fade in the current sound.
       * @param  {Float}    to      Volume to fade to (0.0 to 1.0).
       * @param  {Number}   len     Time in milliseconds to fade.
       * @param  {Function} callback
       * @return {Object}
       */
      fadeIn: function(to, len, callback) {
        var self = this,
          dist = to,
          iterations = dist / 0.01,
          hold = len / iterations;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('load', function() {
            self.fadeIn(to, len, callback);
          });
  
          return self;
        }
  
        self.volume(0).play();
  
        for (var i=1; i<=iterations; i++) {
          (function() {
            var vol = Math.round(1000 * (self._volume + 0.01 * i)) / 1000,
              toVol = to;
            setTimeout(function() {
              self.volume(vol);
  
              if (vol === toVol) {
                if (callback) callback();
              }
            }, hold * i);
          })();
        }
  
        return self;
      },
  
      /**
       * Fade out the current sound and pause when finished.
       * @param  {Float}    to       Volume to fade to (0.0 to 1.0).
       * @param  {Number}   len      Time in milliseconds to fade.
       * @param  {Function} callback
       * @param  {String}   id       (optional) The play instance ID.
       * @return {Object}
       */
      fadeOut: function(to, len, callback, id) {
        var self = this,
          dist = self._volume - to,
          iterations = dist / 0.01,
          hold = len / iterations;
  
        // if the sound hasn't been loaded, add it to the event queue
        if (!self._loaded) {
          self.on('play', function() {
            self.fadeOut(to, len, callback, id);
          });
  
          return self;
        }
  
        for (var i=1; i<=iterations; i++) {
          (function() {
            var vol = Math.round(1000 * (self._volume - 0.01 * i)) / 1000,
              toVol = to;
            setTimeout(function() {
              self.volume(vol, id);
  
              if (vol === toVol) {
                if (callback) callback();
                self.pause(id);
  
                // fire ended event
                self.on('end');
              }
            }, hold * i);
          })();
        }
  
        return self;
      },
  
      /**
       * Get an audio node by ID.
       * @return {Object} Audio node.
       */
      _nodeById: function(id) {
        var self = this,
          node = self._audioNode[0];
  
        // find the node with this ID
        for (var i=0; i<self._audioNode.length; i++) {
          if (self._audioNode[i].id === id) {
            node = self._audioNode[i];
            break;
          }
        }
  
        return node;
      },
  
      /**
       * Get the first active audio node.
       * @return {Object} Audio node.
       */
      _activeNode: function() {
        var self = this,
          node = null;
  
        // find the first playing node
        for (var i=0; i<self._audioNode.length; i++) {
          if (!self._audioNode[i].paused) {
            node = self._audioNode[i];
            break;
          }
        }
  
        // remove excess inactive nodes
        self._drainPool();
  
        return node;
      },
  
      /**
       * Get the first inactive audio node.
       * If there is none, create a new one and add it to the pool.
       * @param  {Function} callback Function to call when the audio node is ready.
       */
      _inactiveNode: function(callback) {
        var self = this,
          node = null;
  
        // find first inactive node to recycle
        for (var i=0; i<self._audioNode.length; i++) {
          if (self._audioNode[i].paused && self._audioNode[i].readyState === 4) {
            callback(self._audioNode[i]);
            node = true;
            break;
          }
        }
  
        // remove excess inactive nodes
        self._drainPool();
  
        if (node) {
          return;
        }
  
        // create new node if there are no inactives
        var newNode;
        if (self._webAudio) {
          newNode = self._setupAudioNode();
          callback(newNode);
        } else {
          self.load();
          newNode = self._audioNode[self._audioNode.length - 1];
          newNode.addEventListener('loadedmetadata', function() {
            callback(newNode);
          });
        }
      },
  
      /**
       * If there are more than 5 inactive audio nodes in the pool, clear out the rest.
       */
      _drainPool: function() {
        var self = this,
          inactive = 0,
          i;
  
        // count the number of inactive nodes
        for (i=0; i<self._audioNode.length; i++) {
          if (self._audioNode[i].paused) {
            inactive++;
          }
        }
  
        // remove excess inactive nodes
        for (i=self._audioNode.length-1; i>=0; i--) {
          if (inactive <= 5) {
            break;
          }
  
          if (self._audioNode[i].paused) {
            inactive--;
            self._audioNode.splice(i, 1);
          }
        }
      },
  
      /**
       * Clear 'onend' timeout before it ends.
       * @param  {Number} timerId The ID of the sound to be cancelled.
       */
      _clearEndTimer: function(timerId) {
        var self = this,
          timer = self._onendTimer.indexOf(timerId);
  
        // make sure the timer gets cleared
        timer = timer >= 0 ? timer : 0;
  
        if (self._onendTimer[timer]) {
          clearTimeout(self._onendTimer[timer]);
          self._onendTimer.splice(timer, 1);
        }
      },
  
      /**
       * Setup the gain node and panner for a Web Audio instance.
       * @return {Object} The new audio node.
       */
      _setupAudioNode: function() {
        var self = this,
          node = self._audioNode,
          index = self._audioNode.length;
  
        // create gain node
        node[index] = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain();
        node[index].gain.value = self._volume;
        node[index].paused = true;
        node[index]._pos = 0;
        node[index].readyState = 4;
        node[index].connect(masterGain);
  
        // create the panner
        node[index].panner = ctx.createPanner();
        node[index].panner.setPosition(self._pos3d[0], self._pos3d[1], self._pos3d[2]);
        node[index].panner.connect(node[index]);
  
        return node[index];
      },
  
      /**
       * Call/set custom events.
       * @param  {String}   event Event type.
       * @param  {Function} fn    Function to call.
       * @return {Object}
       */
      on: function(event, fn) {
        var self = this,
          events = self['_on' + event];
  
        if (fn) {
          events.push(fn);
        } else {
          for (var i=0; i<events.length; i++) {
            events[i].call(self);
          }
        }
  
        return self;
      },
  
      /**
       * Remove a custom event.
       * @param  {String}   event Event type.
       * @param  {Function} fn    Listener to remove.
       * @return {Object}         [description]
       */
      off: function(event, fn) {
        var self = this,
          events = self['_on' + event],
          fnString = fn.toString();
  
        // loop through functions in the event for comparison
        for (var i=0; i<events.length; i++) {
          if (fnString === events[i].toString()) {
            events.splice(i, 1);
            break;
          }
        }
  
        return self;
      }
  
    };
  
    // only define these functions when using WebAudio
    if (usingWebAudio) {
  
      /**
       * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API).
       * @param  {Object} obj The Howl object for the sound to load.
       * @param  {String} url The path to the sound file.
       */
      var loadBuffer = function(obj, url) {
        // check if the buffer has already been cached
        if (url in cache) {
          // set the duration from the cache
          obj._duration = cache[url].duration;
  
          // load the sound into this object
          loadSound(obj);
        } else {
          // load the buffer from the URL
          var xhr = new XMLHttpRequest();
          xhr.open('GET', url, true);
          xhr.responseType = 'arraybuffer';
          xhr.onload = function() {
            // decode the buffer into an audio source
            ctx.decodeAudioData(xhr.response, function(buffer) {
              if (buffer) {
                cache[url] = buffer;
                loadSound(obj, buffer);
              }
            });
          };
          xhr.onerror = function() {
            // if there is an error, switch the sound to HTML Audio
            if (obj._webAudio) {
              obj._buffer = true;
              obj._webAudio = false;
              obj._audioNode = [];
              delete obj._gainNode;
              obj.load();
            }
          };
          try {
            xhr.send();
          } catch (e) {
            xhr.onerror();
          }
        }
      };
  
      /**
       * Finishes loading the Web Audio API sound and fires the loaded event
       * @param  {Object}  obj    The Howl object for the sound to load.
       * @param  {Objecct} buffer The decoded buffer sound source.
       */
      var loadSound = function(obj, buffer) {
        // set the duration
        obj._duration = (buffer) ? buffer.duration : obj._duration;
  
        // setup a sprite if none is defined
        if (Object.getOwnPropertyNames(obj._sprite).length === 0) {
          obj._sprite = {_default: [0, obj._duration * 1000]};
        }
  
        // fire the loaded event
        if (!obj._loaded) {
          obj._loaded = true;
          obj.on('load');
        }
  
        if (obj._autoplay) {
          obj.play();
        }
      };
  
      /**
       * Load the sound back into the buffer source.
       * @param  {Object} obj   The sound to load.
       * @param  {Array}  loop  Loop boolean, pos, and duration.
       * @param  {String} id    (optional) The play instance ID.
       */
      var refreshBuffer = function(obj, loop, id) {
        // determine which node to connect to
        var node = obj._nodeById(id);
  
        // setup the buffer source for playback
        node.bufferSource = ctx.createBufferSource();
        node.bufferSource.buffer = cache[obj._src];
        node.bufferSource.connect(node.panner);
        node.bufferSource.loop = loop[0];
        if (loop[0]) {
          node.bufferSource.loopStart = loop[1];
          node.bufferSource.loopEnd = loop[1] + loop[2];
        }
      };
  
    }
  
    /**
     * Add support for AMD (Async Module Definition) libraries such as require.js.
     */
    if (typeof define === 'function' && define.amd) {
      define('Howler', function() {
        return {
          Howler: Howler,
          Howl: Howl
        };
      });
    } else {
      window.Howler = Howler;
      window.Howl = Howl;
    }
  })();