
tone.js 的 `sequence` 对象虽提供 `state` 和 `progress` 属性,但 `progress` 仅在启用循环(`loop: true`)时返回归一化进度值(0–1),非循环序列中恒为 0;而 `state` 可实时反映 `"started"` / `"stopped"` 状态,需结合 `onstart`、`onstop` 和 `onend` 事件实现精准完成监听。
在 Tone.js 中,直接读取 seq.progress 或 seq.state 并不能可靠反映单次播放的实时进度或生命周期——这是开发者常遇到的认知偏差。根本原因在于:Sequence.progress 并非设计为实时进度条,而是为循环序列(loop: true)服务的归一化时间偏移量。当 loop: false(默认行为)时,其值始终为 0;即使序列正在运行,它也不会随节拍更新。
✅ 正确做法是放弃轮询 progress,转而使用事件驱动机制:
- seq.state 是只读字符串("started" / "stopped"),可用于同步状态判断(如 UI 按钮禁用),但本身不触发回调;
- seq.onstart、seq.onstop、seq.onend 是核心事件钩子,其中 onend 在整个序列完成最后一项后精确触发,是监听“播放完毕”的首选方式。
以下是修正后的完整示例:
const synth = new Tone.FMSynth().toDestination();
const notesArray = [
{ note: "B2", duration: "16n" },
{ note: "B3", duration: "16n" },
{ note: "A2", duration: "16n" },
{ note: "G2", duration: "16n" },
{ note: "B4", duration: "16n" }
];
const seq = new Tone.Sequence(
(time, note) => {
synth.triggerAttackRelease(note.note, note.duration, time, 0.1);
},
notesArray,
{
subdivisions: 1, // 每个数组项对应一个步进(默认即为 1)
loop: false // 明确声明非循环(默认值,但建议显式写出)
}
);
// ✅ 正确:使用 onend 监听完成事件
seq.onend = () => {
console.log("✅ Sequence completed!");
// 在此处触发你的业务逻辑:更新 UI、切换音效、启动下一段等
};
// ✅ 可选:监听状态变化(注意:state 是当前值,非事件)
seq.onstart = () => console.log("▶️ Sequence started");
seq.onstop = () => console.log("⏹️ Sequence stopped");
// 启动序列(推荐使用 Tone.now() 获取精确调度时间)
seq.start(Tone.now());⚠️ 注意事项:
- 不要在回调函数内打印 seq.progress —— 它在此上下文中无意义(非循环模式下恒为 0);
- seq.state 可用于即时判断(如 if (seq.state === "started") { ... }),但不能替代事件监听,因其不保证原子性或实时性;
- 若需自定义进度百分比(如用于进度条),应基于 Tone.Transport.position 手动计算:
const totalBeats = notesArray.length * (1/4); // 假设全为 16 分音符(16n = 1/4 拍) const currentBeat = Tone.Transport.position.split(":")[0]; // 粗略获取小节拍数(需更精确请结合 Transport.ticks) - 所有 onstart/onend 回调均在音频线程安全上下文中执行,可放心调用 Tone 方法。
总结:Tone.js 的 Sequence 是声明式调度器,而非状态机。掌握其事件模型(尤其是 onend)并避免误用 progress,是构建健壮音乐交互应用的关键基础。










