6.031
6.031 — Software Construction
Spring 2021

Code for Reading 27

Download

You can also download a ZIP file containing this code.

Seems to be necessary to have a triple-backtick block at the start of this page,
otherwise all the pre/code tags below aren't syntax highlighted.
So create a hidden one.

Pitch.java

package music;

/**
 * Pitch is an immutable type representing the frequency of a musical note.
 * Standard music notation represents pitches by letters: A, B, C, ..., G.
 * Pitches can be sharp or flat, or whole octaves up or down from these
 * primitive generators.
 * 
 * <p> For example:
 * <br> new Pitch('C') makes middle C
 * <br> new Pitch('C').transpose(1) makes C-sharp
 * <br> new Pitch('E').transpose(-1) makes E-flat
 * <br> new Pitch('C').transpose(OCTAVE) makes high C
 * <br> new Pitch('C').transpose(-OCTAVE) makes low C
 */
public class Pitch {

    private final int value;

    /*
     * Rep invariant: true.
     *
     * Abstraction function AF(value):
     *   AF(0),...,AF(11) map to middle C, C-sharp, D, ..., A, A-sharp, B.
     *   AF(i+12n) maps to n octaves above middle AF(i)
     *   AF(i-12n) maps to n octaves below middle AF(i)
     *
     * Safety from rep exposure:
     *   all fields are private and immutable.
     */

    private static final int[] SCALE = {
            9,  // A
            11, // B
            0,  // C
            2,  // D
            4,  // E
            5,  // F
            7,  // G
    };

    private static final String[] VALUE_TO_STRING = {
            "C", "^C", "D", "^D", "E", "F", "^F", "G", "^G", "A", "^A", "B"
    };
    
    /**
     * Middle C.
     */
    public static final Pitch MIDDLE_C = new Pitch('C');

    /**
     * Number of pitches in an octave.
     */
    public static final int OCTAVE = 12;

    private Pitch(int value) {
        this.value = value;
    }

    /**
     * Make a Pitch named c in the middle octave of the piano keyboard.
     * For example, new Pitch('C') constructs middle C.
     * @param c letter in {'A',...,'G'}
     */
    public Pitch(char c) {
        try {
            value = SCALE[c-'A'];
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IllegalArgumentException(c + " must be in the range A-G", e);
        }
    }

    /**
     * @param semitonesUp 
     * @return pitch made by transposing this pitch by semitonesUp semitones;
     *         for example, middle C transposed by 12 semitones is high C, and
     *         E transposed by -1 semitones is E flat
     */
    public Pitch transpose(int semitonesUp) {
        return new Pitch(value + semitonesUp);
    }

    /**
     * @param that 
     * @return number of semitones between this and that; i.e., n such that
     *         that.transpose(n).equals(this)
     */
    public int difference(Pitch that) {
        return this.value - that.value;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (obj.getClass() != this.getClass()) return false;
        
        Pitch that = (Pitch) obj;
        return this.value == that.value;
    }

    @Override
    public int hashCode() {
        return value;
    }

    /**
     * @return this pitch in abc music notation
     * @see "http://abcnotation.com/examples/"
     */
    @Override
    public String toString() {
        String suffix = "";
        int v = value;

        while (v < 0) {
            suffix += ",";
            v += OCTAVE;
        }

        while (v >= OCTAVE) {
            suffix += "'";
            v -= OCTAVE;
        }

        return VALUE_TO_STRING[v] + suffix;
    }
}

Instrument.java

package music;

/**
 * Instrument represents a musical instrument.
 * 
 * These instruments are the 128 standard General MIDI Level 1 instruments.
 * See: http://www.midi.org/about-midi/gm/gm1sound.shtml
 */
public enum Instrument {
    // Order is important in this enumeration because an instrument's
    // position must correspond to its MIDI program number.

    PIANO,
    BRIGHT_PIANO,
    ELECTRIC_GRAND,
    HONKY_TONK_PIANO,
    ELECTRIC_PIANO_1,
    ELECTRIC_PIANO_2,
    HARPSICHORD,
    CLAVINET,

    CELESTA,
    GLOCKENSPIEL,
    MUSIC_BOX,
    VIBRAPHONE,
    MARIMBA,
    XYLOPHONE,
    TUBULAR_BELL,
    DULCIMER,

    HAMMOND_ORGAN,
    PERC_ORGAN,
    ROCK_ORGAN,
    CHURCH_ORGAN,
    REED_ORGAN,
    ACCORDION,
    HARMONICA,
    TANGO_ACCORDION,

