You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 08:53:17