如何在 Java 中选择特定的 Midi Sequencer?

问题描述

我正在开发一个 MIDI 程序,并且希望用户可以选择使用哪个 MIDI 音序器,如果他们有很多而不是使用 Midisystem.getSequencer()

编辑

我的代码看起来像这样。

public class Demo {

  public static void main(String[] args) {
    Sequencer sequencer;

    //Gets default sequencer if only one argument given
    try {
      sequencer = Midisystem.getSequencer();
    } catch (MidiUnavailableException e) {
      e.printstacktrace();
      return;
    }

    if (args.length == 0) {
      return;
    }

    File file = new File(args[0]);

    boolean select = args.length > 1;

    MidiDevice.Info[] infos = Midisystem.getMidiDeviceInfo();
    List<String> sequencers = new ArrayList<>();
    MidiDevice device;

    //Populates sequencers lists with potential values and prints
    for (int i = 0; i < infos.length; i++) {
      try {
        device = Midisystem.getMidiDevice(infos[i]);
        if (device instanceof Sequencer) {
          System.out.println(sequencers.size() + ": " + device.getDeviceInfo().getName());
          sequencers.add(device.getDeviceInfo().getName());
        }
      } catch (MidiUnavailableException e) {
        System.out.println(e.getMessage());
      }
    }

    String name;

    //If multiple arguments are given select a new sequencer from the list
    if (select) {
      Scanner scanner = new Scanner(system.in);
      System.out.println("Enter the number of the sequencer you wish to use");
      name = sequencers.get(scanner.nextInt());

      for (int i = 0; i < infos.length; i++) {
        try {
          device = Midisystem.getMidiDevice(infos[i]);

          if (device.getDeviceInfo().getName().equals(name)) {
            //Todo: This line does not create a valid sequencer
            sequencer = (Sequencer) device;
            System.out.println("Sequencer changed to " + device.getDeviceInfo().getName());
          }
        } catch (MidiUnavailableException e) {
          System.out.println("Cannot locate device " + name);
        }
      }
    }

    //Attempt to play midi data from a file into selected sequencer
    if (sequencer != null) {
      if (!sequencer.isopen()) {
        try {
          sequencer.open();
        } catch (MidiUnavailableException e) {
          e.printstacktrace();
        }
      }
      try {
        sequencer.setSequence(Midisystem.getSequence(file));
      } catch (InvalidMidiDataException | IOException e) {
        e.printstacktrace();
        return;
      }

      System.out.println("Attempting to play Midi");
      sequencer.start();
    }
  }
  
} 

如果您使用一个指向midi文件的参数运行程序,它会播放它,但如果您有2个参数并且它要求您选择一个音序器,则它是静音的。音序器肯定是在打印语句发生时设置的,并且程序不会像 MIDI 仍在播放一样立即退出,但不会发出噪音。

基于 GUI 的 MRE

此 MRE 将命令行应用程序转换为 GUI,以便更易于使用。