    NYLON_STR_GUITAR,
    STEEL_STRING_GUITAR,
    JAZZ_ELECTRIC_GTR,
    CLEAN_GUITAR,
    MUTED_GUITAR,
    OVERDRIVE_GUITAR,
    DISTORTION_GUITAR,
    GUITAR_HARMONICS,

    ACOUSTIC_BASS,
    FINGERED_BASS,
    PICKED_BASS,
    FRETLESS_BASS,
    SLAP_BASS_1,
    SLAP_BASS_2,
    SYN_BASS_1,
    SYN_BASS_2,

    VIOLIN,
    VIOLA,
    CELLO,
    CONTRABASS,
    TREMOLO_STRINGS,
    PIZZICATO_STRINGS,
    ORCHESTRAL_HARP,
    TIMPANI,

    ENSEMBLE_STRINGS,
    SLOW_STRINGS,
    SYNTH_STRINGS_1,
    SYNTH_STRINGS_2,
    CHOIR_AAHS,
    VOICE_OOHS,
    SYN_CHOIR,
    ORCHESTRA_HIT,

    TRUMPET,
    TROMBONE,
    TUBA,
    MUTED_TRUMPET,
    FRENCH_HORN,
    BRASS_ENSEMBLE,
    SYN_BRASS_1,
    SYN_BRASS_2,

    SOPRANO_SAX,
    ALTO_SAX,
    TENOR_SAX,
    BARITONE_SAX,
    OBOE,
    ENGLISH_HORN,
    BASSOON,
    CLARINET,

    PICCOLO,
    FLUTE,
    RECORDER,
    PAN_FLUTE,
    BOTTLE_BLOW,
    SHAKUHACHI,
    WHISTLE,
    OCARINA,

    SYN_SQUARE_WAVE,
    SYN_SAW_WAVE,
    SYN_CALLIOPE,
    SYN_CHIFF,
    SYN_CHARANG,
    SYN_VOICE,
    SYN_FIFTHS_SAW,
    SYN_BRASS_AND_LEAD,

    FANTASIA,
    WARM_PAD,
    POLYSYNTH,
    SPACE_VOX,
    BOWED_GLASS,
    METAL_PAD,
    HALO_PAD,
    SWEEP_PAD,

    ICE_RAIN,
    SOUNDTRACK,
    CRYSTAL,
    ATMOSPHERE,
    BRIGHTNESS,
    GOBLINS,
    ECHO_DROPS,
    SCI_FI,

    SITAR,
    BANJO,
    SHAMISEN,
    KOTO,
    KALIMBA,
    BAG_PIPE,
    FIDDLE,
    SHANAI,

    TINKLE_BELL,
    AGOGO,
    STEEL_DRUMS,
    WOODBLOCK,
    TAIKO_DRUM,
    MELODIC_TOM,
    SYN_DRUM,
    REVERSE_CYMBAL,

    GUITAR_FRET_NOISE,
    BREATH_NOISE,
    SEASHORE,
    BIRD,
    TELEPHONE,
    HELICOPTER,
    APPLAUSE,
    GUNSHOT,
}

Music.java

package music;

/**
 * Music represents a piece of music played by multiple instruments.
 */
public interface Music {

    /**
     * @return total duration of this piece in beats
     */
    double duration();

    /**
     * Play this piece.
     * @param player player to play on
     * @param atBeat when to play
     */
    void play(SequencePlayer player, double atBeat);

}

MusicLanguage.java

package music;

import static music.Pitch.OCTAVE;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * MusicLanguage defines static methods for constructing and manipulating Music expressions.
 */
public class MusicLanguage {

    /**
     * Prevent instantiation but allow subclassing.
     */
    protected MusicLanguage() {}

    ////////////////////////////////////////////////////
    // Factory methods
    ////////////////////////////////////////////////////

