
本文介绍如何借助 tone.js 的 transport.schedule 方法,在 midi 播放过程中精确、实时地同步显示每个音符的名称、时间与持续时长,避免 settimeout 带来的时序漂移和音频/视觉不同步问题。
本文介绍如何借助 tone.js 的 transport.schedule 方法,在 midi 播放过程中精确、实时地同步显示每个音符的名称、时间与持续时长,避免 settimeout 带来的时序漂移和音频/视觉不同步问题。
在基于 Web 的音乐应用开发中,仅“提前打印所有音符”或依赖 setTimeout 触发 UI 更新,往往会导致视觉反馈严重滞后于实际音频播放——尤其在变速、暂停、快进等动态场景下,setTimeout 完全脱离 Tone.js 的音频时钟,无法响应 Transport 的 tempo 变化或时间偏移。
正确做法是利用 Tone.js 内置的统一调度系统:Transport.schedule()。它基于 Web Audio 的高精度音频时钟(而非 JavaScript 的事件循环),确保回调严格对齐 musical time(以秒为单位的绝对时间点),与 synth.triggerAttackRelease() 等音频操作共享同一时间轴。
以下是推荐实现方案:
async function initiateMidi() {
const midi = await Tone.Midi.fromUrl('src/static/saliha-ya-khdoud-ettefeh.mid');
const synth = new Tone.Synth().toDestination();
synths.push(synth);
// 启动 Transport(可选:自动同步到 MIDI tempo)
if (midi.header.tempo > 0) {
Tone.Transport.bpm.value = midi.header.tempo;
}
const now = Tone.now() + 0.5;
midi.tracks.forEach((track) => {
track.notes.forEach((note) => {
const noteTime = note.time + now;
const noteDuration = note.duration;
// ✅ 正确:用 Transport.schedule 同步音频与 UI
Tone.Transport.schedule((time) => {
console.log(`Note: ${note.name}, Time: ${time.toFixed(3)}s, Duration: ${noteDuration}`);
// ? 此处可更新 DOM,如高亮钢琴键、渲染音符条等
updateNoteDisplay(note.name, noteTime, noteDuration);
}, noteTime);
// 音频触发保持不变,且与 schedule 时间点一致
synth.triggerAttackRelease(note.name, noteDuration, noteTime, note.velocity);
});
});
// ⚠️ 切记:启动 Transport 才会触发 schedule 回调
Tone.Transport.start();
}
关键注意事项:
-
Tone.Transport.start()必须显式调用,否则schedule()回调不会执行; - 所有
schedule()回调接收的time参数即为该事件实际发生的音频时间(单位:秒),可用于精确计算相对延迟或渲染位置; - 若需暂停/恢复显示,可监听
Tone.Transport.on('pause')和on('start')事件统一控制; - 避免混用
setTimeout与Transport.schedule——前者基于系统时间,后者基于音频时钟,二者不可互换; - 对于多轨 MIDI,建议为每轨单独调度,并注意
note.time是相对于该轨起始的时间偏移,已由Tone.Midi自动解析。
通过此方式,UI 更新与合成器发声严格锁相,即使在动态调整 BPM 或启用 swing 时,仍能保持毫秒级同步,真正实现专业级的实时音符可视化体验。











