网络音频 API - 播放同步声音

Web Audio API - Playing synchronized sounds

我正在尝试找出通过网络音频 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]);
}

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

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

您可以尝试应用偏移量:

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

const startTime = context.currentTime + 1.0; // one second in the future

for (let i = 0; i < arrayOfAudioBuffers.length; i++) {
  play(arrayOfAudioBuffers[i], startTime);
}

此代码会将所有声音排队,以便在未来一秒同时播放。如果可行,您可以调低延迟以使声音播放更迅速,甚至可以根据音轨数量计算正确的延迟(例如,每音轨 2 毫秒 * 30 音轨 = 60 毫秒延迟)

由于浏览器可以防止指纹识别和计时攻击,现代浏览器可以降低或舍入引擎盖下的计时精度。 这意味着 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];

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

let songLength = 0;

for(let track of arrayOfAudioBuffers){
    if(track.length > songLength){
        songLength = track.length;
    }
}

接下来我创建了一个方法,它将接受 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;
}

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

现在我们可以像这样使用我们的 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()

有关网络音频精确计时的更多信息here

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

希望这对您有所帮助。

您似乎希望运行所有这些过程都像并行模式一样,以便实现零延迟。

问题是 javascript 与 java 不同,它有一个只能管理单个线程的事件循环(尽管它有更新的功能,旨在 运行 promises和类似异步的函数)。

正如他们所说:

Parallel.js solves that problem by giving you high level access to multi-core processing using web workers. It runs on node and in your browser.

通常情况下,网络浏览器在每个打开的选项卡中只使用一个网络工作者,但有一些库扩展了此功能,因此您可以 运行 多个线程和 运行 并行处理您的网络应用程序。 我从未尝试过,但这里有一些文章供您开始研究:

我是一个非常新手的程序员,也许这个答案不是很好,但我希望它能有所帮助。 对不起我的英语:)

你永远不会只是 source.start();(开始意味着现在)而是你 source.start(sometimeinthefuture); 网络音频有自己的内部时钟并且非常精确。并确保您在该循环中拥有的所有曲目都具有相同的 sometimeinthefuture.

此外,您不需要像答案所示那样混合最终混音,您只需要创建一个调度系统。您通过 javascript 的超时或设置间隔在网络音频时钟上安排到未来的位置。

这在文章 tale of two clocks. There's also a simpler article that explains this. Also check out Looper.js 中进行了解释,该文章使用此调度系统概念来同步音频循环器(其中需要同步多个轨道)。