本文详解 Tone.js 中 Sequence 播放不同步、音符丢失或音量异常的根本原因,指出 triggerAttackRelease 的硬编码时长参数引发的时间冲突,并提供基于节奏单位(如 '4n')的健壮修复方案。
本文详解 tone.js 中 sequence 播放不同步、音符丢失或音量异常的根本原因,指出 `triggerattackrelease` 的硬编码时长参数引发的时间冲突,并提供基于节奏单位(如 `'4n'`)的健壮修复方案。
在使用 Tone.js 构建 Web 音序器时,一个常见却隐蔽的问题是:序列首次播放正常,但暂停后再次播放时出现音符“点击声”(click)、漏音、节奏漂移或音量衰减。该问题并非源于 Transport 控制逻辑或状态管理错误,而根植于 Tone.Sequence 内部音符触发方式的设计缺陷。
核心问题出现在 synth.triggerAttackRelease(note, 0.1, time) 这一行——其中 0.1 是以秒为单位的固定持续时间。当 Transport 的 BPM 发生变化、或多次启停导致内部调度时间线重置时,Tone.js 的音频调度器会严格校验每个事件的 startTime 必须严格大于前一事件的结束时间。而 0.1s 无法随节奏(如 '4n')动态缩放,在高速 tempo(如 160 BPM)下可能造成多个音符的 endTime = time + 0.1 相互重叠,触发 "Start time must be strictly greater than previous start time" 错误,进而导致调度失败、音符静音或异常裁剪。
✅ 正确做法是将时长参数替换为节奏单位字符串(rhythm notation),例如 '4n'(四分音符)、'8n'(八分音符),使 Tone 自动根据当前 BPM 和时间尺度计算精确的毫秒值:
const createSequence = () => {
const synth = new Tone.Synth().toDestination();
const sequence = new Tone.Sequence(
(time, note) => {
// ✅ 正确:使用节奏单位,自动适配 tempo 变化与调度精度
synth.triggerAttackRelease(note, '4n', time);
},
['C3', 'D3', 'E3', 'F3'],
'4n' // 序列步进间隔也为 '4n'
);
sequence.start(0);
return sequence;
};⚠️ 同时需注意以下关键实践:
- 避免重复创建 Synth 实例:每次调用 createSequence() 都新建 Tone.Synth() 会导致资源泄漏与音色叠加。应将 synth 提升至组件作用域外(如 useRef 或模块级单例),并在 stopSequence 中显式调用 synth.dispose()。
- 正确清理 Sequence:sequence.stop() 和 sequence.dispose() 必须成对调用;若未 dispose,残留引用会干扰后续调度。
- Transport 状态同步:确保 Tone.Transport.start() / .stop() 与 UI 状态(isPlaying)严格一致,避免 Transport 处于 started 状态但 Sequence 已销毁的竞态。
最终,将硬编码时间常量升级为节奏感知的符号化时长,不仅解决了时序抖动问题,更让整个 sequencer 具备真正的音乐语义一致性——这才是 Web Audio 开发中“时间即音乐”的最佳实践。


















