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

11,609

Solution 1

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!

Solution 2

If you need to append/concatenate a list of buffer from an array (not only 2), here is a solution. I have just tweaked a bit the @Cwilso code (thanks for your help ;)

function concatBuffer(_buffers) {
    // _buffers[] is an array containig our audiobuffer list

    var buflengh = _buffers.length;
    var channels = [];
    var totalDuration = 0;

    for(var a=0; a<buflengh; a++){
        channels.push(_buffers[a].numberOfChannels);// Store all number of channels to choose the lowest one after
        totalDuration += _buffers[a].duration;// Get the total duration of the new buffer when every buffer will be added/concatenated
    }

    var numberOfChannels = channels.reduce(function(a, b) { return Math.min(a, b); });;// The lowest value contained in the array channels
    var tmp = context.createBuffer(numberOfChannels, context.sampleRate * totalDuration, context.sampleRate);// Create new buffer

    for (var b=0; b<numberOfChannels; b++) {
        var channel = tmp.getChannelData(b);
        var dataIndex = 0;

        for(var c = 0; c < buflengh; c++) {
            channel.set(_buffers[c].getChannelData(b), dataIndex);
            dataIndex+=_buffers[c].length;// Next position where we should store the next buffer values
        }
    }
    return tmp;
}
Share:
11,609

Related videos on Youtube

72lions
Author by

72lions

My name is Thodoris Tsiridis and I am from Greece. In February 2011 I moved to Stockholm, Sweden to work at Fi, where I had the opportunity to work with some really GREAT and TALENTED people. In January 2012 I joined Spotify and I'll be part of the team that makes your life a little bit better through music!

Updated on July 13, 2020

Comments

  • 72lions
    72lions almost 4 years

    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);
    
  • 72lions
    72lions over 11 years
    Thanks, your comment helped me! 72lions.github.com/PlayingChunkedMP3-WebAudioAPI
  • Jon Koops
    Jon Koops over 10 years
    @cwilso Is it possible to connect multiple AudioBufferSourceNodes together into a single one at different times of playback?
  • cwilso
    cwilso over 10 years
    Well, you can just connect them to the same destination. I think that has the effect you're looking for.
  • Faks
    Faks almost 6 years
    @cwilso Thanks for your appendBuffer() example! Just a quick question: Why do you use Math.min() to get the lowest buffer.numberOfChannels? What about using the highest? Would be wrong?
  • cwilso
    cwilso almost 6 years
    @faks if you used Math.max, you would either be trying to copy from buffers that don't exist or TO buffers that don't exist. This algorithm will work fine for stereo to stereo, but will also only copy two channels if one is stereo and one is quad, e.g.. (buffer.getchanneldata/setchanneldata will fail if the parameter is > the number of channels in the buffer.)
  • Simon H
    Simon H almost 4 years
    Rather than use totalDuration, it should calculate totalSamples, else it blows up due to floating point errors...: var totalSamples = 0; for(var a=0; a < buflengh; a++){ totalSamples += bufs[a].length; } var tmp = context.createBuffer(numberOfChannels, totalSamples, context.sampleRate);// Create new buffer
  • B''H Bi'ezras -- Boruch Hashem
    B''H Bi'ezras -- Boruch Hashem over 2 years
    hi do u know how to play it, stop it, restart it, and change the current time etc.?