You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Tone.PolySynth结合Sequence时如何避免音频消失?

Tone.js PolySynth步进音序器声音失真/静音问题

背景

我基于Tone.js文档中的官方步进音序器示例搭建项目,没有用Players方法播放MP3文件,而是采用PolySynth来实现多样音色探索。参考了多款鼓机、步进音序器的实现方案后,完成了一个基础可用的步进音序器,支持切换音符开关播放单音、按设定速度循环序列、切换播放/暂停模式等功能。

问题

音序器基础功能正常,但PolySynth的声音在循环几次后会开始失真,很快完全静音。音频消失后,Sequence调用似乎仍在继续,其他功能也能正常操作,但程序会出现明显卡顿、延迟。我怀疑是Sequence的处理方式有误,导致内存意外过载,但控制台没有输出明显错误信息。

已尝试方案

  • 使用带回调的Transport.scheduleRepeat:仍出现卡顿、静音问题
  • 用async调用处理Sequence播放切换:问题依旧
  • 用Array.forEach或for循环手动循环序列:无法保持Tone的时序同步
  • 用ref跟踪Sequence并持续销毁前一个节拍实例:仍存在卡顿、静音问题(当前代码采用此方案)

代码片段

项目基于React和TypeScript开发,以下是主App组件的核心代码:

function App() {
  type Loop = {
    dispose: any;
  };
  const [isPlaying, setIsPlaying] = React.useState(false);
  const [currentBeat, setCurrentBeat] = React.useState(0);
  const [currentBPM, setCurrentBPM] = React.useState(120);
  const [noteMatrix, setNoteMatrix] = React.useState(defaultNoteMatrix);
  const [currentScale, setCurrentScale] = React.useState(SCALES.major);
  const synth = new Tone.PolySynth().toDestination();
  const loop = React.useRef<Loop | null>(null);
  const noteMap = noteMatrix.map((beat) =>
    beat
      .map((note, i) => {
        return { note: currentScale[i], on: note };
      })
      .filter((note) => note.on)
      .map((item) => item.note)
  );

  const playSingleNote = (note = "") => synth.triggerAttackRelease(note, "16n");

  const tick = (e: React.MouseEvent<HTMLButtonElement>): void => {
    const currentBeat = parseFloat(e.currentTarget.dataset.beat!);
    const selectedNote = parseFloat(e.currentTarget.dataset.index!);
    const isOn = e.currentTarget.dataset.on === "1" ? 0 : 1;
    const updatedMatrix = [...noteMatrix];

    playSingleNote(currentScale[selectedNote]);
    updatedMatrix[currentBeat][selectedNote] = isOn;
    setNoteMatrix(updatedMatrix);
  };
  const reset = useCallback(() => {
    setNoteMatrix([
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat],
      [...emptyBeat]
    ]);
    setIsPlaying(false);
    Tone.Transport.stop();
  }, []);

  React.useEffect(() => {
    Tone.Transport.loop = false;
    Tone.Transport.on("stop", () => setCurrentBeat(0));
  });
  React.useEffect(() => {
    Tone.Transport.bpm.value = currentBPM;
  }, [currentBPM]);
  React.useEffect(() => {
    if (loop.current) {
      loop.current.dispose();
    }
    loop.current = new Tone.Sequence(
      (time, beat) => {
        setCurrentBeat(beat);
        synth.triggerAttackRelease(noteMap[beat], "16n", time);
      },
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
      "16n"
    ).start(0);
  }, [noteMap, isPlaying]);
  const togglePlay = React.useCallback(() => {
    Tone.context.resume();
    Tone.Transport.toggle();
    setIsPlaying((isPlaying) => !isPlaying);
  }, []);
  return (
    <>
      <GridStyle>
        <div className="beat">
          {currentScale.map((note, i) => (
            <div className="head" key={i}>
              {note}
            </div>
          ))}
        </div>
        {noteMatrix.map((beat, i) => (
          <div className="beat" key={i} data-active={currentBeat === i ? 1 : 0}>
            {currentScale.map((note, j) => (
              <button
                className="note"
                key={j}
                onClick={tick}
                data-note={note}
                data-on={beat[j]}
                data-beat={i}
                data-index={j}
              />
            ))}
          </div>
        ))}
      </GridStyle>
      <button onClick={togglePlay}>{isPlaying ? "Stop" : "Play"}</button>
      <button onClick={reset}>Clear</button>
      Scale
      <select
        name="scale"
        id="scale"
        defaultValue="major"
        onChange={(e) => setCurrentScale(SCALES[e.currentTarget.value])}
      >
        {["major", "minor", "suspended"].map((scale) => (
          <option value={scale} key={scale}>
            {scale}
          </option>
        ))}
      </select>
      BPM
      <select
        name="bpm"
        id="bpm"
        onChange={(e) => setCurrentBPM(parseFloat(e.currentTarget.value))}
        defaultValue={currentBPM}
      >
        {[80, 100, 120, 140, 160, 180, 200].map((bpm) => (
          <option value={bpm} key={bpm}>
            {bpm}
          </option>
        ))}
      </select>
    </>
  );
}

export default App;

我是Tone.js新手,但熟悉React,希望能得到相关指导和解决思路。我知道应用的数据处理、DOM处理部分不够优雅,但希望这些不是问题根源。

内容的提问来源于stack exchange,提问作者Bumhan Yu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 10:54:20