Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
like image 766
72lions Avatar asked Jan 03 '13 17:01

72lions


2 Answers

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!

like image 114
cwilso Avatar answered Oct 05 '22 02:10

cwilso


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;
}
like image 35
Faks Avatar answered Oct 05 '22 01:10

Faks