如何从 MIDI 序列中获取 Note On/Off 消息?

How to get Note On/Off messages from a MIDI sequence?

我希望在播放的 MIDI 序列中收到音符 on/off 事件的通知,以便在基于屏幕的(钢琴)键盘上显示音符。

下面的代码在播放 MIDI 文件时添加了一个 MetaEventListener 和一个 ControllerEventListener,但只在音轨的开始和结束处显示了一些消息。

我们如何侦听音符打开和音符关闭 MIDI 事件?

import java.io.File;
import javax.sound.midi.*;
import javax.swing.JOptionPane;

class PlayMidi {

    public static void main(String[] args) throws Exception {
        /* This MIDI file can be found at..
        https://drive.google.com/open?id=0B5B9wDXIGw9lR2dGX005anJsT2M&authuser=0
        */
        File path = new File("I:\projects\EverLove.mid");

        Sequence sequence = MidiSystem.getSequence(path);
        Sequencer sequencer = MidiSystem.getSequencer();

        sequencer.open();

        MetaEventListener mel = new MetaEventListener() {

            @Override
            public void meta(MetaMessage meta) {
                final int type = meta.getType();
                System.out.println("MEL - type: " + type);
            }
        };
        sequencer.addMetaEventListener(mel);

        int[] types = new int[128];
        for (int ii = 0; ii < 128; ii++) {
            types[ii] = ii;
        }
        ControllerEventListener cel = new ControllerEventListener() {

            @Override
            public void controlChange(ShortMessage event) {
                int command = event.getCommand();
                if (command == ShortMessage.NOTE_ON) {
                    System.out.println("CEL - note on!");
                } else if (command == ShortMessage.NOTE_OFF) {
                    System.out.println("CEL - note off!");
                } else {
                    System.out.println("CEL - unknown: " + command);
                }
            }
        };
        int[] listeningTo = sequencer.addControllerEventListener(cel, types);
        for (int ii : listeningTo) {
            System.out.println("Listening To: " + ii);
        }

        sequencer.setSequence(sequence);
        sequencer.start();
        JOptionPane.showMessageDialog(null, "Exit this dialog to end");
        sequencer.stop();
        sequencer.close();
    }
}

我会关注是否有比我的两个建议中的任何一个更好的答案,这两个建议显然不太理想。

  1. 编辑 MIDI 文件以包含与现有键匹配的元事件 on/off
  2. 编写您自己的事件系统

我自己对 MIDI 的了解还不多。我只是偶尔导入 MIDI 乐谱并删除大部分信息,将其转换为与我为自己的音频需求编写的事件系统一起使用(触发我编写的 FM 合成器)。

这是已接受答案的第一个建议的实现。它将显示一个选项窗格确认对话框,关于是否添加新轨道以保存与每个现有轨道的 NOTE_ONNOTE_OFF 消息相对应的元事件。

如果用户选择这样做,他们将在 MIDI 序列的整个播放过程中看到元事件。

import java.io.File;
import javax.sound.midi.*;
import javax.swing.JOptionPane;

class PlayMidi {

