Web Audio API-播放同步的声音

red*_* 87 5 javascript audio web-audio-api

我正在尝试找出通过Web Audio API播放同步音频轨道的最佳方法是什么。我正在尝试实现的是一次播放多个.wav文件,同时尽可能减少音轨同步中的延迟。

我发现同时播放多个音轨的唯一方法是创建多个音轨并在for循环中循环播放。这样做的问题是循环之间只有极短的延迟。延迟通常只有几毫秒,这取决于用户的机器,但是当我有30条需要同时开始的音轨,而我的循环必须循环遍历30条音轨并调用source.start()它们时,会有一个循环开始第30条音轨时出现明显的延迟。

因为我需要尽可能按时播放曲目,所以我想知道是否还有其他解决方案。例如,也许可以在其中通过Web Audio API加载多个源,然后产生一个本地全局事件,该事件将同时启动所有这些轨道。

这是一些显示问题的代码:

const audioBuffer1 = '...'; // Some decoded audio buffer
const audioBuffer2 = '...'; // some other decoded audio buffer
const audioBuffer3 = '...'; // and another audio buffer

const arrayOfAudioBuffers = [audioBuffer1, audioBuffer2, audioBuffer3];    
const context = new AudioContext();

function play(audioBuffer) {
  const source = context.createBufferSource();
  source.buffer = audioBuffer;
  source.connect(context.destination);
  source.start();
}

for (let i = 0; i < arrayOfAudioBuffers.length; i++) {
  // every time this loops the play function is 
  // called around 2 milliseconds after the previous
  // one causing sounds to get slightly out of sync
  play(arrayOfAudioBuffers[i]);
}
Run Code Online (Sandbox Code Playgroud)

Splice Beatmaker是使用多个轨道源并设法保持良好同步的应用示例。我已经探索了一些库,例如Howler和Tone,但它们似乎使用了我相信的循环方法。

很想听听有关如何解决此问题的任何建议

KpT*_*tor 9

由于浏览器可以防止指纹识别和计时攻击,现代浏览器可能会降低或调整引擎盖下的计时精度。这意味着source.start(offset)在您的情况下永远不可能 100% 准确或可靠。我推荐的是逐字节混合源,然后回放最终混合。假设所有音频源应同时启动,并且加载时间灵活,以下将起作用:

例子:

const audioBuffer1 = '...'; // Some decoded audio buffer
const audioBuffer2 = '...'; // some other decoded audio buffer
const audioBuffer3 = '...'; // and another audio buffer

const arrayOfAudioBuffers = [audioBuffer1, audioBuffer2, audioBuffer3];
Run Code Online (Sandbox Code Playgroud)

我们需要通过获取最大长度的缓冲区来计算整首歌曲的长度。

let songLength = 0;

for(let track of arrayOfAudioBuffers){
    if(track.length > songLength){
        songLength = track.length;
    }
}
Run Code Online (Sandbox Code Playgroud)

接下来我创建了一个方法来接收arrayOfAudioBuffers和输出最终的混音。

function mixDown(bufferList, totalLength, numberOfChannels = 2){

    //create a buffer using the totalLength and sampleRate of the first buffer node
    let finalMix = context.createBuffer(numberOfChannels, totalLength, bufferList[0].sampleRate);

    //first loop for buffer list
    for(let i = 0; i < bufferList.length; i++){

           // second loop for each channel ie. left and right   
           for(let channel = 0; channel < numberOfChannels; channel++){

            //here we get a reference to the final mix buffer data
            let buffer = finalMix.getChannelData(channel);

                //last is loop for updating/summing the track buffer with the final mix buffer 
                for(let j = 0; j < bufferList[i].length; j++){
                    buffer[j] += bufferList[i].getChannelData(channel)[j];
                }

           }
    }

    return finalMix;
}
Run Code Online (Sandbox Code Playgroud)

仅供参考:您始终可以通过对每个通道的更新进行硬编码来删除一个循环。

现在我们可以mixDown像这样使用我们的函数:

const mix = context.createBufferSource();
//call our function here
mix.buffer = mixDown(arrayOfAudioBuffers, songLength, 2);

mix.connect(context.destination);

//will playback the entire mixdown
mix.start()
Run Code Online (Sandbox Code Playgroud)

在此处详细了解网络音频精确计时

注意:我们可以OfflineAudioContext用来完成同样的事情,但不能保证精度,仍然依赖于循环和调用start()每个单独的源。

希望这可以帮助。

  • 只是想指出,关于“source.start(offset)”计时准确性的评论不太正确。如果未来偏移量足够远(可能是 10 毫秒或更长),它将在指定的时间开始,以上下文的采样率为模。时钟精度不会降低。 (2认同)