如何让MIDI Sequencer播放NOTE_ON事件时通知其他类?
解决方案:监听MIDI序列的NOTE_ON事件并同步用户输入时机
这个需求完全可行,不需要依赖Sequencer.SyncMode,直接给Sequencer注册MidiEventListener就能精准捕获NOTE_ON事件,进而通知UserInputHandler类设置按键时机。
具体实现步骤
创建MidiEventListener实现类
这个监听器会接收Sequencer发送的所有MIDI事件,我们只筛选NOTE_ON类型的事件,再调用输入处理类的对应方法:import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiMessage; import javax.sound.midi.MidiEventListener; import javax.sound.midi.ShortMessage; public class NoteOnListener implements MidiEventListener { private final UserInputHandler inputHandler; public NoteOnListener(UserInputHandler inputHandler) { this.inputHandler = inputHandler; } @Override public void meta(MidiMessage metaMessage, long timeStamp) { // 可选:处理曲目结束等元事件 } @Override public void midi(MidiEvent event) { MidiMessage message = event.getMessage(); if (message instanceof ShortMessage) { ShortMessage shortMsg = (ShortMessage) message; // 过滤出真正的NOTE_ON事件(排除用velocity=0替代NOTE_OFF的情况) if (shortMsg.getCommand() == ShortMessage.NOTE_ON && shortMsg.getVelocity() > 0) { int note = shortMsg.getData1(); long eventTick = event.getTick(); // 获取事件的节拍时间戳,保证和音乐同步 inputHandler.prepareForKeyPress(note, eventTick); } } } }修改startSequencer方法,注册监听器
在启动Sequencer前完成监听器注册,这样播放时触发NOTE_ON事件就会自动回调:public void startSequencer() throws MidiUnavailableException, InterruptedException, InvalidMidiDataException { sequencer.open(); Thread.sleep(2000); // 实例化输入处理类并注册监听器 UserInputHandler inputHandler = new UserInputHandler(); sequencer.addMidiEventListener(new NoteOnListener(inputHandler)); sequencer.setSequence(sequence); sequencer.start(); while (sequencer.isRunning()) { Thread.sleep(100); } sequencer.close(); }UserInputHandler核心逻辑示例
这个类负责根据收到的NOTE_ON事件,判断用户按键时机是否正确:import java.util.HashMap; import java.util.Map; import javax.sound.midi.Sequencer; public class UserInputHandler { private final Sequencer sequencer; private final Map<Integer, Long> activeNotes = new HashMap<>(); // 输入有效窗口,可根据BPM调整(单位:节拍数) private static final int INPUT_WINDOW = 500; public UserInputHandler(Sequencer sequencer) { this.sequencer = sequencer; } public void prepareForKeyPress(int note, long expectedTick) { activeNotes.put(note, expectedTick); // 可选:启动定时器自动清理超时未处理的音符 } // 用户按键时调用此方法,keyNote为按键对应的MIDI音符值 public void handleKeyPress(int keyNote) { Long expectedTick = activeNotes.get(keyNote); if (expectedTick == null) return; long currentTick = sequencer.getTickPosition(); if (Math.abs(currentTick - expectedTick) <= INPUT_WINDOW) { System.out.println("按键时机正确!"); } else { System.out.println("按键时机偏差过大!"); } activeNotes.remove(keyNote); } }
关键注意点
- NOTE_ON的特殊判断:部分MIDI文件会用
NOTE_ON+velocity=0替代NOTE_OFF,必须加上velocity > 0的条件避免误触发。 - 节拍同步优先:用
event.getTick()获取的节拍数比系统时间更可靠,能适配Sequencer播放速度调整的场景。 - 线程安全:如果输入处理类运行在UI线程或独立输入监听线程,建议用线程安全的集合(比如
ConcurrentHashMap)存储待处理音符。
内容的提问来源于stack exchange,提问作者fakezoo
相关产品推荐
相关产品推荐

