播放由两个 AudioBuffer 组成的节点时,有没有办法实时单独更改增益?

Is there any way to change the gain individually in real time when playing a node that is a composite of two AudioBuffers?

我遇到了以下问题。
当我 运行 AudioBufferSourceNode.start() 当我有多个音轨时,有时会出现延迟
然后,根据 chrisguttandin 的回答,我尝试了使用 offLineAudioContext 的方法。 (感谢 chrisguttandin)。


我想完全同时播放两个不同的mp3文件,所以我使用offlineAudioContext合成了一个audioBuffer。
并且我成功播放了合成节点。 下面是它的演示。
CodeSandBox
demo中的代码是基于下页中的代码。
OfflineAudioContext - Web APIs | MDN

但是,该演示不允许您更改两种音频中每一种的增益。
有什么办法可以改变两种音频播放时的增益吗?

我想做的事情如下

因此,如果你能按照上面的描述实现你想要做的事情,你就不需要使用offlineAudioContext。

我能想到的唯一方法是 运行 在每个输入 type="range" 上开始渲染,但我认为从性能的角度来看这不实用。
另外,我也在寻找解决这个问题的方法,但找不到。

代码

let ctx = new AudioContext(),
  offlineCtx,
  tr1,
  tr2,
  renderedBuffer,
  renderedTrack,
  tr1gain,
  tr2gain,
  start = false;

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

const App = () => {
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    (async () => {
      const bufferArray = trackArray.map(async (track) => {
        const res = await fetch("/" + track + ".mp3");
        const arrayBuffer = await res.arrayBuffer();
        return await ctx.decodeAudioData(arrayBuffer);
      });
      const audioBufferArray = await Promise.all(bufferArray);
      const source = audioBufferArray[0];
      offlineCtx = new OfflineAudioContext(
        source.numberOfChannels,
        source.length,
        source.sampleRate
      );
      tr1 = offlineCtx.createBufferSource();
      tr2 = offlineCtx.createBufferSource();
      tr1gain = offlineCtx.createGain();
      tr2gain = offlineCtx.createGain();
      tr1.buffer = audioBufferArray[0];
      tr2.buffer = audioBufferArray[1];
      tr1.connect(tr1gain);
      tr1gain.connect(offlineCtx.destination);
      tr2.connect(tr1gain);
      tr2gain.connect(offlineCtx.destination);
      tr1.start();
      tr2.start();
      offlineCtx.startRendering().then((buffer) => {
        renderedBuffer = buffer;
        renderedTrack = ctx.createBufferSource();
        renderedTrack.buffer = renderedBuffer;
        setLoading(false);
      });
    })();

    return () => {
      ctx.close();
    };
  }, []);

  const [playing, setPlaying] = useState(false);
  const playAudio = () => {
    if (!start) {
      renderedTrack = ctx.createBufferSource();
      renderedTrack.buffer = renderedBuffer;
      renderedTrack.connect(ctx.destination);
      renderedTrack.start();
      setPlaying(true);
      start = true;
      return;
    }
    ctx.resume();
    setPlaying(true);
  };
  const pauseAudio = () => {
    ctx.suspend();
    setPlaying(false);
  };
  const stopAudio = () => {
    renderedTrack.disconnect();
    start = false;
    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 = trackArray.map((track, index) => (
    <div key={index}>
      <span>{track}</span>
      <input
        type="range"
        onChange={changeVolume}
        step="any"
        max="1"
        aria-label={track}
        disabled={loading ? true : false}
      />
    </div>
  ));

  return (
    <>
      <button
        onClick={playing ? pauseAudio : playAudio}
        disabled={loading ? true : false}
      >
        {playing ? "pause" : "play"}
      </button>
      <button onClick={stopAudio} disabled={loading ? true : false}>
        stop
      </button>
      {Inputs}
    </>
  );
};

作为测试,我会回到您原来的解决方案,而不是

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

试试

t = ctx.currentTime;
tr1.start(t+0.1);
tr2.start(t+0.1);

音频开始前会有100毫秒左右的延迟,但要精确同步。如果可行,请将 0.1 减小到更小的值,但不要为零。一旦成功,您就可以将单独的增益节点连接到每个轨道并实时控制每个轨道的增益。

哦,还有一件事,而不是在调用 start 后恢复上下文,你可能想做一些类似的事情

ctx.resume()
  .then(() => {
    let t = ctx.currentTime;
    tr1.start(t + 0.1);
    tr2.start(t + 0.1);
  });

如果上下文暂停,时钟不会 运行,并且不会立即恢复。重启音频硬件可能需要一些时间。

哦,另一种方法,因为我看到您使用离线上下文创建的缓冲区中有两个通道。

s 为您在离线上下文中创建的 AudioBufferSourceNode。

let splitter = new ChannelSplitterNode(ctx, {numberOfOutputs: 2});
s.connect(splitter);
let g1 = new GainNode(ctx);
let g2 = new GainNode(ctx);
splitter.connect(g1, 0, 0);
splitter.connect(g2, 1, 0);

let merger = new ChannelMergerNode(ctx, {numberOfInputs: 1});
g1.connect(merger, 0, 0);
g2.connect(merger, 0 ,1);

// Connect merger to the downstream nodes or the destination.

您现在可以启动 s 并根据需要修改 g1g2 以生成您想要的输出。

您可以移除在离线上下文中创建的增益节点;除非您真的想在离线环境中应用某种增益,否则不需要它们。

但如果我这样做,除非绝对必要,否则我宁愿不使用离线上下文。