Using MIDI in Java

1.1k views Asked by At

I was experimenting trying to write a Java program that uses MIDI, so the program has to array: notes (which contains all the notes that I want to play) and another array times (which specify when the note should be play) the notes and times are grouped three at a time so I can have multiple chords, the problem is that the program only plays a very brief note and then it stops, what am I doing wrong? Bellow is the code, I am using Java 16.

package application;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;

public class App {

    public static void main(String[] args)
            throws MidiUnavailableException, InvalidMidiDataException, InterruptedException {

        var receiver = MidiSystem.getReceiver();

        int[] notes = { 60, 64, 67, 60, 65, 67, 55, 59, 62, 55, 60, 62, 53, 57, 60, 53, 58, 60 };
        int[] times = { 0, 0, 0, 1000, 1000, 1000, 2000, 2000, 2000, 3000, 3000, 3000, 4000, 4000, 4000, 5000, 5000,
                5000 };

        for (int i = 0; i < notes.length; i++) {

            int note = notes[i];
            int time = times[i];
            System.out.println(note + ":" + time);
            receiver.send(new ShortMessage(ShortMessage.NOTE_ON, 0, note, 127), time * 1000);
            receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, note, 127), (time + 1000) * 1000);
        }

        Thread.sleep(7000);
    }

}

I think it has something to do with ShortMessage.NOTE_OFF but I am not sure and I can't figure it out.

3

There are 3 answers

6
Toerktumlare On BEST ANSWER

The problem has most likely to do with your times array.

Taken from the oracle java documentation on sequancers

As previously mentioned, the program can include a time stamp with each MIDI message it sends to the device's receiver. However, such time stamps are used only for fine-tuning the timing, to correct for processing latency. The caller can't generally set arbitrary time stamps; the time value passed to Receiver.send must be close to the present time, or the receiving device might not be able to schedule the message correctly. This means that if an application program wanted to create a queue of MIDI messages for an entire piece of music ahead of time (instead of creating each message in response to a real-time event), it would have to be very careful to schedule each invocation of Receiver.send for nearly the right time.

Fortunately, most application programs don't have to be concerned with such scheduling. Instead of invoking Receiver.send itself, a program can use a Sequencer object to manage the queue of MIDI messages for it.

You are blindly assuming that the numbers (1000, 2000, 3000... etc.) is milliseconds, but defined in the api this is defined as ticks.

Like in any music you need to define the subdivisions, and the pulse per ticks (PPQ) for instance quarter notes, 16th notes etc. etc.

In java this is usually done by defining a sequencer, and with these settings, and then creating a track on said sequencer, and then playing notes on that track.

Here is an example i found on the internet.

public class MyMidiPlayer {
 
    public static void main(String[] args) {
 
        System.out.println("Enter the number of notes to be played: ");
        Scanner in = new Scanner(System.in);
        int numOfNotes = in.nextInt();
 
        MyMidiPlayer player = new MyMidiPlayer();
        player.setUpPlayer(numOfNotes);
    }
 
    public void setUpPlayer(int numOfNotes) {
 
        try {
 
            // A static method of MidiSystem that returns
            // a sequencer instance.
            Sequencer sequencer = MidiSystem.getSequencer();
            sequencer.open();
 
            // Creating a sequence.
            Sequence sequence = new Sequence(Sequence.PPQ, 4);
 
            // PPQ(Pulse per ticks) is used to specify timing
            // type and 4 is the timing resolution.
 
            // Creating a track on our sequence upon which
            // MIDI events would be placed
            Track track = sequence.createTrack();
 
                // Adding some events to the track
            for (int i = 5; i < (4 * numOfNotes) + 5; i += 4){
 
                // Add Note On event
                track.add(makeEvent(144, 1, i, 100, i));
 
                // Add Note Off event
                track.add(makeEvent(128, 1, i, 100, i + 2));
            }
 
            // Setting our sequence so that the sequencer can
            // run it on synthesizer
            sequencer.setSequence(sequence);
 
            // Specifies the beat rate in beats per minute.
            sequencer.setTempoInBPM(220);
 
            // Sequencer starts to play notes
            sequencer.start();
 
            while (true) {
 
                // Exit the program when sequencer has stopped playing.
                if (!sequencer.isRunning()) {
                    sequencer.close();
                    System.exit(1);
                }
            }
        }
        catch (Exception ex) {
 
            ex.printStackTrace();
        }
    }
 
    public MidiEvent makeEvent(int command, int channel,
                               int note, int velocity, int tick) {
 
        MidiEvent event = null;
 
        try {
 
            // ShortMessage stores a note as command type, channel,
            // instrument it has to be played on and its speed.
            ShortMessage a = new ShortMessage();
            a.setMessage(command, channel, note, velocity);
 
            // A midi event is comprised of a short message(representing
            // a note) and the tick at which that note has to be played
            event = new MidiEvent(a, tick);
        }
        catch (Exception ex) {
 
            ex.printStackTrace();
        }
        return event;
    }
}
0
Thaumatrope On

Yes, this code doesn't work, as you can see via the method device.getMicrosecondPosition()

If it returns -1 the TimeStamp isn't supported. But you can solve this via a different algorithm as shown below.

package application;

import java.io.IOException;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;

public class App {

