
本文详解如何在 tone.js 中准确获取序列(sequence)的播放状态和进度,包括 `state` 和 `progress` 属性的真实行为、常见误区及可靠事件监听方案,助你精准触发完成回调。
在 Tone.js 中,Tone.Sequence 的 state 和 progress 属性常被开发者误用——它们并非实时反映当前播放位置或生命周期状态的通用指标,而是有特定语义和使用前提。
✅ 正确理解 state 与 progress
seq.state 是一个字符串,返回 "started"、"stopped" 或 "paused",仅在显式调用 .start()、.stop()、.pause() 后同步更新。它不会因音频时钟推进而自动刷新,且不表示“是否正在渲染音符”——例如,在 start(0) 后立即读取,可能仍为 "stopped"(因异步调度尚未生效)。
seq.progress 仅在启用循环(loop: true)时有意义:它返回 [0, 1) 区间内的归一化值,表示当前循环内已执行步数占比(如 4 步序列执行到第 2 步时为 0.5)。若 loop: false(默认),该值恒为 0,无法用于判断单次播放进度或是否完成。
因此,你示例中在回调内打印 seq.state 和 seq.progress 始终为 0,是完全符合设计预期的行为,并非 Bug。
✅ 推荐方案:使用事件监听替代轮询
要可靠地检测序列完成(尤其是单次播放),应监听 ended 事件,而非依赖 progress:
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 步(默认即 1)
loop: false // 明确禁用循环
}
).start(0);
// ✅ 正确:监听 ended 事件触发完成逻辑
seq.onended = () => {
console.log("✅ Sequence completed!");
// 在此处执行你的完成回调,如切换 UI、启动下一环节等
};
// ⚠️ 注意:Tone.js 14+ 中推荐使用 addEventListener 以兼容未来变更
// seq.addEventListener('ended', () => { ... });✅ 进阶:手动追踪进度(需自定义)
若需精确知道当前执行到第几步(如用于 UI 进度条),可结合 index 参数与 subdivisions 手动计数:
let currentStep = 0;
const totalSteps = notesArray.length;
const seq = new Tone.Sequence(
(time, note, index) => {
currentStep = index; // 0-based index of current callback invocation
synth.triggerAttackRelease(note.note, note.duration, time, 0.1);
console.log(`Playing step ${index + 1}/${totalSteps} → ${note.note}`);
if (index === totalSteps - 1) {
console.log("➡️ Final note triggered — sequence will end shortly.");
}
},
notesArray,
{ subdivisions: 1 }
).start(0);
seq.onended = () => {
console.log(`? Full sequence finished. Total steps: ${totalSteps}`);
currentStep = -1; // 重置
};⚠️ 注意事项总结
- ❌ 不要依赖 seq.progress 判断单次播放进度——它仅对循环序列有效;
- ❌ 避免在音符回调中直接读取 seq.state 判定播放中状态——它滞后且不可靠;
- ✅ 优先使用 onended 事件捕获完成时机;
- ✅ 如需步进控制,利用回调函数的 index 参数 + subdivisions 配置;
- ✅ 确保调用 Tone.start()(尤其在用户交互后)以解锁 Web Audio 上下文,否则序列可能静默失败。
通过以上方法,你将能稳健、精准地掌控 Tone.js 序列的生命周期与执行状态。










