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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 20:45:08