Search code examples
javascriptconcatenationhtml5-audioweb-audio-apiarraybuffer

Web Audio API append/concatenate different AudioBuffers and play them as one song


I've been playing with the Web Audio API and I'm trying to load multiple parts of a song and append them to a new ArrayBuffer and then use that ArrayBuffer for playing all the parts as one song. In the following example I am using the same song data (which is a small loop) instead of different parts of a song.

The problem is that it still plays just once instead of two times and I don't know why.

Download song

function init() {

  /**
   * Appends two ArrayBuffers into a new one.
   * 
   * @param {ArrayBuffer} buffer1 The first buffer.
   * @param {ArrayBuffer} buffer2 The second buffer.
   */
  function appendBuffer(buffer1, buffer2) {
    var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
    tmp.set( new Uint8Array(buffer1), 0);
    tmp.set( new Uint8Array(buffer2), buffer1.byteLength);
    return tmp;
  }

  /**
   * Loads a song
   * 
   * @param {String} url The url of the song.
   */
  function loadSongWebAudioAPI(url) {
    var request = new XMLHttpRequest();
    var context = new webkitAudioContext();

    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    /**
     * Appends two ArrayBuffers into a new one.
     * 
     * @param {ArrayBuffer} data The ArrayBuffer that was loaded.
     */
    function play(data) {
      // Concatenate the two buffers into one.
      var a = appendBuffer(data, data);
      var buffer = a.buffer;
      var audioSource = context.createBufferSource();
      audioSource.connect(context.destination);

      //decode the loaded data
      context.decodeAudioData(buffer, function(buf) {
        console.log('The buffer', buf);
        audioSource.buffer = buf;
        audioSource.noteOn(0);
        audioSource.playbackRate.value = 1;
      });

    };

    // When the song is loaded asynchronously try to play it.
    request.onload = function() {
      play(request.response);
    }

    request.send();
  }


  loadSongWebAudioAPI('http://localhost:8282/loop.mp3');
}

window.addEventListener('load',init,false);

Solution

  • The problem in your code is that you're copying and appending another copy of the MP3 file onto the end of itself. That copy gets effectively ignored by the decoder - it's not raw buffer data, it's just random spurious junk in the file stream, following a perfectly complete MP3 file.

    What you need to do is first decode the audio data into a AudioBuffer, then append the audio buffers together into a new AudioBuffer. This requires a little bit of restructuring of your code.

    What you want to do is this:

    var context = new webkitAudioContext();
    
    function init() {
    
      /**
       * Appends two ArrayBuffers into a new one.
       * 
       * @param {ArrayBuffer} buffer1 The first buffer.
       * @param {ArrayBuffer} buffer2 The second buffer.
       */
      function appendBuffer(buffer1, buffer2) {
        var numberOfChannels = Math.min( buffer1.numberOfChannels, buffer2.numberOfChannels );
        var tmp = context.createBuffer( numberOfChannels, (buffer1.length + buffer2.length), buffer1.sampleRate );
        for (var i=0; i<numberOfChannels; i++) {
          var channel = tmp.getChannelData(i);
          channel.set( buffer1.getChannelData(i), 0);
          channel.set( buffer2.getChannelData(i), buffer1.length);
        }
        return tmp;
      }
    
      /**
       * Loads a song
       * 
       * @param {String} url The url of the song.
       */
      function loadSongWebAudioAPI(url) {
        var request = new XMLHttpRequest();
    
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';
    
        /**
         * Appends two ArrayBuffers into a new one.
         * 
         * @param {ArrayBuffer} data The ArrayBuffer that was loaded.
         */
        function play(data) {
          //decode the loaded data
          context.decodeAudioData(data, function(buf) {
            var audioSource = context.createBufferSource();
            audioSource.connect(context.destination);
    
            // Concatenate the two buffers into one.
            audioSource.buffer = appendBuffer(buf, buf);
            audioSource.noteOn(0);
            audioSource.playbackRate.value = 1;
          });
    
        };
    
        // When the song is loaded asynchronously try to play it.
        request.onload = function() {
          play(request.response);
        }
    
        request.send();
      }
    
    
      loadSongWebAudioAPI('loop.mp3');
    }
    
    window.addEventListener('load',init,false);
    

    There's a slight playback gap - that's because you have nearly 50ms of silence at the beginning of your sound sample, not due to looping issues.

    Hope this helps!