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

Posted

技术标签:

【中文标题】如何从 MIDI 序列中获取 Note On/Off 消息?【英文标题】:How to get Note On/Off messages from a MIDI sequence? 【发布时间】:2015-01-16 15:34:10 【问题描述】:

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

下面的代码在播放 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】:

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

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

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;
    

【讨论】:

酷!做得很好。关于“选项 2”,它需要很多解释和代码,从一个混合器(在音频意义上,而不是 Java Mixer 对象)开始,它保持已处理帧的运行计数并管理一个事件队列。安德鲁,如果您想进一步讨论它,请告诉我。您的网站上有联系电子邮件地址吗?此外,也许做你正在做的事情不是“黑客”。让一个对象负责不同的功能可能更像是一种黑客行为。在明确处理提示的情况下,这种方式更好吗?我不能肯定地说。我边走边编。 “我边走边编。”我很熟悉那种感觉。 ;) 我不会更进一步,因为我很高兴坚持第一种方法。 (它不打算成为一个完美的产品,所以我可以很容易地做到这一点,IMO 很好 - 而且这种方法非常简单。)如果我能想出键盘的 MCVE,我会将它添加为编辑和通知你.. :) 整洁!看起来挺好的。我将为此代码添加书签以供将来参考。如果您好奇,这里有几个链接,这些链接指向我正在使用我的事件系统的项目。 java-gaming.org/user-generated-content/members/27722/… java-gaming.org/user-generated-content/members/27722/… 两者都在进行中。现在,我正在等待邮件中的组件来构建一台新计算机以更好地处理这项工作。也想用它做android开发。当前计算机是 2004 年的。 我想知道是否有办法让它与另一个项目一起工作。我在 1905 年使用 Erno Rapee 的“百科全书”为一部古老的无声电影写了一个钢琴乐谱:1905 年的“Rescued By Rover”。我即将把它放入midi(通过结局)。在电影滚动时看到钢琴键的图形可能会很整洁。您的工具也可以作为钢琴初学者的教学辅助工具。 哈哈!是的,情况很糟糕。我真的在这里捏便士。一个好的方面:它迫使我多次修改音频库,以便真正高效地处理,以便我的计算机可以在运行游戏图形的同时播放声音(全部动态合成)。【参考方案2】:

我会观察是否有比我的两个建议更好的答案,这显然不太理想。

    编辑 midi 文件以包含与现有按键开/关匹配的元事件 编写你自己的事件系统

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

【讨论】:

嗯,第一个似乎是一个完整的黑客,但给予它应有的信任,它似乎工作。请参阅下面的实现。 ..我不知道如何处理第二个建议。 “我会关注是否有更好的答案..”如果有,可能会有赏金。 另见MIDI Pianola 基于下面的代码,包括 78 个(简单的)MIDI 轨道。 :) 不过,我不确定我是否有got the octaves right。 ;)【参考方案3】:

答案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");
            
        
        ...
    

【讨论】:

以上是关于如何从 MIDI 序列中获取 Note On/Off 消息?的主要内容,如果未能解决你的问题,请参考以下文章