如何在MIDI序列中获取即将到来的事件(基于javax.sound.midi)
实现Guitar Hero克隆:提前获取即将到来的MIDI音符事件
我们在大学项目中使用javax.sound.midi开发简易Guitar Hero克隆程序,目前已实现通过Sequencer播放MIDI文件,也能记录当前播放的MIDI事件,但需要显示即将到来的事件(即MIDI消息中指示音符的数据字节)。当前通过Receiver在声音播放瞬间记录到控制台,无法提前获取即将到来的音符。
现有代码
Sequencer类代码
public void startSequencer() { try { sequencer.open(); Thread.sleep(2000); sequencer.setTempoInBPM(BPM); InputStream is = new BufferedInputStream(new FileInputStream(song)); sequencer.setSequence(is); sequencer.getTransmitter().setReceiver(receiver); sequencer.start(); while (sequencer.isRunning()) { Thread.sleep(100); } sequencer.close(); System.out.println(receiver.getNoteOnAmount()); } catch (MidiUnavailableException | InterruptedException | IOException | InvalidMidiDataException e) { throw new RuntimeException(e); } }
Receiver类代码
public void send(MidiMessage message, long timeStamp) { //If the Status is NOTE_ON (144), process the note if (message.getStatus() == 144) { System.out.println(message.getMessage()[1]); } }
解决方案:提前解析MIDI序列并跟踪播放进度
要获取即将到来的音符,不能仅依赖Receiver的实时回调,需要提前解析整个MIDI序列的事件,结合Sequencer的当前播放位置预判后续事件:
步骤1:预解析所有NOTE_ON事件及时间戳
在启动播放前,遍历MIDI序列的所有轨道,提取所有NOTE_ON事件(状态码144),并转换为绝对微秒时间戳:
// 解析MIDI序列,收集所有NOTE_ON事件 private List<NoteEvent> parseNoteEvents(Sequence sequence) { List<NoteEvent> noteEvents = new ArrayList<>(); for (Track track : sequence.getTracks()) { for (int i = 0; i < track.size(); i++) { MidiEvent event = track.get(i); MidiMessage msg = event.getMessage(); if (msg.getStatus() == 144) { // 筛选NOTE_ON事件 // 将节拍数转换为绝对微秒时间 long eventTime = event.getTick() * sequence.getResolution() * 1000000 / sequencer.getTempoInMPQ(); int note = msg.getMessage()[1]; noteEvents.add(new NoteEvent(note, eventTime)); } } } // 按时间戳排序事件 Collections.sort(noteEvents, Comparator.comparingLong(NoteEvent::getTime)); return noteEvents; } // 辅助类存储音符和对应时间戳 static class NoteEvent { private int note; private long time; public NoteEvent(int note, long time) { this.note = note; this.time = time; } public int getNote() { return note; } public long getTime() { return time; } }
步骤2:播放时实时查询即将到来的事件
在播放循环中,获取当前播放的微秒位置,筛选出当前时间+提前量范围内的事件:
修改startSequencer方法,加入事件查询逻辑:
public void startSequencer() { try { sequencer.open(); Thread.sleep(2000); sequencer.setTempoInBPM(BPM); InputStream is = new BufferedInputStream(new FileInputStream(song)); Sequence sequence = sequencer.setSequence(is); sequencer.getTransmitter().setReceiver(receiver); // 预解析所有NOTE_ON事件 List<NoteEvent> allNoteEvents = parseNoteEvents(sequence); // 设置提前预警时间(比如2秒,单位微秒) long warningThreshold = 2000000; sequencer.start(); while (sequencer.isRunning()) { // 获取当前播放的微秒位置 long currentTime = sequencer.getMicrosecondPosition(); // 筛选即将到来的音符事件 List<NoteEvent> upcomingNotes = new ArrayList<>(); for (NoteEvent event : allNoteEvents) { if (event.getTime() > currentTime && event.getTime() <= currentTime + warningThreshold) { upcomingNotes.add(event); } else if (event.getTime() > currentTime + warningThreshold) { // 因为事件已排序,后续事件时间更大,可提前终止循环 break; } } // 输出即将到来的音符 if (!upcomingNotes.isEmpty()) { System.out.print("即将到来的音符:"); upcomingNotes.forEach(note -> System.out.print(note.getNote() + " ")); System.out.println(); } Thread.sleep(100); } sequencer.close(); System.out.println(receiver.getNoteOnAmount()); } catch (MidiUnavailableException | InterruptedException | IOException | InvalidMidiDataException e) { throw new RuntimeException(e); } }
关键说明
- 时间戳转换:通过节拍数、序列分辨率和播放速度,将MIDI事件的相对节拍转换为绝对微秒时间,确保时间计算准确。
- 提前量调整:
warningThreshold可根据游戏节奏灵活修改,比如设置为1500000微秒(1.5秒),给玩家足够反应时间。 - 性能优化:利用事件已排序的特性,遍历到超出提前量范围的事件时直接终止循环,避免不必要的遍历。
内容的提问来源于stack exchange,提问作者fakezoo
相关产品推荐
相关产品推荐