    /**
     * Make Music from a string using a variant of abc notation
     *    (see http://abcnotation.com/examples/).
     * 
     * <p> The notation consists of whitespace-delimited symbols representing
     * either notes or rests. The vertical bar | may be used as a delimiter
     * for measures; notes() treats it as a space.
     * Grammar:
     * <pre>
     *     notes ::= symbol*
     *     symbol ::= . duration          // for a rest
     *              | pitch duration      // for a note
     *     pitch ::= accidental letter octave*
     *     accidental ::= empty string    // for natural,
     *                  | _               // for flat,
     *                  | ^               // for sharp
     *     letter ::= [A-G]
     *     octave ::= '                   // to raise one octave
     *              | ,                   // to lower one octave
     *     duration ::= empty string      // for 1-beat duration
     *                | /n                // for 1/n-beat duration
     *                | n                 // for n-beat duration
     *                | n/m               // for n/m-beat duration
     * </pre>
     * <p> Examples (assuming 4/4 common time, i.e. 4 beats per measure):
     *     C     quarter note, middle C
     *     A'2   half note, high A
     *     _D/2  eighth note, middle D flat
     * 
     * @param notes string of notes and rests in simplified abc notation given above
     * @param instr instrument to play the notes with
     * @return the music in notes played by instr
     */
    public static Music notes(String notes, Instrument instr) {
        Music music = rest(0);
        for (String sym : notes.split("[\\s|]+")) {
            if (!sym.isEmpty()) {
                music = concat(music, parseSymbol(sym, instr));
            }
        }
        return music;
    }

    /* Parse a symbol into a Note or a Rest. */
    private static Music parseSymbol(String symbol, Instrument instr) {
        Matcher m = Pattern.compile("(?<pitch>[^/0-9]*)(?<numerator>[0-9]+)?(?<denominator>/[0-9]+)?").matcher(symbol);
        
        if (!m.matches()) throw new IllegalArgumentException("couldn't understand " + symbol);

        String pitchSymbol = m.group("pitch");

        double duration = 1.0;
        if (m.group("numerator") != null) duration *= Integer.valueOf(m.group("numerator"));
        if (m.group("denominator") != null) duration /= Integer.valueOf(m.group("denominator").substring(1));

        if (pitchSymbol.equals(".")) return rest(duration);
        else return note(duration, parsePitch(pitchSymbol), instr);
    }

    /* Parse a symbol into a Pitch. */
    private static Pitch parsePitch(String symbol) {
        if (symbol.endsWith("'"))
            return parsePitch(symbol.substring(0, symbol.length()-1)).transpose(OCTAVE);
        else if (symbol.endsWith(","))
            return parsePitch(symbol.substring(0, symbol.length()-1)).transpose(-OCTAVE);
        else if (symbol.startsWith("^"))
            return parsePitch(symbol.substring(1)).transpose(1);
        else if (symbol.startsWith("_"))
            return parsePitch(symbol.substring(1)).transpose(-1);
        else if (symbol.length() != 1)
            throw new IllegalArgumentException("can't understand " + symbol);
        else
            return new Pitch(symbol.charAt(0));
    }

    /**
     * @param duration duration in beats, must be >= 0
     * @param pitch pitch to play
     * @param instrument instrument to use
     * @return pitch played by instrument for duration beats
     */
    public static Music note(double duration, Pitch pitch, Instrument instrument) {
        return new Note(duration, pitch, instrument);
    }

    /**
     * @param duration duration in beats, must be >= 0
     * @return rest that lasts for duration beats
     */
    public static Music rest(double duration) {
        return new Rest(duration);
    }

    ////////////////////////////////////////////////////
    // Producers
    ////////////////////////////////////////////////////

    /**
     * @param m1 first piece of music
     * @param m2 second piece of music
     * @return m1 followed by m2
     */
    public static Music concat(Music m1, Music m2) {
        return new Concat(m1, m2);
    }
}

Note.java

package music;

/**
 * Note represents a note played by an instrument.
 */
public class Note implements Music {

    private final double duration;
    private final Pitch pitch;
    private final Instrument instrument;

    private void checkRep() {
        assert duration >= 0;
        assert pitch != null;
        assert instrument != null;
    }

    /**
     * Make a Note played by instrument for duration beats.
     * @param duration duration in beats, must be >= 0
     * @param pitch pitch to play
     * @param instrument instrument to use
     */
    public Note(double duration, Pitch pitch, Instrument instrument) {
        this.duration = duration;
        this.pitch = pitch;
        this.instrument = instrument;
        checkRep();
    }

    /**
     * @return pitch of this note
     */
    public Pitch pitch() {
        return pitch;
    }

    /**
     * @return instrument that should play this note
     */
    public Instrument instrument() {
        return instrument;
    }

    /**
     * @return duration of this note
     */
    @Override
    public double duration() {
        return duration;
    }

