Tone.js序列暂停后重放出现时序错乱、音量异常问题求助
问题:Tone.js音序器暂停后重播出现爆音、时序错乱
我用Tone.js构建音序器,点击播放按钮可触发简单序列播放,首次播放正常,但暂停后再次播放时,音符会随机出现爆音、时序错乱或音量降低的情况。此前已解决重复创建多个synth导致同音符叠加播放的问题,但当前序列播放仍无法保持稳定。问题出在sequencer/src/components/controls/Controls.js文件,播放按钮同时兼具播放和暂停功能。
相关代码:
const Controls = () => { const [started, setIsStarted] = useState(false) const [isPlaying, setIsPlaying] = useState(false) const [tempo, setTempo] = useTempoHook('100') const [subDivision, setSubDivision] = useSubDivisionHook('1/4') const [sequence, setSequence] = useState(false) useEffect(() => { console.log('Is Playing: ', isPlaying) console.log('Tempo: ', tempo) console.log('SubDivision: ', subDivision) }) Tone.Transport.bpm.value = tempo const createSequence = () => { const synth = new Tone.Synth().toDestination() const sequence = new Tone.Sequence((time, note) => { synth.triggerAttackRelease(note, 0.1, time) }, ['C3', 'D3', 'E3', 'F3'], '4n') sequence.start(0) return sequence } const startTone = async () => { await Tone.start() setIsStarted(true) } const stopSequence = () => { Tone.Transport.stop() sequence.stop() sequence.dispose() setIsPlaying(false) } const startSequence = () => { Tone.Transport.start() setSequence(createSequence()) setIsPlaying(true) } const start = () => { if (Tone.Transport.state === 'started') { stopSequence() } else { startSequence() } console.log(Tone.Transport.state) } const stop = () => { // setIsPlaying(false) } return ( <div> { !started && <ButtonControl icon={'Start'} color={'blue'} handler={startTone} /> } <AudioPlayBackManager playAudio={start} stopAudio={stop} /> <TempoSliderControl tempo={tempo} setTempo={setTempo} /> <SubDivisionControl setSubDivision={setSubDivision} /> </div> ) }
问题根源
- 每次重播重复创建实例:
startSequence中每次调用createSequence都会新建Synth和Sequence,旧实例未彻底清理,引发音频资源冲突。 - Transport与Sequence控制逻辑脱节:暂停时先停止Transport再处理Sequence,时序混乱;重播时重新创建Sequence并启动Transport,易导致不同步。
- React状态异步更新隐患:用
useState保存Sequence实例,状态更新的异步特性可能导致操作时拿不到最新实例,销毁不彻底。
修复方案
修改后的Controls.js代码
import { useState, useEffect, useRef } from 'react'; const Controls = () => { const [started, setIsStarted] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [tempo, setTempo] = useTempoHook('100'); const [subDivision, setSubDivision] = useSubDivisionHook('1/4'); // 用ref保存实例,规避React状态异步问题 const synthRef = useRef(null); const sequenceRef = useRef(null); // 仅初始化一次Synth和Sequence useEffect(() => { synthRef.current = new Tone.Synth().toDestination(); sequenceRef.current = new Tone.Sequence((time, note) => { synthRef.current.triggerAttackRelease(note, 0.1, time); }, ['C3', 'D3', 'E3', 'F3'], '4n'); // 组件卸载时清理资源 return () => { sequenceRef.current?.dispose(); synthRef.current?.dispose(); Tone.Transport.stop(); }; }, []); // 监听tempo变化,实时更新Transport速度 useEffect(() => { Tone.Transport.bpm.value = tempo; }, [tempo]); // 监听subDivision变化,更新序列间隔 useEffect(() => { if (sequenceRef.current) { sequenceRef.current.interval = subDivision; } }, [subDivision]); const startTone = async () => { await Tone.start(); setIsStarted(true); }; const stopSequence = () => { sequenceRef.current.stop(); Tone.Transport.stop(); setIsPlaying(false); }; const startSequence = () => { Tone.Transport.start(); sequenceRef.current.start(0); setIsPlaying(true); }; const togglePlay = () => { Tone.Transport.state === 'started' ? stopSequence() : startSequence(); }; return ( <div> {!started && <ButtonControl icon={'Start'} color={'blue'} handler={startTone} />} <AudioPlayBackManager playAudio={togglePlay} stopAudio={stopSequence} /> <TempoSliderControl tempo={tempo} setTempo={setTempo} /> <SubDivisionControl setSubDivision={setSubDivision} /> </div> ); }; export default Controls;
核心优化点
- 复用实例:用
useRef保存Synth和Sequence,避免每次重播重复创建,减少资源冲突。 - 统一控制逻辑:暂停时先停Sequence再停Transport,重播时先启动Transport再启动Sequence,保证时序同步。
- 实时响应配置变化:添加监听逻辑,tempo和subDivision修改时自动更新对应参数,无需重建序列。
- 资源清理:组件卸载时销毁实例,释放音频资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者basketstheclown
相关产品推荐
相关产品推荐