    /** Iterates the MIDI events of the first track and if they are a 
     * NOTE_ON or NOTE_OFF message, adds them to the second track as a 
     * Meta event. */
    public static final void addNotesToTrack(
            Track track,
            Track trk) throws InvalidMidiDataException {
        for (int ii = 0; ii < track.size(); ii++) {
            MidiEvent me = track.get(ii);
            MidiMessage mm = me.getMessage();
            if (mm instanceof ShortMessage) {
                ShortMessage sm = (ShortMessage) mm;
                int command = sm.getCommand();
                int com = -1;
                if (command == ShortMessage.NOTE_ON) {
                    com = 1;
                } else if (command == ShortMessage.NOTE_OFF) {
                    com = 2;
                }
                if (com > 0) {
                    byte[] b = sm.getMessage();
                    int l = (b == null ? 0 : b.length);
                    MetaMessage metaMessage = new MetaMessage(com, b, l);
                    MidiEvent me2 = new MidiEvent(metaMessage, me.getTick());
                    trk.add(me2);
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        /* This MIDI file can be found at..
         https://drive.google.com/open?id=0B5B9wDXIGw9lR2dGX005anJsT2M&authuser=0
         */
        File path = new File("I:\projects\EverLove.mid");

        Sequence sequence = MidiSystem.getSequence(path);
        Sequencer sequencer = MidiSystem.getSequencer();

        sequencer.open();

        MetaEventListener mel = new MetaEventListener() {

            @Override
            public void meta(MetaMessage meta) {
                final int type = meta.getType();
                System.out.println("MEL - type: " + type);
            }
        };
        sequencer.addMetaEventListener(mel);

        int[] types = new int[128];
        for (int ii = 0; ii < 128; ii++) {
            types[ii] = ii;
        }
        ControllerEventListener cel = new ControllerEventListener() {

            @Override
            public void controlChange(ShortMessage event) {
                int command = event.getCommand();
                if (command == ShortMessage.NOTE_ON) {
                    System.out.println("CEL - note on!");
                } else if (command == ShortMessage.NOTE_OFF) {
                    System.out.println("CEL - note off!");
                } else {
                    System.out.println("CEL - unknown: " + command);
                }
            }
        };
        int[] listeningTo = sequencer.addControllerEventListener(cel, types);
        StringBuilder sb = new StringBuilder();
        for (int ii = 0; ii < listeningTo.length; ii++) {
            sb.append(ii);
            sb.append(", ");
        }
        System.out.println("Listenning to: " + sb.toString());

        int mirror = JOptionPane.showConfirmDialog(
                null,
                "Add note on/off messages to another track as meta messages?",
                "Confirm Mirror",
                JOptionPane.OK_CANCEL_OPTION);
        if (mirror == JOptionPane.OK_OPTION) {
            Track[] tracks = sequence.getTracks();
            Track trk = sequence.createTrack();
            for (Track track : tracks) {
                addNotesToTrack(track, trk);
            }
        }

        sequencer.setSequence(sequence);
        sequencer.start();
        JOptionPane.showMessageDialog(null, "Exit this dialog to end");
        sequencer.stop();
        sequencer.close();
    }
}

键盘的实现

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

import javax.sound.midi.*;

import java.util.ArrayList;
import java.io.*;
import java.net.URL;

public class MidiPianola {

    private JComponent ui = null;
    public static final int OTHER = -1;
    public static final int NOTE_ON = 1;
    public static final int NOTE_OFF = 2;
    private OctaveComponent[] octaves;
    Sequencer sequencer;
    int startOctave = 0;
    int numOctaves = 0;

    MidiPianola(int startOctave, int numOctaves)
            throws MidiUnavailableException {
        this.startOctave = startOctave;
        this.numOctaves = numOctaves;
        initUI();
    }

    public void openMidi(URL url)
            throws InvalidMidiDataException, IOException {
        openMidi(url.openStream());
    }

    public void openMidi(InputStream is)
            throws InvalidMidiDataException, IOException {
        Sequence sequence = MidiSystem.getSequence(is);
        Track[] tracks = sequence.getTracks();
        Track trk = sequence.createTrack();
        for (Track track : tracks) {
            addNotesToTrack(track, trk);
        }
        sequencer.setSequence(sequence);
        startMidi();
    }

    public void startMidi() {
        sequencer.start();
    }

    public void stopMidi() {
        sequencer.stop();
    }

    public void closeSequencer() {
        sequencer.close();
    }

    private void handleNote(final int command, int note) {
        OctaveComponent octave = getOctaveForNote(note);
        PianoKey key = octave.getKeyForNote(note);
        if (command == NOTE_ON) {
            key.setPressed(true);
        } else if (command == NOTE_OFF) {
            key.setPressed(false);
        }
        ui.repaint();
    }

    private OctaveComponent getOctaveForNote(int note) {
        return octaves[(note / 12) - startOctave];
    }

    public void initUI() throws MidiUnavailableException {
        if (ui != null) {
            return;
        }
        sequencer = MidiSystem.getSequencer();
        MetaEventListener mel = new MetaEventListener() {

            @Override
            public void meta(MetaMessage meta) {
                final int type = meta.getType();
                byte b = meta.getData()[1];
                int i = (int) (b & 0xFF);
                handleNote(type, i);
            }
        };
        sequencer.addMetaEventListener(mel);
        sequencer.open();

        ui = new JPanel(new BorderLayout(4, 4));
        ui.setBorder(new EmptyBorder(4, 4, 4, 4));

        JPanel keyBoard = new JPanel(new GridLayout(1, 0));
        ui.add(keyBoard, BorderLayout.CENTER);
        int end = startOctave + numOctaves;
        octaves = new OctaveComponent[end - startOctave];
        for (int i = startOctave; i < end; i++) {
            octaves[i - startOctave] = new OctaveComponent(i);
            keyBoard.add(octaves[i - startOctave]);
        }

        JToolBar tools = new JToolBar();
        tools.setFloatable(false);
        ui.add(tools, BorderLayout.PAGE_START);
        tools.setFloatable(false);
        Action open = new AbstractAction("Open") {

            JFileChooser fileChooser = new JFileChooser();

            @Override
            public void actionPerformed(ActionEvent e) {
                int result = fileChooser.showOpenDialog(ui);
                if (result == JFileChooser.APPROVE_OPTION) {
                    File f = fileChooser.getSelectedFile();
                    try {
                        openMidi(f.toURI().toURL());
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        };
        tools.add(open);

        Action rewind = new AbstractAction("Rewind") {

            @Override
            public void actionPerformed(ActionEvent e) {
                sequencer.setTickPosition(0);
            }
        };
        tools.add(rewind);

        Action play = new AbstractAction("Play") {

            @Override
            public void actionPerformed(ActionEvent e) {
                startMidi();
            }
        };
        tools.add(play);

        Action stop = new AbstractAction("Stop") {

            @Override
            public void actionPerformed(ActionEvent e) {
                stopMidi();
            }
        };
        tools.add(stop);
    }

    public JComponent getUI() {
        return ui;
    }

    /**
     * Iterates the MIDI events of the first track, and if they are a NOTE_ON or
     * NOTE_OFF message, adds them to the second track as a Meta event.
     */
    public static final void addNotesToTrack(
            Track track,
            Track trk) throws InvalidMidiDataException {
        for (int ii = 0; ii < track.size(); ii++) {
            MidiEvent me = track.get(ii);
            MidiMessage mm = me.getMessage();
            if (mm instanceof ShortMessage) {
                ShortMessage sm = (ShortMessage) mm;
                int command = sm.getCommand();
                int com = OTHER;
                if (command == ShortMessage.NOTE_ON) {
                    com = NOTE_ON;
                } else if (command == ShortMessage.NOTE_OFF) {
                    com = NOTE_OFF;
                }
                if (com > OTHER) {
                    byte[] b = sm.getMessage();
                    int l = (b == null ? 0 : b.length);
                    MetaMessage metaMessage = new MetaMessage(
                            com,
                            b,
                            l);
                    MidiEvent me2 = new MidiEvent(metaMessage, me.getTick());
                    trk.add(me2);
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    try {
                        UIManager.setLookAndFeel(
                                UIManager.getSystemLookAndFeelClassName());
                    } catch (Exception useDefault) {
                    }
                    SpinnerNumberModel startModel = 
                            new SpinnerNumberModel(2,0,6,1);
                    JOptionPane.showMessageDialog(
                            null,
                            new JSpinner(startModel),
                            "Start Octave",
                            JOptionPane.QUESTION_MESSAGE);
                    SpinnerNumberModel octavesModel = 
                            new SpinnerNumberModel(5,5,11,1);
                    JOptionPane.showMessageDialog(
                            null,
                            new JSpinner(octavesModel),
                            "Number of Octaves",
                            JOptionPane.QUESTION_MESSAGE);
                    final MidiPianola o = new MidiPianola(
                            startModel.getNumber().intValue(),
                            octavesModel.getNumber().intValue());

                    WindowListener closeListener = new WindowAdapter() {

                        @Override
                        public void windowClosing(WindowEvent e) {
                            o.closeSequencer();
                        }
                    };

                    JFrame f = new JFrame("MIDI Pianola");
                    f.addWindowListener(closeListener);
                    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                    f.setLocationByPlatform(true);

                    f.setContentPane(o.getUI());
                    f.setResizable(false);
                    f.pack();

                    f.setVisible(true);
                } catch (MidiUnavailableException ex) {
                    ex.printStackTrace();
                } catch (InvalidMidiDataException ex) {
                    ex.printStackTrace();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        };
        SwingUtilities.invokeLater(r);
    }
}

class OctaveComponent extends JPanel {

    int octave;
    ArrayList<PianoKey> keys;
    PianoKey selectedKey = null;

    public OctaveComponent(int octave) {
        this.octave = octave;
        init();
    }

    public PianoKey getKeyForNote(int note) {
        int number = note % 12;
        return keys.get(number);
    }

    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        for (PianoKey key : keys) {
            key.draw(g2);
        }
    }

    public static final Shape
            removeArrayFromShape(Shape shape, Shape[] shapes) {
        Area a = new Area(shape);

        for (Shape sh : shapes) {
            a.subtract(new Area(sh));
        }

        return a;
    }

    public final Shape getEntireBounds() {
        Area a = new Area();
        for (PianoKey key : keys) {
            a.add(new Area(key.keyShape));
        }
        return a;
    }

    @Override
    public Dimension getPreferredSize() {
        Shape sh = getEntireBounds();
        Rectangle r = sh.getBounds();
        Dimension d = new Dimension(r.x + r.width, r.y + r.height + 1);
        return d;
    }

    public void init() {

        keys = new ArrayList<PianoKey>();
        int w = 30;
        int h = 200;
        int x = 0;
        int y = 0;
        int xs = w - (w / 3);
        Shape[] sharps = new Shape[5];
        int hs = h * 3 / 5;
        int ws = w * 2 / 3;
        sharps[0] = new Rectangle2D.Double(xs, y, ws, hs);
        xs += w;
        sharps[1] = new Rectangle2D.Double(xs, y, ws, hs);
        xs += 2 * w;
        sharps[2] = new Rectangle2D.Double(xs, y, ws, hs);
        xs += w;
        sharps[3] = new Rectangle2D.Double(xs, y, ws, hs);
        xs += w;
        sharps[4] = new Rectangle2D.Double(xs, y, ws, hs);

        Shape[] standards = new Shape[7];
        for (int ii = 0; ii < standards.length; ii++) {
            Shape shape = new Rectangle2D.Double(x, y, w, h);
            x += w;
            standards[ii] = removeArrayFromShape(shape, sharps);
        }

        int note = 0;
        int ist = 0;
        int ish = 0;
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "C", this));
        keys.add(new PianoKey(sharps[ish++], (octave * 12) + note++, "C#", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "D", this));
        keys.add(new PianoKey(sharps[ish++], (octave * 12) + note++, "D#", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "E", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "F", this));
        keys.add(new PianoKey(sharps[ish++], (octave * 12) + note++, "F#", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "G", this));
        keys.add(new PianoKey(sharps[ish++], (octave * 12) + note++, "G#", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "A", this));
        keys.add(new PianoKey(sharps[ish++], (octave * 12) + note++, "A#", this));
        keys.add(new PianoKey(standards[ist++], (octave * 12) + note++, "B", this));
    }
}

class PianoKey {

    Shape keyShape;
    int number;
    String name;
    Component component;
    boolean pressed = false;

    PianoKey(Shape keyShape, int number, String name, Component component) {
        this.keyShape = keyShape;
        this.number = number;
        this.name = name;
        this.component = component;
    }

    public void draw(Graphics2D g) {
        if (name.endsWith("#")) {
            g.setColor(Color.BLACK);
        } else {
            g.setColor(Color.WHITE);
        }
        g.fill(keyShape);
        g.setColor(Color.GRAY);
        g.draw(keyShape);
        if (pressed) {
            Rectangle r = keyShape.getBounds();
            GradientPaint gp = new GradientPaint(
                    r.x,
                    r.y,
                    new Color(255, 225, 0, 40),
                    r.x,
                    r.y + (int) r.getHeight(),
                    new Color(255, 225, 0, 188));
            g.setPaint(gp);
            g.fill(keyShape);
            g.setColor(Color.GRAY);
            g.draw(keyShape);
        }
    }

    public boolean isPressed() {
        return pressed;
    }

    public void setPressed(boolean pressed) {
        this.pressed = pressed;
    }
}

一个答案here。类似于:

class MidiPlayer implements Receiver {
    private Receiver myReceiver;

    void play() {
        Sequencer sequencer = MidiSystem.getSequencer();
        ...
        // Save the original receiver
        this.myReceiver = sequencer.getReceiver();
        // Override the receiver
        sequencer.getTransmitter().setReceiver(this);

        sequencer.start();
    }

    @Override
    public void send(MidiMessage msg, long tstamp) {
        // Send the message to the original receiver
        this.myReceiver.send(msg, tstamp);

        // Process the message
        if (msg instanceof ShortMessage) {
            ShortMessage shortMsg = (ShortMessage) msg;
            if (shortMsg.getCommand() == ShortMessage.NOTE_ON) {
                System.out.printf("NOTE ON\n");
            }
        }
        ...
    }