How to select specific Midi Sequencer in Java?

490 Views Asked by At

I'm working on a midi program and want the user to have the option to select which midi sequencer is used if they have many instead of using MidiSystem.getSequencer().

EDIT

My code looks like this.

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();
    }
  }
  
} 

If you run the program with one argument pointing to a midi file it plays it, but if you have 2 arguments and it asks you to select a sequencer, it is silent. The sequencer is definitely set as the print statement happens and the program doesn't exit immediately as if a midi is still playing, but no noise comes out.

GUI based MRE

This MRE converted the command line app to a GUI to be easier to work with.

To test this code:

  1. Launch the GUI
  2. Ensure the text field (at top) points to a valid MIDI file
  3. Select one of the sequencers in the list below
  4. Click in the text field to give it focus
  5. Activate the action listener added to the field (on Windows that means 'hit enter')

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, 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);
    }
}
1

There are 1 best solutions below

4
jjazzboss On

TLDR: there is no point in letting users choose the sequencer implementation, because if your program uses a standard JDK, there is 99.99999% chances there is only 1 Sequencer implementation available.

Now the explanations of the errors in your program.

1/ A Sequencer needs to be connected to a Midi output device or a synth in order to produce sound.

When you use MidiSystem.getSequencer(), it is equivalent to MidiSystem.getSequencer(true) (see Javadoc), which means the returned sequencer is automatically connected to the default Synthesizer. This is why you hear sound with this instance.

The 2nd Sequencer instance you get is obtained via MidiSystem.getMidiDevice(), so you need to do the connection manually (using Transmitter/Receiver).

2/ Your code has unintentionally created 2 identical Sequencer instances!

When you call MidiSystem.getMidiDeviceInfo(), one of the MidiDevice.Info you get is the Info from the Sequencer instance created using MidiSystem.getSequencer(). So when you call MidiSystem.getDevice(info), you generate a second instance using the same Sequencer implementation (which is actually RealTimeSequencer since Java 5).

Bug fix

I was able to make your program work with both sequencer instances by replacing MidiSystem.getSequencer() by MidiSystem.getSequencer(false) in populateSequencers(),

and add the connection code below, after sequencer.setSequence() in playSequence() :

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

In many years the one and only case I've seen such a similar feature (let user choose Sequencer) in a Java application, it was in TuxGuitar, because TuxGuitar included its own realtime Sequencer implementation, besides the JDK's one (which was awful until JDK1.4, but since 1.5 it's reliable).