如何用Tone.js实时显示MIDI文件播放时的音符?
使用Tone.js播放MIDI时实时显示音符的实现方案
问题描述
我希望用Tone.js播放MIDI文件时实时显示音符,但目前点击播放按钮会一次性打印所有音符信息。试过用setTimeout实现时序显示,但想找Tone.js原生的实现方式或更可靠的方案。
初始代码(一次性打印所有音符)
async function initiateMidi() { const midi = await Midi.fromUrl('src\static\saliha-ya-khdoud-ettefeh.mid'); const synth = new Tone.Synth().toDestination(); synths.push(synth); const now = Tone.now() + 0.5; midi.tracks.forEach((track) => { track.instrument; const notes = track.notes; notes.forEach((note) => { const noteTime = note.time + now; synth.triggerAttackRelease(note.name, note.duration, noteTime, note.velocity); console.log(`Note: ${note.name}, Time: ${noteTime}, Duration: ${note.duration}`); }); }); }
尝试用setTimeout的版本
async function initiateMidi() { const midi = await Midi.fromUrl('src\static\saliha-ya-khdoud-ettefeh.mid'); const synth = new Tone.Synth().toDestination(); synths.push(synth); const now = Tone.now() + 0.5; midi.tracks.forEach((track) => { const notes = track.notes; notes.forEach((note) => { const noteTime = note.time + now; const noteDuration = note.duration; setTimeout(() => { console.log(`Note: ${note.name}, Time: ${noteTime}, Duration: ${noteDuration}`); }, noteTime * 1000); synth.triggerAttackRelease(note.name, noteDuration, noteTime, note.velocity); }); }); }
可行实现方案
1. 使用Tone.js原生的Tone.Draw.schedule()
Tone.js提供了专门用于UI同步更新的Tone.Draw.schedule()方法,它会和音频引擎时钟完全同步,比setTimeout精度更高,能避免音频与显示不同步的问题。
修改后的代码示例:
async function initiateMidi() { const midi = await Midi.fromUrl('src\static\saliha-ya-khdoud-ettefeh.mid'); const synth = new Tone.Synth().toDestination(); synths.push(synth); const now = Tone.now() + 0.5; midi.tracks.forEach((track) => { const notes = track.notes; notes.forEach((note) => { const noteTime = note.time + now; const noteDuration = note.duration; // 用Tone.Draw调度UI更新,与音频触发时间完全同步 Tone.Draw.schedule(() => { console.log(`Note: ${note.name}, Time: ${noteTime}, Duration: ${noteDuration}`); // 可替换为DOM操作,比如在页面上展示当前音符 }, noteTime); synth.triggerAttackRelease(note.name, noteDuration, noteTime, note.velocity); }); }); }
2. 封装音符生命周期事件(可选)
如果需要同时处理音符的开始和结束显示,可以把两个事件都绑定到同一时间线:
async function initiateMidi() { const midi = await Midi.fromUrl('src\static\saliha-ya-khdoud-ettefeh.mid'); const synth = new Tone.Synth().toDestination(); synths.push(synth); const now = Tone.now() + 0.5; midi.tracks.forEach((track) => { track.notes.forEach(note => { const startTime = note.time + now; const endTime = startTime + note.duration; // 音符开始时显示 Tone.Draw.schedule(() => { console.log(`🎹 开始播放: ${note.name}, 力度: ${note.velocity}`); }, startTime); // 音符结束时标记(可选) Tone.Draw.schedule(() => { console.log(`🔚 停止播放: ${note.name}`); }, endTime); synth.triggerAttackRelease(note.name, note.duration, startTime, note.velocity); }); }); }
3. 使用Tone.Part批量管理事件
如果需要控制播放、暂停、循环等操作,Tone.Part可以帮你统一管理所有音符事件,代码更整洁可控:
async function initiateMidi() { const midi = await Midi.fromUrl('src\static\saliha-ya-khdoud-ettefeh.mid'); const synth = new Tone.Synth().toDestination(); synths.push(synth); const now = Tone.now() + 0.5; // 创建Part实例统一管理事件 const notePart = new Tone.Part((time, note) => { // 触发音频播放 synth.triggerAttackRelease(note.name, note.duration, time, note.velocity); // 实时显示音符 Tone.Draw.schedule(() => { console.log(`Note: ${note.name}, Time: ${time}, Duration: ${note.duration}`); }, time); }, []); // 把所有MIDI音符添加到Part中 midi.tracks.forEach(track => { track.notes.forEach(note => { notePart.at(note.time + now, note); }); }); // 启动播放 notePart.start(); }
方案优势说明
Tone.Draw.schedule()是Tone.js专为UI同步设计的API,完全规避setTimeout的精度误差(如浏览器延迟、时钟偏移),确保显示与音频完全同步。- 使用
Tone.Part可灵活控制整个MIDI轨道的播放状态,同时统一管理事件时序,代码可维护性更强。
内容的提问来源于stack exchange,提问作者Fyro
相关产品推荐
相关产品推荐

