Search code examples
javascriptapihtml5-audioweb-audio-api

Web Audio API - multiple synchronized tracks - stopping previous track when new track starts


I'm trying to mimic the Web Audio API multitrack demo from the 'Mozilla Web Audio API for games' Web doc.

https://developer.mozilla.org/en-US/docs/Games/Techniques/Audio_for_Web_Games#Web_Audio_API_for_games

The only caveat I have is that I want the previous track to stop, once a new track is clicked (instead of playing layered on top of each other).

An example would be, click drums, the drums start playing, then click guitar, the drums stop and the guitar starts right where the drums left off.

Any ideas? Are there better tools/libraries for handling web audio?

http://jsfiddle.net/c87z11jj/1/

<ul>
  <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-leadguitar.mp3">Lead Guitar</a></li>
  <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-drums.mp3">Drums</a></li>
  <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-bassguitar.mp3">Bass Guitar</a></li>
  <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-horns.mp3">Horns</a></li>
  <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-clav.mp3">Clavi</a></li>
</ul>

    window.AudioContext = window.AudioContext || window.webkitAudioContext;

    var offset = 0;
    var context = new AudioContext();

    function playTrack(url) {
      var request = new XMLHttpRequest();
      request.open('GET', url, true);
      request.responseType = 'arraybuffer';

      var audiobuffer;

      // Decode asynchronously
      request.onload = function() {

        if (request.status == 200) {

          context.decodeAudioData(request.response, function(buffer) {
            var source = context.createBufferSource();
            source.buffer = buffer;
            source.connect(context.destination);
            console.log('context.currentTime '+context.currentTime);

            if (offset == 0) {
              source.start();
              offset = context.currentTime;
            } else {
              source.start(0,context.currentTime - offset);
            }

          }, function(e) {
            console.log('Error decoding audio data:' + e);
          });
        } else {


     console.log('Audio didn\'t load successfully; error code:' + request.statusText);
    }
  }
  request.send();
}

var tracks = document.getElementsByClassName('track');

for (var i = 0, len = tracks.length; i < len; i++) {
  tracks[i].addEventListener('click', function(e){
    console.log(this.href);
    playTrack(this.href);
    e.preventDefault();
  });
}

Solution

  • Figured it out with help from @Kaiido

    Example with both synchronization and starting a new track where a previous track stops:

    <ul>
      <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-leadguitar.mp3">Lead Guitar</a></li>
      <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-drums.mp3">Drums</a></li>
      <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-bassguitar.mp3">Bass Guitar</a></li>
      <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-horns.mp3">Horns</a></li>
      <li><a class="track" href="http://jPlayer.org/audio/mp3/gbreggae-clav.mp3">Clavi</a></li>
    </ul>
    
    let active_source = null;
    let buffers = {};
    const context = new(window.AudioContext || window.webkitAudioContext)();
    let offset = 0;
    const tempo = 3.074074076;
    const tracks = document.getElementsByClassName('track');
    
    function playTrack(url) {
      let buffer = buffers[url];
      let source = context.createBufferSource();
    
      source.buffer = buffer;
      source.connect(context.destination);
      source.loop = true;
    
      if (offset == 0) {
        source.start();
        offset = context.currentTime;
        active_source = source;
      } else {
        let relativeTime = context.currentTime - offset;
        let beats = relativeTime / tempo;
        let remainder = beats - Math.floor(beats);
        let delay = tempo - (remainder*tempo);
        let when = context.currentTime+delay;
    
        stopActiveSource(when);
        source.start(context.currentTime+delay,relativeTime+delay);
        active_source = source;
        source.onended = function() {
          active_source = null;
        };
      }
    }
    
    for (var i = 0, len = tracks.length; i < len; i++) {
      tracks[i].addEventListener('click', function(e) {
        playTrack(this.href);
        e.preventDefault();
      });
      getBuffer(tracks[i].href);
    }
    
    function getBuffer(url) {
      const request = new XMLHttpRequest();
      request.open('GET', url, true);
      request.responseType = 'arraybuffer';
      request.onload = function(evt) {
        context.decodeAudioData(request.response, store);
      };
      request.send();
    
      function store(buffer) {
        buffers[url] = buffer;
      }
    }
    
    function stopActiveSource(when) {
      if (active_source) {
        active_source.onended = null;
        active_source.stop(when);
      }
    }
    

    http://jsfiddle.net/mdq2c1wv/1/