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

时间:2019-07-23 00:15:43

标签: javascript audio web-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]);
}

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

希望听到有关如何解决此问题的任何建议

5 个答案:

答案 0 :(得分:1)

由于浏览器可以防止指纹和定时攻击,因此现代浏览器可以降低或舍弃定时精度。 这意味着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;
}

fyi:您始终可以通过对每个通道的更新进行硬编码来消除一个循环。

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

希望这会有所帮助。

答案 1 :(得分:1)

您永远不会只是source.start();(开始是指现在),而是source.start(sometimeinthefuture);网络音频具有自己的内部时钟,并且非常精确。并确保您在该循环中拥有的所有这些曲目在将来的某个时间都具有相同的

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

tale of two clocks一文中对此进行了解释。也有a simpler article对此进行了解释。另外,请检查使用此调度系统概念的Looper.js来同步音频循环程序(需要同步多个音轨)。

答案 2 :(得分:0)

您可以尝试应用偏移:

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 ms * 30轨道= 60 ms延迟)计算正确的延迟

答案 3 :(得分:0)

为了使延迟或延迟为零,您似乎希望像在并行模式下一样运行所有这些过程。

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

如他们所说:

  

Parallel.js通过为您提供使用Web Worker进行多核处理的高级访问来解决该问题。它在节点和浏览器中运行。

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

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

答案 4 :(得分:0)

一种更简约的现代ES6方法。

<button id="start">playSound</button>
const audioPlay = async url => {
  const context = new AudioContext();
  const source = context.createBufferSource();
  const audioBuffer = await fetch(url)
    .then(res => res.arrayBuffer())
    .then(ArrayBuffer => context.decodeAudioData(ArrayBuffer));

  source.buffer = audioBuffer;
  source.connect(context.destination);
  source.start();
};

document.querySelector('#start').onclick = () => audioPlay('music/music.mp3');

停止播放:source.stop();

Web Audio API由事件触发,并且自动播放将无效。