当我有多个音轨时运行 AudioBufferSourceNode.start() 时,有时会出现延迟

问题描述

我正在制作一个读取和播放两个音频文件的应用程序。
CodeSnadBox
上面的 CodeSandBox 有以下规格。

  • 按“播放”按钮播放音频
  • 两个音轨的音量都可以改变。

问题

播放音频时,有时会出现延迟。
但是,并非总是存在音频延迟,有时可以完全同时播放两首曲目。

虽然没有在上面的 CodeSandBox 中实现,但我目前正在开发的应用程序实现了一个搜索栏来指示当前播放位置。 通过移动搜索栏以指示当前播放位置,重新加载音频并且可以消除由此产生的延迟。 另一方面,即使音频在完全相同的时间播放,移动搜索栏也可能导致延迟。

无论如何,有没有办法以稳定一致的方式同时播放多个音轨?

代码

let ctx,tr1,tr2,tr1gain = 0,tr2gain = 0,start = false;

const trackList = ["track1","track2"];

const App = () => {
  useEffect(() => {
    ctx = new AudioContext();
    tr1 = ctx.createBufferSource();
    tr2 = ctx.createBufferSource();
    tr1gain = ctx.createGain();
    tr2gain = ctx.createGain();
    trackList.forEach(async (item) => {
      const res = await fetch("/" + item + ".mp3");
      const arrayBuffer = await res.arrayBuffer();
      const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
      item === "track1"
        ? (tr1.buffer = audioBuffer)
        : (tr2.buffer = audioBuffer);
    });
    tr1.connect(tr1gain);
    tr1gain.connect(ctx.destination);
    tr2.connect(tr2gain);
    tr2gain.connect(ctx.destination);
    return () => ctx.close();
  },[]);

  const [playing,setPlaying] = useState(false);
  const playAudio = () => {
    if (!start) {
      tr1.start();
      tr2.start();
      start = true;
    }
    ctx.resume();
    setPlaying(true);
  };
  const pauseAudio = () => {
    ctx.suspend();
    setPlaying(false);
  };

  const changeVolume = (e) => {
    const target = e.target.ariaLabel;
    target === "track1"
      ? (tr1gain.gain.value = e.target.value)
      : (tr2gain.gain.value = e.target.value);
  };
  const Inputs = trackList.map((item,index) => (
    <div key={index}>
      <span>{item}</span>
      <input
        type="range"
        onChange={changeVolume}
        step="any"
        max="1"
        aria-label={item}
      />
    </div>
  ));

  return (
    <>
      <button
        onClick={playing ? pauseAudio : playAudio}
        style={{ display: "block" }}
      >
        {playing ? "pause" : "play"}
      </button>
      {Inputs}
    </>
  );
};

解决方法

在不带参数的情况下调用 start() 时,与调用 start 时将 currentTimeAudioContext 作为第一个参数相同。在您的示例中,看起来像这样:

tr1.start(tr1.context.currentTime);
tr2.start(tr2.context.currentTime);

根据定义,currentTimeAudioContext 会随着时间增加。这完全有可能发生在两次调用之间。因此,解决问题的第一个尝试可能是确保两个函数调用使用相同的值。

const currentTime = tr1.context.currentTime;

tr1.start(currentTime);
tr2.start(currentTime);

由于 currentTime 通常随着渲染量的时间而增加,您可以通过增加一点延迟来增加额外的安全网。

const currentTime = tr1.context.currentTime + 128 / tr1.context.sampleRate;

tr1.start(currentTime);
tr2.start(currentTime);

如果这没有帮助,您还可以使用 OfflineAudioContext 将您的混音预先渲染为单个 AudioBuffer