1 import javax.sound.midi.*;
7 * Moosique - The MIDI Tracker
9 * Main class that handles initiation, IO and sound.
11 * @author Einar Pehrson
14 public class Moosique {
16 private static MooGUI gui;
17 private static Sequence seq;
18 private static Sequencer sequencer;
19 private static Synthesizer synthesizer;
20 private static Receiver receiver;
21 private static MidiChannel[] channels;
22 private static MidiChannel activeChannel;
23 private static Track recordTrack = null;
24 private static MooTrackView recordTrackView = null;
26 private static ArrayList copyBuffer, emptyTracks, timeSignatures, tempoChanges;
27 private static TreeSet selection;
28 private static Map trackMute = new HashMap();
29 private static Map trackSolo = new HashMap();
30 private static Thread player;
32 private static File file = null;
33 private static long editPosition;
34 private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
35 public static final int DEFAULT_RESOLUTION = 96, DEFAULT_TRACKS = 4;
36 public static final int WHOLE_NOTE = 16, HALF_NOTE = 8, QUARTER_NOTE = 4, EIGHTH_NOTE = 2, SIXTEENTH_NOTE = 1;
39 * Starts the application.
41 * Parses command-line arguments, acquires MIDI devices and connects them,
42 * loads a sequence and creates the GUI.
44 public static void main (String[] args) {
45 out("\nMoosique version 1.0\n", true);
47 // Parses command-line arguments.
48 String fileArg = null;
49 for (int i = 0; i < args.length; i++) {
50 if (args[i].equals("-n")) makeGUI = false;
51 else if (fileArg == null) fileArg = args[i];
54 // Acquires MIDI devices and connects them.
55 out("Initializing MIDI devices.", false);
57 // Configures sequencer.
58 sequencer = MidiSystem.getSequencer();
61 sequencer.addMetaEventListener(new SongEndListener());
63 // Configures synthesizer.
64 synthesizer = MidiSystem.getSynthesizer();
69 receiver = synthesizer.getReceiver();
70 sequencer.getTransmitter().setReceiver(receiver);
72 // Configures channels.
73 channels = synthesizer.getChannels();
75 } catch (MidiUnavailableException e) {
76 out("Failed, quitting.", true);
81 // Loads user preferences (work directory, last opened files etc).
84 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
85 if (fileArg != null) {
86 out("Loading MIDI sequence from " + fileArg + "...", false);
87 if (!load(new File(fileArg))) {
88 out("Failed, creating new sequence", true);
94 // Otherwise creates a new empty one.
98 // Builds GUI, unless n-flag is set.
100 out("Building GUI.", false);
102 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
103 } catch (Exception e) {}
104 gui = new MooGUI(seq, file);
107 out("Playing...", false);
109 while (sequencer.isRunning()) {}
123 ** ACCESSOR METHODS **
134 * Returns the currently active MidiChannel.
135 * @return the active MidiChannel
137 public static MidiChannel getActiveChannel() {
138 return activeChannel;
142 * Returns the MidiChannels of the selected synthesizer.
143 * @return the available MidiChannels
145 public static MidiChannel getChannel(int i) {
150 * Returns the current copy buffer.
151 * @return the current copy buffer
153 public static ArrayList getCopyBuffer() {
158 * Returns the current editing position of the sequencer.
159 * @return the tick position
161 public static long getEditPosition() {
169 public static MooGUI getGUI() {
174 * Returns the receiver of the current sequencer.
175 * @return the receiver
177 public static Receiver getReceiver() {
182 * Returns the current sequence.
183 * @return the current sequence
185 public static Sequence getSequence() {
190 * Returns the current sequencer.
191 * @return the current sequencer
193 public static Sequencer getSequencer() {
198 * Returns true if the current sequence has been edited.
199 * @return the tick position
201 public static boolean isEdited() {
206 * Returns whether the given track should be drawn
207 * @return true if the given track should be drawn
209 public static boolean shouldBeDrawn(Track track) {
210 if (drawEmptyTracks) return true;
211 else return (!emptyTracks.contains(track));
222 ** MUTATOR METHODS **
233 * Fast forwards the current sequence the given number of measures.
234 * @param measures the number of measures to fast forward
236 public static void forward(long ticks) {
237 editPosition += ticks;
241 * Rewinds the current sequence the given number of measures.
242 * @param measures the number of measures to rewind
244 public static void rewind(long ticks) {
245 editPosition -= ticks;
249 * Sets the currently active MidiChannel.
250 * @param channel the number of the MidiChannel to activate
252 public static void setActiveChannel(int channel) {
253 activeChannel = channels[channel];
257 * Sets the current copy buffer.
258 * @param the copy buffer
260 public static void setCopyBuffer(ArrayList buffer) {
265 * Sets whether empty tracks should be drawn
266 * @param state true if empty tracks should be drawn
268 public static void setDrawEmptyTracks(boolean state) {
269 drawEmptyTracks = state;
273 * Sets the current sequence as edited, which implies prompts when loading a new sequence.
275 public static void setEdited() {
280 * Sets the current editing position of the sequencer.
281 * @param ticks the tick position
283 public static void setEditPosition(long ticks) {
284 editPosition = ticks;
288 * Sets the current editing position of the sequencer.
289 * @param ticks the tick position
291 public static void setTempo(int bpm) {
296 * Sets the current editing position of the sequencer.
297 * @param ticks the tick position
299 public static void setTimeSig(int bpm) {
304 * Sets the solo setting of the given track.
305 * @param on true for solo, false for not
307 public static void setTrackSolo(Track track, boolean on){
308 trackSolo.put(track, new Boolean(on));
312 * Sets the mute setting of the given track.
313 * @param on true for mute, false for not
315 public static void setTrackMute(Track track, boolean on){
316 trackMute.put(track, new Boolean(on));
320 * Sets the current playback volume.
321 * @param volume the volume, between 0 and 1
323 public void setVolume(long volume) {
324 for (int i = 0; i < channels.length; i++) {
325 channels[i].controlChange(7, (int)(volume * 127.0));
337 ** SELECTION METHODS **
348 * Returns the current selection.
349 * @return the current selection
351 public static TreeSet getSelection() {
356 * Selects the given note
357 * @param the note to select
359 public static void selectNote(MooNoteElement elem) {
364 * Deselects the given note
365 * @param the note to deselect
367 public static void deselectNote(MooNoteElement elem) {
368 selection.remove(elem);
372 * Deselects all notes.
374 public static void deselectAllNotes() {
375 Iterator it = selection.iterator();
376 while(it.hasNext()) {
377 ((MooNoteElement)it.next()).deselect();
383 * Determines if the given MooNoteElement is the only one in the track view that is selected.
384 * @return if the given element is the only selected one
386 public static boolean isTheOnlySelected(MooNoteElement elem) {
387 Iterator it = selection.iterator();
388 while(it.hasNext()) {
389 if (!it.next().equals(elem)) return false;
402 ** PLAYBACK METHODS **
413 * Starts playback of the current sequence.
415 public static void play() {
416 sequencer.setTickPosition(editPosition);
421 * Resumes playback of the current sequence.
423 public static void resume() {
426 sequencer.setSequence(seq);
427 } catch (InvalidMidiDataException e) {}
428 Track[] tracks = seq.getTracks();
432 for (int i = 0; i < tracks.length; i++) {
434 Object ob = trackSolo.get(tracks[i]);
435 if(ob instanceof Boolean){
436 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
439 ob = trackMute.get(tracks[i]);
440 if(ob instanceof Boolean){
441 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
445 // Disables input to volatile components
448 // Creates the visualisation thread and starts it.
449 player = new PlayThread();
454 * Pauses playback of the current sequence.
456 public static void pause() {
457 if (sequencer.isRunning()) {
464 * Stops playback of the current sequence.
466 public static void stop() {
467 if (sequencer.isRunning()) {
470 sequencer.setTickPosition(editPosition);
474 if (recordTrack != null && recordTrackView != null) {
475 recordTrackView.disableKeyboardRecording();
476 sequencer.stopRecording();
477 sequencer.recordDisable(recordTrack);
478 // if (quantize) recordTrackView.placeNewNotes(quantize(
479 // convertTrack(recordTrack), SIXTEENTH_NOTE, true, true));
480 // else recordTrackView.placeNewNotes(Moosique.convertTrack(recordTrack));
485 * Enables recording to the given track.
487 public static boolean record(Track track) {
489 sequencer.recordEnable(track, channel);
490 sequencer.startRecording();
491 } catch(Exception e) {
494 recordTrackView = gui.getView().getTrackView(track);
495 recordTrackView.enableKeyboardRecording();
496 Moosique.setEdited();
509 ** TEMPO & TIME SIGNATURE METHODS **
520 * Returns the byte array for the given tempo.
521 * @param tempo the tempo in beats per minute
522 * @return an array of bytes representing the given tempo
524 public static byte[] encodeTempo(int tempo) {
525 int microSecsPerQuarter = 60000000 / tempo;
526 byte[] b = new byte[3];
527 b[0] = (byte)(microSecsPerQuarter / 65536);
528 b[1] = (byte)((microSecsPerQuarter - (b[0] * 65536)) / 256);
529 b[2] = (byte)(microSecsPerQuarter - (b[0] * 65536) - (b[1] * 256));
534 * Returns the tempo for the given byte array.
535 * @param an array of three bytes representing the tempo
536 * @return the tempo in beats per minute
538 public static int decodeTempo(byte[] bytes) {
539 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]);
543 * Returns the tempo in the given tick position.
544 * @param tick the tick position for which to return the tempo
545 * @return the tempo at the specified tick position
547 public static int getTempo(long tick) {
548 if (tempoChanges.size() == 0) return 120;
549 MidiEvent tempoEvent = (MidiEvent)tempoChanges.get(0);
550 if (tempoChanges.size() > 1) {
551 for (int i = 1; i < tempoChanges.size(); i++) {
552 MidiEvent nextTempoEvent = (MidiEvent)tempoChanges.get(i);
553 if (nextTempoEvent.getTick() < tick && nextTempoEvent.getTick() > tempoEvent.getTick())
554 tempoEvent = nextTempoEvent;
557 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
561 * Returns the byte array for the given time signature.
562 * @param numerator the numerator of the time signature
563 * @param denominator the denominator of the time signature
564 * @return an array of bytes representing the given time signature
566 public static byte[] encodeTimeSig(int numerator, int denominator) {
569 (byte)(Math.log(denominator) / Math.log(2)), // logarithm of denominator in base 2
576 * Returns the time signature for the given byte array.
577 * @param an array of four bytes representing the time signature
578 * @return an array of two integers where [0] is the numerator and [1] the denominator
580 public static int[] decodeTimeSig(byte[] bytes) {
589 * Returns the time signature in the given tick position.
590 * @param tick the tick position for which to return the time signature
591 * @return an array of two integers where [0] is the numerator and [1] the denominator
593 public static int[] getTimeSig(long tick) {
595 if (timeSignatures.size() == 0) return ts;
596 MidiEvent timeSigEvent = (MidiEvent)timeSignatures.get(0);
597 if (timeSignatures.size() > 1) {
598 for (int i = 1; i < timeSignatures.size(); i++) {
599 MidiEvent nextTimeSigEvent = (MidiEvent)timeSignatures.get(i);
600 if (nextTimeSigEvent.getTick() < tick && nextTimeSigEvent.getTick() > timeSigEvent.getTick())
601 timeSigEvent = nextTimeSigEvent;
604 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
608 * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
609 * @param tickPosition the tick position for which to calculate the position
610 * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
612 public static int[] getPositionForTicks(long tickPosition) {
613 int ticksPerBeat = seq.getResolution();
614 long measures = 0, beats = 0, ticks = 0;
616 // Counts for each time signature change up to the last one before the given tick position.
617 Iterator it = timeSignatures.iterator();
618 MidiEvent lastTSEvent = (MidiEvent)it.next();
619 while(it.hasNext()) {
620 MidiEvent nextTSEvent = (MidiEvent)it.next();
621 if (nextTSEvent.getTick() > tickPosition) break;
622 long tickDiff = nextTSEvent.getTick() - lastTSEvent.getTick();
623 int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
624 int beatsPerMeasure = ts[0] * (4 / ts[1]);
625 long thisTSMeasures = tickDiff / (beatsPerMeasure * ticksPerBeat);
626 measures += thisTSMeasures;
627 long thisTSBeats = (tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat) / ticksPerBeat;
628 beats += thisTSBeats;
629 ticks += tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat - thisTSBeats * ticksPerBeat;
630 lastTSEvent = nextTSEvent;
633 // Counts from the given tick position to the last time signature change before it.
634 long tickDiff = tickPosition - lastTSEvent.getTick();
635 int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
636 int beatsPerMeasure = ts[0] * (4 / ts[1]);
637 long thisTSMeasures = tickDiff / (beatsPerMeasure * ticksPerBeat);
638 measures += thisTSMeasures;
639 long thisTSBeats = (tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat) / ticksPerBeat;
640 beats += thisTSBeats;
641 ticks += tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat - thisTSBeats * ticksPerBeat;
643 // Corrects any overflows.
644 if (ticks > ticksPerBeat) {
645 beats += Math.floor(ticks / ticksPerBeat);
646 ticks = ticks % ticksPerBeat;
648 if (beats > beatsPerMeasure) {
649 measures += Math.floor(beats / beatsPerMeasure);
650 beats = beats % beatsPerMeasure;
653 // Returns the calculated values.
654 int[] pos = {(int)measures, (int)beats, (int)ticks};
659 * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
660 * @param measures the measure of the current position
661 * @param beats the beat of the current position
662 * @param tick the tick of the current position
663 * @return the tick position.
665 public static long getTicksForPosition(int measures, int beats, int ticks) {
666 int res = seq.getResolution();
667 int[] lastTSPos = new int[3];
668 long tickPosition = 0;
670 // Counts for each time signature change up to the last one before the given tick position.
671 Iterator it = timeSignatures.iterator();
672 MidiEvent lastTSEvent = (MidiEvent)it.next();
673 while(it.hasNext()) {
674 MidiEvent nextTSEvent = (MidiEvent)it.next();
675 int[] nextTSPos = getPositionForTicks(nextTSEvent.getTick());
676 if (nextTSPos[0] >= measures) break;
677 lastTSPos = nextTSPos;
678 lastTSEvent = nextTSEvent;
681 // Counts from the given tick position to the last time signature change before it.
682 int measdiff = measures - lastTSPos[0];
683 int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
684 int beatsPerMeasure = ts[0] * (4 / ts[1]);
685 tickPosition = lastTSEvent.getTick() + (beatsPerMeasure * measures + beats) * res + ticks;
698 ** SYSTEM & IO METHODS **
709 * Replaces the current sequence with a new one, holding three empty tracks.
711 public static void clearSequence() {
713 // Creates a new sequence.
714 seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
715 Track[] tracks = seq.getTracks();
717 // Creates messages for default tempo (120) and time signature (4/4), and adds them to track 0.
718 MetaMessage timeSigMsg = new MetaMessage();
719 MetaMessage tempoMsg = new MetaMessage();
721 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
722 tempoMsg.setMessage(81, encodeTempo(120), 3);
723 } catch (InvalidMidiDataException e) {}
724 tracks[0].add(new MidiEvent(timeSigMsg, (long)0));
725 tracks[0].add(new MidiEvent(tempoMsg, (long)0));
727 // Sets program and title for the tracks.
728 initializeTrack(tracks[1], 0, 24, "Guitar");
729 initializeTrack(tracks[2], 1, 33, "Bass");
730 initializeTrack(tracks[3], 9, 0, "Drums");
731 } catch (InvalidMidiDataException e) {}
733 // Reinitializes sequence variables
737 // Sends the sequence to the GUI.
738 if (gui != null) gui.setSequence(seq, null);
742 * Creates event in the given track for program change and title.
744 private static void initializeTrack(Track track, int channel, int program, String title) {
745 // Creates program change and title message.
746 ShortMessage programMsg = new ShortMessage();
747 MetaMessage titleMsg = new MetaMessage();
749 // Sets the data of the messages.
751 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
752 titleMsg.setMessage(3, title.getBytes(), title.length());
753 } catch (InvalidMidiDataException e) {}
755 // Sends the program change to the channel
756 getChannel(channel).programChange(program);
758 // Adds them to the track.
759 track.add(new MidiEvent(programMsg, (long)0));
760 track.add(new MidiEvent(titleMsg, (long)0));
764 * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
765 * @param track the track to convert
766 * @param quantize whether to round locations and durations in the track to nearest 16th
767 * @return a list of the created MooNotes
769 public static List convertTrack(Track track) {
770 // Searches the track for NoteOn and NoteOff events
771 ArrayList noteOns = new ArrayList(track.size() / 2);
772 ArrayList noteOffs = new ArrayList(track.size() / 2);
773 ArrayList newMooNotes = new ArrayList();
775 for (int j = 0; j < track.size(); j++) {
776 event = track.get(j);
777 if (event.getMessage().getStatus() >= 144 &&
778 event.getMessage().getStatus() < 160) noteOns.add(event);
779 if (event.getMessage().getStatus() >= 128 &&
780 event.getMessage().getStatus() < 144) noteOffs.add(event);
782 noteOns.trimToSize();
783 noteOffs.trimToSize();
784 if (noteOns.size() == 0) emptyTracks.add(track);
786 // Sorts the note lists by tick position.
787 Comparator c = new MidiEventComparator();
788 Collections.sort(noteOns, c);
789 Collections.sort(noteOffs, c);
791 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
792 Iterator iOn = noteOns.iterator(), iOff;
793 MidiEvent on, off = null, nextOff;
794 ShortMessage onMsg, nextOffMsg;
795 while(iOn.hasNext()) {
796 on = (MidiEvent)iOn.next();
797 if (!(on instanceof MooNote)) {
798 onMsg = (ShortMessage)on.getMessage();
799 iOff = noteOffs.iterator();
800 while(iOff.hasNext()) {
801 nextOff = (MidiEvent)iOff.next();
802 nextOffMsg = (ShortMessage)nextOff.getMessage();
803 if(onMsg.getChannel() == nextOffMsg.getChannel() &&
804 onMsg.getData1() == nextOffMsg.getData1() &&
805 c.compare(nextOff, on) > 0) {
815 mn = new MooNote(on, off);
817 mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
828 * Loads a MIDI sequence from the given file.
829 * @param filename the filename to use
831 public static boolean load(File loadFile) {
832 // Loads sequence from file
833 try {seq = MidiSystem.getSequence(loadFile);}
834 catch (Exception e) {return false;}
838 Track[] tracks = seq.getTracks();
841 // Searches track 0 for changes in tempo and time signature.
844 for (int i = 0; i < tracks[0].size(); i++) {
845 event = tracks[0].get(i);
846 if (event.getMessage().getStatus() == MetaMessage.META) {
847 metaMsg = (MetaMessage)event.getMessage();
848 switch(metaMsg.getType()) {
849 case 81: tempoChanges.add(event); break;
850 case 88: timeSignatures.add(event);
854 Comparator c = new MidiEventComparator();
855 Collections.sort(tempoChanges, c);
856 Collections.sort(timeSignatures, c);
859 // If no time signature specified at tick 0, adds the standard one.
860 if (timeSignatures.size() == 0 || ((MidiEvent)timeSignatures.get(0)).getTick() != 0) {
861 MetaMessage timeSigMsg = new MetaMessage();
862 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
863 timeSignatures.add(0, new MidiEvent(timeSigMsg, (long)0));
866 // If no tempo specified at tick 0, adds the standard one.
867 if (tempoChanges.size() == 0 || ((MidiEvent)tempoChanges.get(0)).getTick() != 0) {
868 MetaMessage tempoMsg = new MetaMessage();
869 tempoMsg.setMessage(81, encodeTempo(120), 3);
870 tempoChanges.add(0, new MidiEvent(tempoMsg, (long)0));
872 } catch (Exception e) {}
875 for (int i = 0; i < tracks.length; i++) {
876 convertTrack(tracks[i]);
879 // Sends sequence to GUI and sequencer, then returns
880 if (gui != null) gui.setSequence(seq, file);
882 sequencer.setSequence(seq);
883 } catch (InvalidMidiDataException e) {}
888 * Quantizes the given list of MIDI events
889 * @param notes a list of the notes to quantize
890 * @param resolution the note size to round each note to
891 * @param location whether the quantize should affect the location of the note
892 * @param duration whether the quantize should affect the duration of the note
894 public static List quantize(List notes, int resolution, boolean location, boolean duration) {
895 Iterator it = notes.iterator();
896 int noteSize = resolution * seq.getResolution() / 4;
897 while(it.hasNext()) {
898 MidiEvent note = (MidiEvent)it.next();
899 if (note instanceof MooNote) {
900 MooNote mn = (MooNote)note;
901 if (location) mn.setTick(Math.round(mn.getTick() / noteSize) * noteSize);
903 int length = Math.round(mn.getDuration() / noteSize) * noteSize;
904 if (length < noteSize) length = noteSize;
905 mn.setDuration(length);
913 * Reinitializes sequence-specific collections.
915 private static void reinitializeLists() {
916 emptyTracks = new ArrayList();
917 timeSignatures = new ArrayList();
918 tempoChanges = new ArrayList();
919 copyBuffer = new ArrayList();
920 trackSolo = new HashMap();
921 trackMute = new HashMap();
922 selection = new TreeSet();
926 * Loads the user preferences.
928 public static void loadPreferences() {
933 * Saves the user preferences.
935 public static void savePreferences() {
940 * Prompts the user to save any unsaved changes.
942 public static boolean promptOnUnsavedChanges() {
943 if (!edited) return false;
944 int exitOption = JOptionPane.showConfirmDialog(gui,
945 "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
946 "File not saved - continue?",
947 JOptionPane.OK_CANCEL_OPTION,
948 JOptionPane.WARNING_MESSAGE);
949 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
954 * Saves the current sequence to the previously given filename.
956 public static boolean save() {
957 if (file == null) return false;
965 * Saves the current sequence to the given filename
966 * @param file the filename to use
968 public static boolean saveAs(File saveFile) {
970 MidiSystem.write(seq, 1, saveFile);
973 gui.setStatus("Saved " + file.getAbsolutePath());
975 } catch (IOException e) {
976 gui.setStatus("Failed in saving " + file.getAbsolutePath());
982 * Prints the given string to the System output.
984 private static void out(String text, boolean newLine) {
985 if (newLine) System.out.println(text);
986 else System.out.print(text);
990 * Advances the current progress counter by printing a "." to the System output.
992 private static void advanceStatus() {
997 * Releases all reserved devices and exits the program.
999 public static void quit() {
1001 if (promptOnUnsavedChanges()) return;
1004 if (sequencer.isOpen()) sequencer.close();
1005 if (synthesizer.isOpen()) synthesizer.close();
1010 * A Ccmparator for sorting lists of MidiEvents.
1012 public static class MidiEventComparator implements Comparator {
1013 public int compare(Object o1, Object o2) {
1014 int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
1015 if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
1016 return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
1021 * The thread that updates the GUI during playback.
1023 public static class PlayThread extends Thread {
1025 Thread currentThread = Thread.currentThread();
1026 while(currentThread == player) {
1027 gui.update(sequencer.getTickPosition());
1030 } catch (InterruptedException e) {
1038 * A listener for detecting the Meta event signifying the end of the sequence.
1040 public static class SongEndListener implements MetaEventListener {
1041 public void meta(MetaMessage event) {
1042 if (event.getType() == 47)