docs: function(sprite, internal) {
        var self = this;
        var id = null;
  
        // Determine if a sprite, sound id or nothing was passed
        if (typeof sprite === 'number') {
          id = sprite;
          sprite = null;
        } else if (typeof sprite === 'string' && self._state === 'loaded' && !self._sprite[sprite]) {
          // If the passed sprite doesn't exist, do nothing.
          return null;
        } else if (typeof sprite === 'undefined') {
          // Use the default sound sprite (plays the full audio length).
          sprite = '__default';
  
          // Check if there is a single paused sound that isn't ended. 
          // If there is, play that sound. If not, continue as usual.  
          if (!self._playLock) {
            var num = 0;
            for (var i=0; i<self._sounds.length; i++) {
              if (self._sounds[i]._paused && !self._sounds[i]._ended) {
                num++;
                id = self._sounds[i]._id;
              }
            }
  
            if (num === 1) {
              sprite = null;
            } else {
              id = null;
            }
          }
        }
  
        // Get the selected node, or get one from the pool.
        var sound = id ? self._soundById(id) : self._inactiveSound();
  
        // If the sound doesn't exist, do nothing.
        if (!sound) {
          return null;
        }
  
        // Select the sprite definition.
        if (id && !sprite) {
          sprite = sound._sprite || '__default';
        }
  
        // If the sound hasn't loaded, we must wait to get the audio's duration.
        // We also need to wait to make sure we don't run into race conditions with
        // the order of function calls.
        if (self._state !== 'loaded') {
          // Set the sprite value on this sound.
          sound._sprite = sprite;
  
          // Mark this sound as not ended in case another sound is played before this one loads.
          sound._ended = false;
  
          // Add the sound to the queue to be played on load.
          var soundId = sound._id;
          self._queue.push({
            event: 'play',
            action: function() {
              self.play(soundId);
            }
          });
  
          return soundId;
        }
  
        // Don't play the sound if an id was passed and it is already playing.
        if (id && !sound._paused) {
          // Trigger the play event, in order to keep iterating through queue.
          if (!internal) {
            self._loadQueue('play');
          }
  
          return sound._id;
        }
  
        // Make sure the AudioContext isn't suspended, and resume it if it is.
        if (self._webAudio) {
          Howler._autoResume();
        }
  
        // Determine how long to play for and where to start playing.
        var seek = Math.max(0, sound._seek > 0 ? sound._seek : self._sprite[sprite][0] / 1000);
        var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek);
        var timeout = (duration * 1000) / Math.abs(sound._rate);
        var start = self._sprite[sprite][0] / 1000;
        var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000;
        var loop = !!(sound._loop || self._sprite[sprite][2]);
        sound._sprite = sprite;
  
        // Mark the sound as ended instantly so that this async playback
        // doesn't get grabbed by another call to play while this one waits to start.
        sound._ended = false;
  
        // Update the parameters of the sound.
        var setParams = function() {
          sound._paused = false;
          sound._seek = seek;
          sound._start = start;
          sound._stop = stop;
          sound._loop = loop;
        };
  
        // End the sound instantly if seek is at the end.
        if (seek >= stop) {
          self._ended(sound);
          return;
        }
  
        // Begin the actual playback.
        var node = sound._node;
        if (self._webAudio) {
          // Fire this when the sound is ready to play to begin Web Audio playback.
          var playWebAudio = function() {
            self._playLock = false;
            setParams();
            self._refreshBuffer(sound);
  
            // Setup the playback params.
            var vol = (sound._muted || self._muted) ? 0 : sound._volume;
            node.gain.setValueAtTime(vol, Howler.ctx.currentTime);
            sound._playStart = Howler.ctx.currentTime;
  
            // Play the sound using the supported method.
            if (typeof node.bufferSource.start === 'undefined') {
              sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration);
            } else {
              sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration);
            }
  
            // Start a new timer if none is present.
            if (timeout !== Infinity) {
              self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
            }
  
            if (!internal) {
              setTimeout(function() {
                self._emit('play', sound._id);
                self._loadQueue();
              }, 0);
            }
          };
  
          if (Howler.state === 'running') {
            playWebAudio();
          } else {
            self._playLock = true;
  
            // Wait for the audio context to resume before playing.
            self.once('resume', playWebAudio);
  
            // Cancel the end timer.
            self._clearTimer(sound._id);
          }
        } else {
          // Fire this when the sound is ready to play to begin HTML5 Audio playback.
          var playHtml5 = function() {
            node.currentTime = seek;
            node.muted = sound._muted || self._muted || Howler._muted || node.muted;
            node.volume = sound._volume * Howler.volume();
            node.playbackRate = sound._rate;
  
            // Some browsers will throw an error if this is called without user interaction.
            try {
              var play = node.play();
  
              // Support older browsers that don't support promises, and thus don't have this issue.
              if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) {
                // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause().
                self._playLock = true;
  
                // Set param values immediately.
                setParams();
  
                // Releases the lock and executes queued actions.
                play
                  .then(function() {
                    self._playLock = false;
                    node._unlocked = true;
                    if (!internal) {
                      self._emit('play', sound._id);
                      self._loadQueue();
                    }
                  })
                  .catch(function() {
                    self._playLock = false;
                    self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' +
                      'on mobile devices and Chrome where playback was not within a user interaction.');
  
                    // Reset the ended and paused values.
                    sound._ended = true;
                    sound._paused = true;
                  });
              } else if (!internal) {
                self._playLock = false;
                setParams();
                self._emit('play', sound._id);
                self._loadQueue();
              }
  
              // Setting rate before playing won't work in IE, so we set it again here.
              node.playbackRate = sound._rate;
  
              // If the node is still paused, then we can assume there was a playback issue.
              if (node.paused) {
                self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' +
                  'on mobile devices and Chrome where playback was not within a user interaction.');
                return;
              }
  
              // Setup the end timer on sprites or listen for the ended event.
              if (sprite !== '__default' || sound._loop) {
                self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
              } else {
                self._endTimers[sound._id] = function() {
                  // Fire ended on this audio node.
                  self._ended(sound);
  
                  // Clear this listener.
                  node.removeEventListener('ended', self._endTimers[sound._id], false);
                };
                node.addEventListener('ended', self._endTimers[sound._id], false);
              }
            } catch (err) {
              self._emit('playerror', sound._id, err);
            }
          };
  
          // If this is streaming audio, make sure the src is set and load again.
          if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') {
            node.src = self._src;
            node.load();
          }
  
          // Play immediately if ready, or wait for the 'canplaythrough'e vent.
          var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS);
          if (node.readyState >= 3 || loadedNoReadyState) {
            playHtml5();
          } else {
            self._playLock = true;
  
            var listener = function() {
              // Begin playback.
              playHtml5();
  
              // Clear this listener.
              node.removeEventListener(Howler._canPlayEvent, listener, false);
            };
            node.addEventListener(Howler._canPlayEvent, listener, false);
  
            // Cancel the end timer.
            self._clearTimer(sound._id);
          }
        }
  
        return sound._id;
      }