要测试此代码

  1. 启动 GUI
  2. 确保文本字段(顶部)指向有效的 MIDI 文件
  3. 在下面的列表中选择一个音序器
  4. 单击文本字段以使其成为焦点
  5. 激活添加到字段中的动作侦听器(在 Windows 上,这意味着“按回车”)

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import java.util.*;
import javax.sound.midi.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class Midisequencers {

    private JComponent ui = null;
    Vector<Sequencer> sequencers = new Vector<>();
    public static String URLString = "https://bitmidi.com/uploads/18908.mid";
        // Used on my local system,given the hot-link had problems
        //"file:/C:/Users/Andrew/Downloads/Queen%20-%20Bohemian%20Rhapsody.mid";
    URL url;
    JList sequencerList;

    Midisequencers() {
        try {
            initUI();
        } catch (Exception ex) {
            ex.printstacktrace();
        }
    }

    public final void initUI() throws Exception {
        if (ui != null) {
            return;
        }

        ui = new JPanel(new BorderLayout(4,4));
        ui.setBorder(new EmptyBorder(4,4,4));
        
        populateSequencers();
        sequencerList = new JList(sequencers);
        ui.add(new JScrollPane(sequencerList));
        
        JTextField textField = new JTextField(URLString,10);
        ui.add(textField,BorderLayout.PAGE_START);
        
        ActionListener playListener = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                Sequencer sequencer = (Sequencer)sequencerList.getSelectedValue();
                playSequence(sequencer);
            }
        };
        textField.addActionListener(playListener);
    }

    private void populateSequencers() throws Exception {
        Sequencer sequencer;

        //Gets default sequencer if only one argument given
        sequencer = Midisystem.getSequencer();
        sequencers.add(sequencer);

        url = new URL(URLString);

        MidiDevice.Info[] infos = Midisystem.getMidiDeviceInfo();
        MidiDevice device;

        // Populates sequencers list with available sequencers
        for (int i = 0; i < infos.length; i++) {
            try {
                device = Midisystem.getMidiDevice(infos[i]);
                if (device instanceof Sequencer) {
                    System.out.println(sequencers.size() + ": " + device.getDeviceInfo().getName());
                    sequencers.add((Sequencer) device);
                }
            } catch (MidiUnavailableException e) {
                e.printstacktrace();
            }
        }
    }

    private void playSequence(Sequencer sequencer) {
        if (!sequencer.isopen()) {
            try {
                sequencer.open();
            } catch (MidiUnavailableException e) {
                e.printstacktrace();
            }
        }
        try {
            sequencer.setSequence(Midisystem.getSequence(url));
        } catch (Exception e) {
            e.printstacktrace();
            return;
        }

        System.out.println("Attempting to play Midi");
        sequencer.start();
    }

    public JComponent getUI() {
        return ui;
    }

    public static void main(String[] args) {
        Runnable r = () -> {
            try {
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception useDefault) {
            }
            Midisequencers o = new Midisequencers();

            JFrame f = new JFrame(o.getClass().getSimpleName());
            f.setDefaultCloSEOperation(JFrame.EXIT_ON_CLOSE);
            f.setLocationByPlatform(true);

            f.setContentPane(o.getUI());
            f.pack();
            f.setMinimumSize(f.getSize());

            f.setVisible(true);
        };
        SwingUtilities.invokelater(r);
    }
}

解决方法

TLDR:让用户选择定序器实现是没有意义的,因为如果您的程序使用标准 JDK,则有 99.99999% 的可能性只有 1 个定序器实现可用。

现在解释程序中的错误。

1/ 音序器需要连接到 Midi 输出设备或合成器才能产生声音。

当您使用MidiSystem.getSequencer()时,它相当于MidiSystem.getSequencer(true)(参见Javadoc),这意味着返回的音序器会自动连接到默认的合成器。这就是您在此实例中听到声音的原因。

您获得的第二个 Sequencer 实例是通过 MidiSystem.getMidiDevice() 获取的,因此您需要手动进行连接(使用 Transmitter/Receiver)。

2/ 您的代码无意中创建了 2 个相同的 Sequencer 实例!

当您调用 MidiSystem.getMidiDeviceInfo() 时,您获得的 MidiDevice.Info 之一是来自使用 MidiSystem.getSequencer() 创建的 Sequencer 实例的信息。因此,当您调用 MidiSystem.getDevice(info) 时,您会使用相同的 Sequencer 实现(从 Java 5 开始实际上是 RealTimeSequencer)生成第二个实例。

错误修复

通过将 MidiSystem.getSequencer() 中的 MidiSystem.getSequencer(false) 替换为 populateSequencers(),我能够使您的程序与两个音序器实例一起工作,

并在 sequencer.setSequence() 中的 playSequence() 之后添加下面的连接代码:

        Synthesizer synth = MidiSystem.getSynthesizer();            
        synth.open();
        Transmitter t = sequencer.getTransmitter();
        t.setReceiver(synth.getReceiver());

多年来,我在 Java 应用程序中看到过这样一个也是唯一的案例(让用户选择 Sequencer),它是在 TuxGuitar 中,因为 TuxGuitar 包含自己的实时 Sequencer 实现,除了 JDK 的一个(它在 JDK1.4 之前很糟糕,但从 1.5 开始它是可靠的)。