    public static void main(String[] args)
            throws MidiUnavailableException, InvalidMidiDataException, InterruptedException, IOException {
        // TODO Auto-generated method stub
        
        MidiDevice device  = null;
        MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
        for (int i = 0; i < infos.length; i++) {
            try {
                device = MidiSystem.getMidiDevice(infos[i]);
                System.out.println("Supports Timestamp: " + device.toString()+ " / " + device.getMicrosecondPosition());
                System.out.println(infos[i]);
            } catch (MidiUnavailableException e) {
                  // Handle or throw exception...
            }              
        }

        Receiver receiver = MidiSystem.getReceiver();


        int[] notes = { 60, 64, 67, 60, 65, 67, 55, 59, 62, 55, 60, 62, 53, 57, 60, 53, 58, 60 };
        long[] times = { 0, 0, 0, 1000, 1000, 1000, 2000, 2000, 2000, 3000, 3000, 3000, 4000, 4000, 4000, 5000, 5000, 5000};

        for (int i = 0; i < notes.length; i++) {
            int note = notes[i];
            long time = times[i];

            if((i%3 == 0) && (i > 0)){
                Thread.sleep(1000);
                System.out.println("i is: " + i);
                receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[i-3], 127), -1); // -1 instantly
                receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[i-2], 127), -1); // -1 instantly
                receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[i-1], 127), -1); // -1 instantly
            }
            receiver.send(new ShortMessage(ShortMessage.NOTE_ON, 0, note, 127), -1); // -1 instantly
//          receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, note, 127), -1); // -1 instantly
            System.out.println("Played: " + note + " / at: " + time);
        }
        Thread.sleep(3000);
        receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[notes.length-3], 127), -1); // -1 instantly
        receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[notes.length-2], 127), -1); // -1 instantly
        receiver.send(new ShortMessage(ShortMessage.NOTE_OFF, 0, notes[notes.length-1], 127), -1); // -1 instantly
        Thread.sleep(10000);

    }

}
0
Thaumatrope On

And here is a version with a correct Sequencer. Split up in its own class for reuse or studying the concept. (Oracle Tutorials helped me a lot)

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiUnavailableException;

public class App {

    public static void main(String[] args)
            throws MidiUnavailableException, InvalidMidiDataException, InterruptedException, IOException {
        // TODO Auto-generated method stub

        int[] notes = { 60, 64, 67, 60, 65, 67, 55, 59, 62, 55, 60, 62, 53, 57, 60, 53, 58, 60 };
        long[] times = { 0, 0, 0, 1000, 1000, 1000, 2000, 2000, 2000, 3000, 3000, 3000, 4000, 4000, 4000, 5000, 5000, 5000};

        PlayMidi.playMidi(notes, times);

    }
}

package application;

import java.io.IOException;
import java.net.URL;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;

public class PlayMidi {

    public static void playMidiSong() throws InvalidMidiDataException, IOException, MidiUnavailableException {

        URL url = new URL("http://www.vgmusic.com/music/computer/microsoft/windows/touhou_6_stage3_boss.mid");

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

        sequencer.open();
        sequencer.setSequence(sequence);

        sequencer.start();

        // Thread.sleep(8000);

    }

    public static void playMidi(int[] notes, long[] times) {
            
        try {
             
            // A static method of MidiSystem that returns
            // a sequencer instance.
            Sequencer sequencer = MidiSystem.getSequencer();
            sequencer.open();
            System.out.println("Sequencer is open.");
            
            int bpm = 220;
            int resolution = 4;
 
            // Creating a sequence.
            Sequence sequence = new Sequence(Sequence.PPQ, resolution);
 
            // PPQ(Pulse per ticks) is used to specify timing
            // type and 4 is the timing resolution.
            
            double ticksPerSecond = resolution * (bpm / 60.0);
            double tickSize = 1.0 / ticksPerSecond;
            
 
            // Creating a track on our sequence upon which
            // MIDI events would be placed
            Track track = sequence.createTrack();
 
            // Adding some events to the track
            for (int i = 0; i < notes.length; i++) {
 
                double timeFactor = (times[i]/1000);
                // Add Note On event
                track.add(makeEvent(ShortMessage.NOTE_ON, 1, notes[i], 120, ticksPerSecond * timeFactor));  //ShortMessage.NOTE_ON 144
     
                // Add Note Off event
                track.add(makeEvent(ShortMessage.NOTE_OFF, 1, notes[i], 120, (ticksPerSecond * timeFactor) + ticksPerSecond)); //ShortMessage.NOTE_OFF 128
            }
 
            // Setting our sequence so that the sequencer can
            // run it on synthesizer
            sequencer.setSequence(sequence);
 
            // Specifies the beat rate in beats per minute.
            sequencer.setTempoInBPM(bpm);
 
            // Sequencer starts to play notes
            sequencer.start();
 
            while (true) {
 
                // Exit the program when sequencer has stopped playing.
                if (!sequencer.isRunning()) {
                    sequencer.close();
                    System.out.println("Sequencer is closed.");
                    System.exit(1);
                }
            }
        }
        catch (Exception ex) {
 
            ex.printStackTrace();
        }
        
        
        
    }

    public static MidiEvent makeEvent(int command, int channel, int note, int velocity, double tick) {

        MidiEvent event = null;

        try {

            // ShortMessage stores a note as command type, channel,
            // instrument it has to be played on and its speed.
            ShortMessage a = new ShortMessage();
            a.setMessage(command, channel, note, velocity);

            // A midi event is comprised of a short message(representing
            // a note) and the tick at which that note has to be played
            event = new MidiEvent(a, (int)tick);
        } catch (Exception ex) {

            ex.printStackTrace();
        }
        return event;
    }

}