When does the Sequence from midi.Sequencer ends?

364 views Asked by At

Having this simple code which works (but just never ends until I terminated it with ^-c):

import javax.sound.midi.*;

public class Foo {

    public void play(int instrument, int note) {
        try {
            Sequencer player = MidiSystem.getSequencer();
            player.open();

            Sequence seq = new Sequence(Sequence.PPQ, 4);
            Track track = seq.createTrack();

            ShortMessage first = new ShortMessage();
            first.setMessage(192, 1, instrument, 0);
            MidiEvent changeInstrument = new MidiEvent(first, 1);
            track.add(changeInstrument);

            ShortMessage a = new ShortMessage();
            a.setMessage(144, 1, note, 100);
            MidiEvent noteOn = new MidiEvent(a, 1); //fired on "tick 1"
            track.add(noteOn);

            ShortMessage b = new ShortMessage();
            b.setMessage(128, 1, note, 100);
            MidiEvent noteOff = new MidiEvent(b, 8); //fired on "tick 8"
            track.add(noteOff);

            player.setSequence(seq);
            player.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.play(0, 42); 
    }
}

The noteOn MidiEvent is fired on 1 tick (second argument to MidiEvent constructor). The noteOff MidiEvent is fired on 8 tick, which means. there should be 7 ticks span. But I can never see the end (end of program). When triggered from cmd, the prompt never returns back unless I forced the process to terminate (with <ctrl-c>). Why is that? When is the end of Sequencer and thus end of the program?

EDIT: It could be caused be opening the Sequencer, but not closing it. So I wanted to close it at the end, but it cannot follow immediately the player.start() statement, otherwise it immediately ends. So there must be some delay. It is clunky solution though

        ...
        player.setSequence(seq);
        player.start();
        TimeUnit.SECONDS.sleep(3);
        player.close();

After which, the prompt gets back. But is there a better solution?

2

There are 2 answers

1
DevilsHnd - 退した On

The Midi sequence is finished when Sequencer#isRunning() equals false. I think you should close the sequencer object before starting another sequence. Declaring the Sequence object as a class member can give a bit more flexability:

// Declare Sequencer as a class member variable and initialize to null.
javax.sound.midi.Sequencer sequencer = null;

public void playMidi(String midiFilePath) {
    // Prevent a new Midi audio file is one is already playing.
    if (sequencer != null) {
        // If sequncer is running get outta here.
        if (sequencer.isRunning()) { 
            return; 
        }
        // Otherwise close the sequencer.
        else { 
            sequencer.close(); 
        }
    }
    // Play the Midi file in its own thread.
    Thread newThread = new Thread(() -> {
        try {
            // Obtains the default Sequencer connected to the System's default device.
            sequencer = javax.sound.midi.MidiSystem.getSequencer();

            // Opens the device, indicating that it should now acquire any
            // system resources it needs and becomes operational.
            if (sequencer.isRunning()) {
                return;
            }
            sequencer.open();

            // Stream in a Midi file
            java.io.InputStream is = new java.io.BufferedInputStream(new java.io.FileInputStream(new File(midiFilePath)));

            // Sets the current sequence on which the sequencer operates.
            // The stream must point to MIDI file data.
            sequencer.setSequence(is);
            is.close();
            // Starts playback of the MIDI data in the currently loaded sequence.
            sequencer.start();
        }
        catch (MidiUnavailableException | IOException | InvalidMidiDataException ex) {
            Logger.getLogger("playMidi() Method Error!").log(Level.SEVERE, null, ex);
        }
    });
    newThread.start();
}
0
jjazzboss On

You need to add a MetaEventListener to catch the "end of sequence" event:

  player.addMetaEventListener(metaEvent ->
        {
            if (metaEvent.getType() == 47) // end of sequence
            {
                player.stop();  // Not mandatory according to API doc, but better to have it

                // This is called from the Sequencer thread, NOT from the EDT
                // So we need SwingUtilities if we want to update the UI
                Runnable doRun= new Runnable()
                {
                    @Override
                    public void run()
                    {
                        doSomethingAfterStop();
                    }
                };
                SwingUtilities.invokeLater(doRun);
            }
        });

If you just want to exit your program when sequence is over, just call directly System.exit() in the listener.

Note that if you use Sequencer.setLoopCount(x) with x>0, the end-of-sequence listener is never called, you'll need a UI button (or a timer) to trigger the call to Sequencer.stop().

There are a few tricks to know when using the Java sequencer, if you need example code check out my application JJazzLab-X on GitHub, especially MusicController.java in the MusicController module.