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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 03:55:13