Java处理MIDI文件时如何准确定位特定NoteOn事件对应的NoteOff事件
MIDI NoteOn与对应NoteOff事件的可靠匹配方案
核心实现逻辑
针对同音高重叠音符的匹配场景,最稳定的方案是为每个MIDI通道的每个音高单独维护一个先进后出的栈结构,完全匹配MIDI事件的物理触发逻辑:先后按下的同音高按键,松键时总是先匹配最晚按下的未释放按键。
具体遍历规则如下:
- 按时间戳从小到大顺序遍历所有MIDI事件
- 遇到有效NoteOn事件(Velocity值大于0):将该事件的引用、触发tick等信息推入对应「通道号+音高」的栈中
- 遇到NoteOff事件,或者Velocity值为0的NoteOn事件:取出对应「通道号+音高」栈的栈顶元素,该元素就是当前NoteOff事件关联的NoteOn事件,配对完成后弹出栈顶即可
Java代码实现示例
基于Java原生javax.sound.midi包的实现参考:
import javax.sound.midi.MidiEvent; import javax.sound.midi.ShortMessage; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; // 存储NoteOn事件的上下文信息 class NoteOnContext { public MidiEvent noteOnEvent; public long triggerTick; public NoteOnContext(MidiEvent event, long tick) { this.noteOnEvent = event; this.triggerTick = tick; } } public class MidiNotePairMatcher { // 键格式:通道号_音高值,值为对应音高的未关闭NoteOn事件栈 private final Map<String, Deque<NoteOnContext>> noteStackMap = new HashMap<>(); public void processSingleEvent(MidiEvent event) { if (!(event.getMessage() instanceof ShortMessage shortMsg)) { return; } int command = shortMsg.getCommand(); int channel = shortMsg.getChannel(); int noteNumber = shortMsg.getData1(); int velocity = shortMsg.getData2(); String stackKey = channel + "_" + noteNumber; // 初始化对应音高的栈 Deque<NoteOnContext> targetStack = noteStackMap.computeIfAbsent(stackKey, k -> new LinkedList<>()); // 处理有效NoteOn事件 if (command == ShortMessage.NOTE_ON && velocity > 0) { targetStack.push(new NoteOnContext(event, event.getTick())); return; } // 处理NoteOff类事件 if (command == ShortMessage.NOTE_OFF || (command == ShortMessage.NOTE_ON && velocity == 0)) { if (!targetStack.isEmpty()) { NoteOnContext matchedNoteOn = targetStack.pop(); // 此处添加自定义业务逻辑,比如存储音符起止时间、移动音符等 handlePairedNote(matchedNoteOn, event); } } } private void handlePairedNote(NoteOnContext noteOnCtx, MidiEvent noteOffEvent) { // 示例:计算音符时长 long noteDuration = noteOffEvent.getTick() - noteOnCtx.triggerTick; } }
边界情况处理
- 未关闭的悬空NoteOn事件:遍历完所有事件后如果栈内还有剩余元素,可默认将音轨的总时长作为对应NoteOff的触发时间,避免出现无尾音符
- 跨音轨操作场景:MIDI通道是全局生效的,如果你的工具支持跨音轨处理事件,需要按全局「通道+音高」维护栈,不要按单个音轨独立维护
- 鼓组通道适配:通用MIDI的鼓组通道(默认第10通道,索引从0开始为9)的匹配规则完全一致,无需单独调整
内容的提问来源于stack exchange,提问作者Daemoth
相关产品推荐
相关产品推荐