    /**
     * Play this note.
     */
    @Override
    public void play(SequencePlayer player, double atBeat) {
        player.addNote(instrument, pitch, atBeat, duration);
    }

    @Override
    public int hashCode() {
        long durationBits = Double.doubleToLongBits(duration);
        return (int) (durationBits ^ (durationBits >>> Integer.SIZE))
                + instrument.hashCode()
                + pitch.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        
        final Note other = (Note) obj;
        return duration == other.duration
                && instrument.equals(other.instrument)
                && pitch.equals(other.pitch);
    }

    @Override
    public String toString() {
        return pitch.toString() + duration;
    }
}

Rest.java

package music;

/**
 * Rest represents a pause in a piece of music.
 */
public class Rest implements Music {

    private final double duration;

    private void checkRep() {
        assert duration >= 0;
    }

    /**
     * Make a Rest that lasts for duration beats.
     * @param duration duration in beats, must be >= 0
     */
    public Rest(double duration) {
        this.duration = duration;
        checkRep();
    }

    /**
     * @return duration of this rest
     */
    @Override
    public double duration() {
        return duration;
    }

    /**
     * Play this rest.
     */
    @Override
    public void play(SequencePlayer player, double atBeat) {
        return;
    }

    @Override
    public int hashCode() {
        long durationBits = Double.doubleToLongBits(duration);
        return (int) (durationBits ^ (durationBits >>> Integer.SIZE));
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        
        final Rest other = (Rest) obj;
        return duration == other.duration;
    }

    @Override
    public String toString() {
        return "." + duration;
    }
}

Concat.java

package music;

/**
 * Concat represents two pieces of music played one after the other.
 */
public class Concat implements Music {

    private final Music first;
    private final Music second;

    private void checkRep() {
        assert first != null;
        assert second != null;
    }

    /**
     * Make a Music sequence that plays m1 followed by m2.
     * @param m1 music to play first
     * @param m2 music to play second
     */
    public Concat(Music m1, Music m2) {
        this.first = m1;
        this.second = m2;
        checkRep();
    }

    /**
     * @return first piece in this concatenation
     */
    public Music first() {
        return first;
    }

    /**
     * @return second piece in this concatenation
     */
    public Music second() {
        return second;
    }

    /**
     * @return duration of this concatenation
     */
    @Override
    public double duration() {
        return first.duration() + second.duration();
    }

    /**
     * Play this concatenation.
     */
    @Override
    public void play(SequencePlayer player, double atBeat) {
        first.play(player, atBeat);
        second.play(player, atBeat + first.duration());
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        return first.hashCode() + prime * second.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        
        final Concat other = (Concat) obj;
        return first.equals(other.first) && second.equals(other.second);
    }

    @Override
    public String toString() {
        return first + " " + second;
    }
}

ScaleSequence.java

package music.examples;

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

import music.Instrument;
import music.Pitch;
import music.SequencePlayer;
import music.midi.MidiSequencePlayer;

public class ScaleSequence {
    
