Web Audio API如何将不同的AudioBuffers合并/连接起来,并作为一首歌曲播放

20

我一直在使用Web Audio API玩耍,尝试加载歌曲的多个部分并将它们附加到一个新的ArrayBuffer中,然后使用该ArrayBuffer作为播放所有部分的一首歌曲。在下面的例子中,我使用相同的歌曲数据(一个小循环)而不是歌曲的不同部分。

问题在于它仍然只播放一次,而不是两次,我不知道原因所在。

下载歌曲

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);
2个回答

25
您的代码问题在于您正在将另一个副本的MP3文件复制并附加到其末尾。该副本被解码器有效地忽略 - 它不是原始缓冲区数据,只是在完全的MP3文件后跟随的随机无用文件流中的垃圾数据。

您需要做的是首先将音频数据解码为AudioBuffer,然后将音频缓冲区连接到一个新的AudioBuffer中。这需要对您的代码进行一些重构。

您想要做的是:

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);

这是因为您的声音样本开头有近50毫秒的静默,而不是由于循环问题导致的,所以会出现轻微的播放间隔。

希望这可以帮助您!


1
谢谢,你的评论对我很有帮助!http://72lions.github.com/PlayingChunkedMP3-WebAudioAPI/ - 72lions
@cwilso 在不同的播放时间将多个AudioBufferSourceNodes连接到一个单独的节点中,这是否可能? - Jon Koops
好的,您可以将它们连接到相同的目标。我认为这就是您想要的效果。 - cwilso
@cwilso 感谢您的 appendBuffer() 示例!我有一个快速问题:为什么要使用 Math.min() 来获取最低的 buffer.numberOfChannels?使用最高的会有什么问题吗? - Faks
1
@faks 如果您使用 Math.max,则可能会尝试从不存在的缓冲区复制或复制到不存在的 TO 缓冲区。此算法将在立体声到立体声情况下正常工作,但如果一个是立体声而另一个是四声道,则仅将复制两个通道,例如。 (如果参数>缓冲区中的通道数,则 buffer.getchanneldata/setchanneldata 将失败。) - cwilso
嗨,你知道怎样玩、停止、重新启动它以及更改当前时间吗? - B''H Bi'ezras -- Boruch Hashem

6
如果您需要将数组中的一系列缓冲区附加/连接起来(不仅限于2个),这里提供了一个解决方案。我稍微修改了 @Cwilso 的代码(感谢你的帮助;)
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;
}

不要使用totalDuration,应该计算totalSamples,否则由于浮点错误会出现问题... var totalSamples = 0; for(var a=0; a < buflengh; a++){ totalSamples += bufs[a].length; }var tmp = context.createBuffer(numberOfChannels, totalSamples, context.sampleRate);// 创建新的缓冲区 - Simon H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接