    /**
     * Play an octave up and back down starting from middle C.
     * 
     * @param args not used
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
    */
    public static void main(String[] args) throws MidiUnavailableException, InvalidMidiDataException {

        Instrument piano = Instrument.PIANO;

        // create a new player
        final int beatsPerMinute = 120; // a beat is a quarter note, so this is 120 quarter notes per minute
        final int ticksPerBeat = 2;
        SequencePlayer player = new MidiSequencePlayer(beatsPerMinute, ticksPerBeat);

        // addNote(instr, pitch, startBeat, numBeats) schedules a note with pitch value 'pitch'
        // played by 'instr' starting at 'startBeat' to be played for 'numBeats' beats.
        
        int startBeat = 0;
        int numBeats = 1;
        player.addNote(piano, new Pitch('C'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('D'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('E'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('F'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('G'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('A'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('B'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('C').transpose(Pitch.OCTAVE), startBeat++, numBeats);
        player.addNote(piano, new Pitch('B'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('A'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('G'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('F'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('E'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('D'), startBeat++, numBeats);
        player.addNote(piano, new Pitch('C'), startBeat++, numBeats);

        System.out.println(player);

        // play!
        player.play();
    }
}

ScaleMusic.java

package music.examples;

import static music.Instrument.PIANO;
import static music.MusicLanguage.notes;

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

import music.Music;
import music.midi.MusicPlayer;

public class ScaleMusic {

    /**
     * Play an octave up and back down starting from middle C.
     * 
     * @param args not used
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
     */
    public static void main(String[] args) throws MidiUnavailableException, InvalidMidiDataException {
        
        // parse simplified abc into a Music
        Music scale = notes("C D E F G A B C' B A G F E D C", PIANO);
        
        System.out.println(scale);
        
        // play!
        MusicPlayer.play(scale);
    }
}

RowYourBoatInitial.java

package music.examples;

import music.*;
import music.midi.MusicPlayer;
import static music.Instrument.*;
import static music.MusicLanguage.*;

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

public class RowYourBoatInitial {

    /**
     * Play a song.
     * 
     * @param args not used
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
     */
    public static void main(String[] args) throws MidiUnavailableException, InvalidMidiDataException {
        Music rowYourBoat =
            notes("C C C3/4 D/4 E |"       // Row, row, row your boat,
                + "E3/4 D/4 E3/4 F/4 G2 |" // Gently down the stream.
                + "C'/3 C'/3 C'/3 G/3 G/3 G/3 E/3 E/3 E/3 C/3 C/3 C/3 |"
                                           // Merrily, merrily, merrily, merrily,
                + "G3/4 F/4 E3/4 D/4 C2",  // Life is but a dream.
                PIANO);
        
        System.out.println(rowYourBoat);
        
        MusicPlayer.play(rowYourBoat);
    }
}

SequencePlayer.java

package music;

/**
 * Schedules and plays a sequence of notes at given times.
 */
public interface SequencePlayer {

    /**
     * Schedule a note to be played starting at startBeat for the duration numBeats.
     * @param instr instrument for the note
     * @param pitch pitch value of the note
     * @param startBeat the starting beat (while playing, must be now or in the future)
     * @param numBeats the number of beats the note is played
     */
    public void addNote(Instrument instr, Pitch pitch, double startBeat, double numBeats);

    /**
     * Play the scheduled music.
     */
    public void play();

}

MidiSequencePlayer.java

package music.midi;

import java.math.BigInteger;
import java.text.MessageFormat;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Consumer;

import javax.sound.midi.*;

import music.Instrument;
import music.Pitch;
import music.SequencePlayer;

/**
 * Schedules and plays a sequence of notes using the MIDI synthesizer.
 */
public class MidiSequencePlayer implements SequencePlayer {

    /**
     * Default tempo.
     */
    public static final int DEFAULT_BEATS_PER_MINUTE = 120;
    /**
     * Default MIDI ticks per beat.
     */
    public static final int DEFAULT_TICKS_PER_BEAT = 64;

    // the volume
    private static final int DEFAULT_VELOCITY = 100;

    // the generic marker meta message type
    private static final int META_MARKER = 6;
    // the "end_of_track" meta message type
    private static final int META_END_OF_TRACK = 47;

    // MIDI note number representing middle C
    private static final int MIDI_NOTE_MIDDLE_C = 60;
    
    private final Synthesizer synthesizer;

    // active MIDI channels, assigned to instruments
    private final Map<Instrument, Integer> channelForInstrument = new HashMap<>();

    // next available channel number (not assigned to an instrument yet)
    private int nextChannel = 0;

    private final Sequencer sequencer;
    private final Track track;
    private final int beatsPerMinute;
    private final int ticksPerBeat;

    // event callback functions
    private final SortedMap<Integer, Consumer<Double>> callbacks = new TreeMap<>();
    private final MetaMessage keepalive = new MetaMessage(META_MARKER, new byte[] { 0 }, 1);

    /*
     * Rep invariant:
     *   sequencer and track are non-null,
     *   beatsPerMinute and ticksPerBeat are positive,
     *   channels and callbacks are non-null,
     *   channels does not contain value nextChannel
     */

    private void checkRep() {
        assert sequencer != null : "sequencer should be non-null";
        assert track != null : "track should be non-null";
        assert beatsPerMinute >= 0 : "should be positive number of beats per minute";
        assert ticksPerBeat >= 0 : "should be positive number of ticks per beat";
        assert callbacks != null : "callbacks should be non-null";
        assert callbacks.keySet().stream().allMatch(n -> n >= 1) : "callback numbers should be positive";
        assert ! channelForInstrument.values().contains(nextChannel) : "nextChannel should not be assigned";
    }

    /**
     * Make a new MIDI sequence player with the default parameters.
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
     */
    public MidiSequencePlayer() throws MidiUnavailableException, InvalidMidiDataException {
        this(DEFAULT_BEATS_PER_MINUTE, DEFAULT_TICKS_PER_BEAT);
    }

    /**
     * Make a new MIDI sequence player.
     * @param beatsPerMinute the number of beats per minute
     * @param ticksPerBeat the number of ticks per beat; every note plays for an integer number of ticks
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
     */
    public MidiSequencePlayer(int beatsPerMinute, int ticksPerBeat)
            throws MidiUnavailableException, InvalidMidiDataException {
        synthesizer = MidiSystem.getSynthesizer();
        synthesizer.open();
        synthesizer.loadAllInstruments(synthesizer.getDefaultSoundbank());

        this.sequencer = MidiSystem.getSequencer();

        // create a sequence object with with tempo-based timing, where
        // the resolution of the time step is based on ticks per quarter note
        Sequence sequence = new Sequence(Sequence.PPQ, ticksPerBeat);
        this.beatsPerMinute = beatsPerMinute;
        this.ticksPerBeat = ticksPerBeat;

        // create an empty track; notes will be added to this track
        this.track = sequence.createTrack();

        sequencer.setSequence(sequence);

        checkRep();
    }

    /**
     * Schedule a note to be played with MIDI synthesizer.
     */
    @Override
    public void addNote(Instrument instr, Pitch pitch, double startBeat, double numBeats) {
        int channel = getChannel(instr);
        int note = getMidiNote(pitch);
        try {
            // schedule two events in the track, one for starting a note and
            // the other for ending the note.
            addMidiNoteEvent(ShortMessage.NOTE_ON, channel, note, (int) (startBeat * ticksPerBeat));
            addMidiNoteEvent(ShortMessage.NOTE_OFF, channel, note, (int) ((startBeat + numBeats) * ticksPerBeat));
        } catch (InvalidMidiDataException imde) {
            String msg = MessageFormat.format("Cannot add note with the pitch {0} at beat {1} " +
                                              "for duration {2}", note, startBeat, numBeats);
            throw new RuntimeException(msg, imde);
        }
    }

    /**
     * Schedule a MIDI note event.
     * @param eventType valid MidiMessage type in ShortMessage
     * @param channel valid channel
     * @param note valid pitch value
     * @param tick tick >= 0
     * @throws InvalidMidiDataException
     */
    private void addMidiNoteEvent(int eventType, int channel, int note, int tick) throws InvalidMidiDataException {
        ShortMessage msg = new ShortMessage(eventType, channel, note, DEFAULT_VELOCITY);
        this.track.add(new MidiEvent(msg, tick));
    }

    /**
     * Schedule a callback when the synthesizer reaches a time.
     * @param callback function to call
     * @param atBeat beat at which to call the callback (while playing, must be now or in the future)
     */
    public void addEvent(Consumer<Double> callback, double atBeat) {
        int callbackNumber = saveCallback(callback);
        try {
            addMidiMetaEvent(callbackNumber, (int) (atBeat * ticksPerBeat));
        } catch (InvalidMidiDataException imde) {
            throw new RuntimeException("Cannot add event at beat " + atBeat, imde);
        }
    }

    private int saveCallback(Consumer<Double> callback) {
        int key = callbacks.isEmpty() ? 1 : callbacks.lastKey() + 1;
        callbacks.put(key, callback);
        checkRep();
        return key;
    }

    /**
     * Schedule a MIDI meta event.
     * @param callback active callback number
     * @param tick tick >= 0
     * @throws InvalidMidiDataException
     */
    private void addMidiMetaEvent(int callback, int tick) throws InvalidMidiDataException {
        byte[] bytes = BigInteger.valueOf(callback).toByteArray();
        MetaMessage msg = new MetaMessage(META_MARKER, bytes, bytes.length);
        this.track.add(new MidiEvent(msg, tick));
        this.track.add(new MidiEvent(keepalive, tick+1));
    }

    /**
     * Open the MIDI sequencer and play the scheduled music.
     */
    @Override
    public void play() {
        try {
            sequencer.open();
        } catch (MidiUnavailableException mue) {
            throw new RuntimeException("Unable to open MIDI sequencer", mue);
        }
        sequencer.setTempoInBPM(this.beatsPerMinute);

        sequencer.addMetaEventListener(meta -> {
            try {
                if (meta.getType() == META_MARKER) {
                    // trigger event callback
                    int callbackNumber = new BigInteger(meta.getData()).intValue();
                    if (callbackNumber > 0) {
                        callbacks.remove(callbackNumber).accept(sequencer.getTickPosition() / (double)ticksPerBeat);
                    }
                } else if (meta.getType() == META_END_OF_TRACK) {
                    // allow the sequencer to finish
                    try { Thread.sleep(Duration.ofSeconds(1).toMillis()); } catch (InterruptedException ie) { }
                    // stop & close the sequencer
                    sequencer.stop();
                    sequencer.close();
                }
            } catch (Throwable t) {
                t.printStackTrace();
                throw t;
            }
        });

        // start playing!
        sequencer.start();
    }

    /**
     * Get a MIDI channel for the given instrument, allocating one if necessary.
     * @param instr instrument
     * @return channel for the instrument
     */
    private int getChannel(Instrument instr) {
        // check whether this instrument already has a channel
        if (channelForInstrument.containsKey(instr)) {
            return channelForInstrument.get(instr);
        }
        
        int channel = allocateChannel();
        patchInstrumentIntoChannel(channel, instr);
        channelForInstrument.put(instr, channel);
        checkRep();
        return channel;
    }

    /**
     * @return next available channel number
     */
    private int allocateChannel() {
        MidiChannel[] channels = synthesizer.getChannels();
        if (nextChannel >= channels.length) {
            throw new RuntimeException("Tried to use too many instruments: limited to " + channels.length);
        }
        return nextChannel++;
    }

    private void patchInstrumentIntoChannel(int channel, Instrument instr) {
        try {
            addMidiNoteEvent(ShortMessage.PROGRAM_CHANGE, channel, instr.ordinal(), 0);
        } catch (InvalidMidiDataException imde) {
            throw new RuntimeException("Cannot set instrument", imde);
        }
    }

    /**
     * @return the MIDI note number for a pitch, defined as the number of
     *         semitones above C 5 octaves below middle C; for example,
     *         middle C is note 60
     */
    private static int getMidiNote(Pitch pitch) {
        return MIDI_NOTE_MIDDLE_C + pitch.difference(Pitch.MIDDLE_C);
    }

    /**
     * @return a string that displays the entire track information as a
     *         sequence of MIDI events, where each event is either turning on
     *         or off a note at a certain tick, a marker event, or the end of
     *         the track
     */
    @Override
    public String toString() {
        String trackInfo = "";

        for (int i = 0; i < track.size(); i++) {
            final MidiEvent e = track.get(i);
            final MidiMessage msg = e.getMessage();
            final String msgString;

            if (msg instanceof ShortMessage) {
                final ShortMessage smg = (ShortMessage) msg;
                final int command = smg.getCommand();

                if (command == ShortMessage.NOTE_OFF) {
                    msgString = "NOTE_OFF Pitch: " + smg.getData1();
                } else if (command == ShortMessage.NOTE_ON) {
                    msgString = "NOTE_ON Pitch: " + smg.getData1();
                } else if (command == ShortMessage.PROGRAM_CHANGE) {
                    msgString = "PROGRAM_CHANGE " + Instrument.values()[smg.getData2()];
                } else {
                    msgString = "Unknown command " + command;
                }

            } else if (msg instanceof MetaMessage) {
                final MetaMessage mmg = (MetaMessage) msg;
                final int type = mmg.getType();
                final String typeName;

                if (type == META_MARKER) {
                    typeName = "MARKER";
                } else if (type == META_END_OF_TRACK) {
                    typeName = "END_OF_TRACK";
                } else {
                    typeName = "Unknown type " + type;
                }

                msgString = "Meta event: " + typeName;

            } else {
                msgString = "Unknown event";
            }

            trackInfo += msgString + " Tick: " + e.getTick() + "\n";
        }

        return trackInfo;
    }
}

MusicPlayer.java

package music.midi;

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

import music.*;

/**
 * MusicPlayer can play a Music expression on the MIDI synthesizer.
 */
public class MusicPlayer {

    /**
     * Play music.
     * @param music music to play
     * @throws MidiUnavailableException if MIDI device unavailable
     * @throws InvalidMidiDataException if MIDI play fails
     */
    public static void play(Music music) throws MidiUnavailableException, InvalidMidiDataException {
        final SequencePlayer player = new MidiSequencePlayer();
        
        // load the player with a sequence created from music (add a small delay at the beginning)
        final double warmup = 0.125;
        music.play(player, warmup);
        
        // start playing
        player.play();
    }
}