]> ruin.nu Git - moosique.git/blob - Moosique.java
Fixed accessors, encoders and decoders for tempo and time signature.
[moosique.git] / Moosique.java
1 import javax.sound.midi.*;
2 import java.io.*;
3 import javax.swing.*;
4 import java.util.*;
5
6 /**
7  * Moosique - The MIDI Tracker
8  * 
9  * Main class that handles initiation, IO and sound.
10  * 
11  * @author  Einar Pehrson
12  */
13  
14 public class Moosique {
15
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
24         private static ArrayList copyBuffer, emptyTracks, timeSignatures, tempoChanges;
25         private static Map trackMute = new HashMap();
26         private static Map trackSolo = new HashMap();
27         private static Thread player;
28
29         private static File file = null;
30         private static long editPosition;
31         private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
32         public static final int DEFAULT_RESOLUTION = 96, DEFAULT_TRACKS = 4;
33         public static final int WHOLE_NOTE = 0, HALF_NOTE = 1, QUARTER_NOTE = 2, EIGHTH_NOTE = 3, SIXTEENTH_NOTE = 4;
34
35         /** 
36          * Starts the application.
37          * 
38          * Parses command-line arguments, acquires MIDI devices and connects them,
39          * loads a sequence and creates the GUI.
40          */
41         public static void main (String[] args) {
42                 out("\nMoosique version 1.0\n");
43
44                 // Parses command-line arguments.
45                 String fileArg = null;
46                 for (int i = 0; i < args.length; i++) {
47                         if (args[i].equals("-n")) makeGUI = false;
48                         else if (fileArg == null) fileArg = args[i];
49                 }
50
51                 // Acquires MIDI devices and connects them.
52                 System.out.print("Initializing MIDI devices.");
53                 try {
54                         // Configures sequencer
55                         sequencer = MidiSystem.getSequencer();
56                         System.out.print(".");
57                         sequencer.open();
58                         sequencer.addMetaEventListener(new SongEndListener());
59
60                         // Configures synthesizer
61                         synthesizer = MidiSystem.getSynthesizer();
62                         System.out.print(".");
63                         synthesizer.open();
64
65                         // Configures receiver, transmitter and channels.
66                         receiver = synthesizer.getReceiver();
67                         sequencer.getTransmitter().setReceiver(receiver);
68                         channels = synthesizer.getChannels();
69                         setActiveChannel(0);
70                 } catch (MidiUnavailableException e) {
71                         out("Failed, quitting.");
72                         System.exit(1);
73                 }
74                 out("Done");
75
76                 // Loads user preferences (work directory, last opened files etc).
77                 loadPreferences();
78
79                 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
80                 if (fileArg != null) {
81                         System.out.print("Loading MIDI sequence from " + fileArg + "...");
82                         if (!load(new File(fileArg))) {
83                                 out("Failed, creating new sequence");
84                                 clearSequence();
85                         } else {
86                                 out("Done");
87                         }
88                 } else {
89                         // Otherwise creates a new empty one.
90                         clearSequence();
91                 }
92
93                 // Builds GUI, unless n-flag is set.
94                 if (makeGUI) {
95                         System.out.print("Building GUI.");
96                         try {
97                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
98                         } catch (Exception e) {}
99                         gui = new MooGUI(seq, file);
100                         out("Done");
101                 } else {
102                         System.out.print("Playing...");
103                         play();
104                         while (sequencer.isRunning()) {}
105                         out("Done");
106                         quit();
107                 }
108         }
109
110
111
112
113
114
115
116
117         /*                              ***
118          **       ACCESSOR METHODS       **
119          ***                              */
120
121
122
123
124
125
126
127
128         /** 
129          * Returns the currently active MidiChannel.
130          * @return the active MidiChannel
131          */
132         public static MidiChannel getActiveChannel() {
133                 return activeChannel;
134         }
135
136         /** 
137          * Returns the MidiChannels of the selected synthesizer.
138          * @return the available MidiChannels
139          */
140         public static MidiChannel getChannel(int i) {
141                 return channels[i];
142         }
143
144         /** 
145          * Returns the current copy buffer.
146          * @return the current copy buffer
147          */
148         public static ArrayList getCopyBuffer() {
149                 return copyBuffer;
150         }
151
152         /** 
153          * Returns the current editing position of the sequencer.
154          * @return the tick position
155          */
156         public static long getEditPosition() {
157                 return editPosition;
158         }
159
160         /** 
161          * Returns the GUI.
162          * @return the GUI
163          */
164         public static MooGUI getGUI() {
165                 return gui;
166         }
167
168         /** 
169          * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
170          * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
171          */
172         public static int[] getPositionForTicks(long ticks) {
173                 /*
174                 int measures, beats, ticks;
175                 for (int i = 0; i < timeSignatures.length; i++) {
176                         long tick = timeSignatures[i].getTick();
177                         // Split the ticks in the interval into measures, beats and ticks.
178                 }
179                 */
180                 int[] pos = {1, 1, 1};
181                 return pos;
182         }
183
184         /** 
185          * Returns the receiver of the current sequencer.
186          * @return the receiver
187          */
188         public static Receiver getReceiver() {
189                 return receiver;
190         }
191
192         /** 
193          * Returns the current sequence.
194          * @return the current sequence
195          */
196         public static Sequence getSequence() {
197                 return seq;
198         }
199
200         /** 
201          * Returns the current sequencer.
202          * @return the current sequencer
203          */
204         public static Sequencer getSequencer() {
205                 return sequencer;
206         }
207
208         /** 
209          * Returns the tempo in the given tick position.
210          * @param tick the tick position for which to return the tempo
211          * @return the tempo at the specified tick position
212          */
213         public static int getTempo(long tick) {
214                 if (tempoChanges.size() == 0) return 120;
215                 MidiEvent tempoEvent = (MidiEvent)tempoChanges.get(0);
216                 if (tempoChanges.size() > 1) {
217                         for (int i = 1; i < tempoChanges.size(); i++) {
218                                 MidiEvent nextTempoEvent = (MidiEvent)tempoChanges.get(i);
219                                 if (nextTempoEvent.getTick() < tick && nextTempoEvent.getTick() > tempoEvent.getTick())
220                                         tempoEvent = nextTempoEvent;
221                         }
222                 }
223                 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
224         }
225
226         /** 
227          * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
228          * @return the tick position.
229          */
230         public static long getTicksForPosition(int measures, int beats, int ticks) {
231                 long tickPos = 0;
232                 /*
233                 for (int i = 0; i < timeSignatures.length; i++) {
234                         long tick = timeSignatures[i].getTick();
235                         // Add the measures, beats and ticks in the interval.
236                 }
237                 */
238                 return tickPos;
239         }
240
241         /** 
242          * Returns the time signature in the given tick position.
243          * @param tick the tick position for which to return the time signature
244          * @return an array of two integers where [0] is the numerator and [1] the denominator
245          */
246         public static int[] getTimeSig(long tick) {
247                 int[] ts = {4, 4};
248                 if (timeSignatures.size() == 0) return ts;
249                 MidiEvent timeSigEvent = (MidiEvent)timeSignatures.get(0);
250                 if (timeSignatures.size() > 1) {
251                         for (int i = 1; i < timeSignatures.size(); i++) {
252                                 MidiEvent nextTimeSigEvent = (MidiEvent)timeSignatures.get(i);
253                                 if (nextTimeSigEvent.getTick() < tick && nextTimeSigEvent.getTick() > timeSigEvent.getTick())
254                                         timeSigEvent = nextTimeSigEvent;
255                         }
256                 }
257                 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
258         }
259
260         /** 
261          * Returns true if the current sequence has been edited.
262          * @return the tick position
263          */
264         public static boolean isEdited() {
265                 return edited;
266         }
267
268         /** 
269          * Returns whether the given track should be drawn
270          * @return true if the given track should be drawn
271          */
272         public static boolean shouldBeDrawn(Track track) {
273                 if (drawEmptyTracks) return true;
274                 else return (!emptyTracks.contains(track));
275         }
276
277
278
279
280
281
282
283
284         /*                              ***
285          **       MUTATOR METHODS        **
286          ***                              */
287
288
289
290
291
292
293
294
295         /** 
296          * Fast forwards the current sequence the given number of measures.
297          * @param measures      the number of measures to fast forward
298          */
299         public static void forward(long ticks) {
300                 editPosition += ticks;
301         }
302
303         /** 
304          * Rewinds the current sequence the given number of measures.
305          * @param measures      the number of measures to rewind
306          */
307         public static void rewind(long ticks) {
308                 editPosition -= ticks;
309         }
310
311         /** 
312          * Sets the currently active MidiChannel.
313          * @param channel       the number of the MidiChannel to activate
314          */
315         public static void setActiveChannel(int channel) {
316                 activeChannel = channels[channel];
317         }
318
319         /** 
320          * Sets the current copy buffer.
321          * @param the copy buffer
322          */
323         public static void setCopyBuffer(ArrayList buffer) {
324                 copyBuffer = buffer;
325         }
326
327         /** 
328          * Sets whether empty tracks should be drawn
329          * @param state         true if empty tracks should be drawn
330          */
331         public static void setDrawEmptyTracks(boolean state) {
332                 drawEmptyTracks = state;
333         }
334
335         /** 
336          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
337          */
338         public static void setEdited() {
339                 edited = true;
340         }
341
342         /** 
343          * Sets the current editing position of the sequencer.
344          * @param ticks         the tick position
345          */
346         public static void setEditPosition(long ticks) {
347                 editPosition = ticks;
348         }
349
350         /** 
351          * Sets the current editing position of the sequencer.
352          * @param ticks         the tick position
353          */
354         public static void setTempo(int bpm) {
355                 // tempoMsg
356         }
357
358         /** 
359          * Sets the current editing position of the sequencer.
360          * @param ticks         the tick position
361          */
362         public static void setTimeSig(int bpm) {
363                 // timeSigMsg
364         }
365         
366         /** 
367          * Sets the solo setting of the given track.
368          * @param on    true for solo, false for not
369          */
370         public static void setTrackSolo(Track track, boolean on){
371                 trackSolo.put(track, new Boolean(on));  
372         }
373
374         /** 
375          * Sets the mute setting of the given track.
376          * @param on    true for mute, false for not
377          */
378         public static void setTrackMute(Track track, boolean on){
379                 trackMute.put(track, new Boolean(on));  
380         }
381
382         /** 
383          * Sets the current playback volume.
384          * @param volume        the volume, between 0 and 1
385          */
386         public void setVolume(long volume) {
387                 for (int i = 0; i < channels.length; i++) {
388                         channels[i].controlChange(7, (int)(volume * 127.0));
389                 }
390         }
391
392
393
394
395
396
397
398
399         /*                              ***
400          **  ENCODING / DECODING METHODS **
401          ***                              */
402
403
404
405
406
407
408
409
410         /** 
411          * Returns the byte array for the given tempo.
412          * @param tempo the tempo in beats per minute
413          * @return an array of bytes representing the given tempo
414          */
415         public static byte[] encodeTempo(int tempo) {
416                 int microSecsPerQuarter = 60000000 / tempo;
417                 byte[] b = new byte[3];
418                 b[0] = (byte)(microSecsPerQuarter / 65536);
419                 b[1] = (byte)((microSecsPerQuarter - (b[0] * 65536)) / 256);
420                 b[2] = (byte)(microSecsPerQuarter - (b[0] * 65536) - (b[1] * 256));
421                 return b;
422         }
423
424         /** 
425          * Returns the tempo for the given byte array.
426          * @param an array of three bytes representing the tempo
427          * @return the tempo in beats per minute
428          */
429         public static int decodeTempo(byte[] bytes) {
430                 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]); // bytes[0] & 0xFF ???
431         }
432
433         /** 
434          * Returns the byte array for the given time signature.
435          * @param numerator     the numerator of the time signature
436          * @param denominator   the denominator of the time signature
437          * @return an array of bytes representing the given time signature
438          */
439         public static byte[] encodeTimeSig(int numerator, int denominator) {
440                 byte[] b = {
441                         (byte)numerator,
442                         (byte)(Math.log(denominator) / Math.log(2)),
443                         (byte)96,
444                         (byte)8
445                 };
446                 return b;
447         }
448         /** 
449          * Returns the time signature for the given byte array.
450          * @param an array of four bytes representing the time signature
451          * @return an array of two integers where [0] is the numerator and [1] the denominator
452          */
453         public static int[] decodeTimeSig(byte[] bytes) {
454                 int[] t = {
455                         (int)bytes[0],
456                         (int)(1 << bytes[1])
457                 };
458                 return t;
459         }
460
461
462
463
464
465
466
467
468         /*                              ***
469          **       PLAYBACK METHODS       **
470          ***                              */
471
472
473
474
475
476
477
478
479         /** 
480          * Starts playback of the current sequence.
481          */
482         public static void play() {
483                 sequencer.setTickPosition(editPosition);
484                 resume();
485         }
486
487         /** 
488          * Resumes playback of the current sequence.
489          */
490         public static void resume() {
491                 gui.update(0);
492                 try {
493                         sequencer.setSequence(seq);
494                 } catch (InvalidMidiDataException e) {}
495                 Track[] tracks = seq.getTracks();
496
497                 sequencer.start();
498
499                 for (int i = 0; i < tracks.length; i++) {
500
501                         Object ob = trackSolo.get(tracks[i]);
502                         if(ob instanceof Boolean){
503                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
504                         }
505
506                         ob = trackMute.get(tracks[i]);
507                         if(ob instanceof Boolean){
508                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
509                         }
510                 }
511
512                 // Disables input to volatile components
513                 // gui.disable();
514
515                 // Creates the visualisation thread and starts it.
516                 player = new PlayThread();
517                 player.start();
518         }
519
520         /** 
521          * Pauses playback of the current sequence.
522          */
523         public static void pause() {
524                 if (sequencer.isRunning()) {
525                         sequencer.stop();
526                 }
527                 if (player != null) player.interrupt();
528         }
529
530         /** 
531          * Stops playback of the current sequence.
532          */
533         public static void stop() {
534                 if (sequencer.isRunning()) {
535                         sequencer.stop();
536                 }
537                 sequencer.setTickPosition(editPosition);
538                 if (player != null) player.interrupt();
539                 gui.update((long)0);
540         }
541
542
543
544
545
546
547
548
549         /*                              ***
550          **       SYSTEM & IO METHODS    **
551          ***                              */
552
553
554
555
556
557
558
559
560         /** 
561          * Replaces the current sequence with a new one, holding three empty tracks.
562          */
563         public static void clearSequence() {
564                 try {
565                         // Creates a new sequence.
566                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
567                         Track[] tracks = seq.getTracks();
568
569                         // Creates messages for default tempo (120) and time signature (4/4).
570                         MetaMessage timeSigMsg = new MetaMessage();
571                         MetaMessage tempoMsg = new MetaMessage();
572                         try {
573                                 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
574                                 tempoMsg.setMessage(81, encodeTempo(120), 3);
575                         } catch (InvalidMidiDataException e) {}
576
577                         // Adds them to track 0.
578                         tracks[0].add(new MidiEvent(timeSigMsg, (long)0));
579                         tracks[0].add(new MidiEvent(tempoMsg, (long)0));
580
581                         // Sets program and title for the tracks.
582                         initializeTrack(tracks[1], 0, 24, "Guitar");
583                         initializeTrack(tracks[2], 1, 33, "Bass");
584                         initializeTrack(tracks[3], 9, 0, "Drums");
585
586                         file = null;
587                         emptyTracks = new ArrayList();
588                         timeSignatures = new ArrayList();
589                         tempoChanges = new ArrayList();
590                         trackSolo = new HashMap();
591                         trackMute = new HashMap();
592                         copyBuffer = new ArrayList();
593                 } catch (InvalidMidiDataException e) {}
594                 // Sends the sequence to the GUI.
595                 if (gui != null) gui.setSequence(seq);
596         }
597
598         /** 
599          * Creates event in the given track for program change and title.
600          */
601         private static void initializeTrack(Track track, int channel, int program, String title) {
602                         // Creates program change and title message.
603                         ShortMessage programMsg = new ShortMessage();
604                         MetaMessage titleMsg = new MetaMessage();
605
606                         // Sets the data of the messages.
607                         try {
608                                 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
609                                 titleMsg.setMessage(3, title.getBytes(), title.length());
610                         } catch (InvalidMidiDataException e) {}
611
612                         // Adds them to the track.
613                         track.add(new MidiEvent(programMsg, (long)0));
614                         track.add(new MidiEvent(titleMsg, (long)0));
615         }
616         
617         /** 
618          * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
619          * @param track         the track to convert
620          * @param quantize      whether to round locations and durations in the track to nearest 16th
621          * @return a list of the created MooNotes
622          */
623         public static List convertTrack(Track track, boolean quantize) {
624                 // Searches the track for NoteOn and NoteOff events
625                 ArrayList noteOns = new ArrayList(track.size() / 2);
626                 ArrayList noteOffs = new ArrayList(track.size() / 2);
627                 ArrayList newMooNotes = new ArrayList();
628                 MidiEvent event;
629                 for (int j = 0; j < track.size(); j++) {
630                         event = track.get(j);
631                         if (event.getMessage().getStatus() >= 144 &&
632                             event.getMessage().getStatus() < 160) noteOns.add(event);
633                         if (event.getMessage().getStatus() >= 128 &&
634                             event.getMessage().getStatus() < 144) noteOffs.add(event);
635                 }
636                 noteOns.trimToSize();
637                 noteOffs.trimToSize();
638                 if (noteOns.size() == 0) emptyTracks.add(track);
639                 
640                 // Sorts the note lists by tick position.
641                 Comparator c = new MidiEventComparator();
642                 Collections.sort(noteOns, c);
643                 Collections.sort(noteOffs, c);
644
645                 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
646                 Iterator iOn = noteOns.iterator(), iOff;
647                 MidiEvent on, off = null, nextOff;
648                 ShortMessage onMsg, nextOffMsg;
649                 while(iOn.hasNext()) {
650                         on = (MidiEvent)iOn.next();
651                         if (!(on instanceof MooNote)) {
652                                 onMsg = (ShortMessage)on.getMessage();
653                                 iOff = noteOffs.iterator();
654                                 while(iOff.hasNext()) {
655                                         nextOff = (MidiEvent)iOff.next();
656                                         nextOffMsg = (ShortMessage)nextOff.getMessage();
657                                         if(onMsg.getChannel() == nextOffMsg.getChannel() &&
658                                            onMsg.getData1() == nextOffMsg.getData1() &&
659                                            c.compare(nextOff, on) > 0) {
660                                                 off = nextOff;
661                                                 iOff.remove();
662                                                 break;
663                                         }
664                                                 
665                                 }
666                                 track.remove(on);
667                                 MooNote mn;
668                                 if (off != null) {
669                                         mn = new MooNote(on, off);
670                                 } else {
671                                         mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
672                                 }
673                                 track.add(mn);
674                                 newMooNotes.add(mn);
675                                 iOn.remove();
676                         }
677                 }
678                 if (quantize) quantize(newMooNotes, SIXTEENTH_NOTE, true, true);
679                 return newMooNotes;
680         }
681
682         /** 
683          * Loads a MIDI sequence from the given file.
684          * @param filename      the filename to use
685          */
686         public static boolean load(File loadFile) {
687                 // Loads sequence from file
688                 try {seq = MidiSystem.getSequence(loadFile);}
689                 catch (Exception e) {return false;}
690                 file = loadFile;
691                 edited = false;
692
693                 Track[] tracks = seq.getTracks();
694                 emptyTracks = new ArrayList();
695                 timeSignatures = new ArrayList();
696                 tempoChanges = new ArrayList();
697                 trackMute = new HashMap();
698                 trackSolo = new HashMap();
699                 copyBuffer = new ArrayList();
700
701                 // Searches track 0 for changes in tempo and time signature.
702                 MidiEvent event;
703                 MetaMessage metaMsg;
704                 ArrayList ts = new ArrayList(), tc = new ArrayList();
705                 for (int i = 0; i < tracks[0].size(); i++) {
706                         event = tracks[0].get(i);
707                         if (event.getMessage().getStatus() == MetaMessage.META) {
708                                 metaMsg = (MetaMessage)event.getMessage();
709                                 switch(metaMsg.getType()) {
710                                         case 81: tempoChanges.add(event); break;
711                                         case 88: timeSignatures.add(event);
712                                 }
713                         }
714                 }
715
716                 // Converts tracks.
717                 for (int i = 0; i < tracks.length; i++) {
718                         convertTrack(tracks[i], false);
719                 }
720
721                 // Sends sequence to GUI and sequencer, then returns
722                 if (gui != null) gui.setSequence(seq);
723                 try {
724                         sequencer.setSequence(seq);
725                 } catch (InvalidMidiDataException e) {}
726                 return true;
727         }
728
729         /** 
730          * Quantizes the given list of MIDI events
731          * @param notes         a list of the notes to quantize
732          * @param resolution    the note size to round each note to
733          * @param location      whether the quantize should affect the location of the note
734          * @param duration      whether the quantize should affect the duration of the note
735          */
736         public static void quantize(List notes, int resolution, boolean location, boolean duration) {
737                 // Math.round(mn.getTick() / ticksPerSixteenth);
738         }
739
740         /** 
741          * Loads the user preferences.
742          */
743         public static void loadPreferences() {
744                 
745         }
746
747         /** 
748          * Saves the user preferences.
749          */
750         public static void savePreferences() {
751                 
752         }
753
754         /** 
755          * Prompts the user .
756          */
757         public static boolean promptOnUnsavedChanges() {
758                 if (!edited) return false;
759                 int exitOption = JOptionPane.showConfirmDialog(gui,
760                         "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
761                         "File not saved - continue?", 
762                         JOptionPane.OK_CANCEL_OPTION, 
763                         JOptionPane.WARNING_MESSAGE);
764                 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
765                 return false;
766         }
767
768         /** 
769          * Saves the current sequence to the previously given filename.
770          */
771         public static boolean save() {
772                 if (file == null) return false;
773                 else {
774                         saveAs(file);
775                         return true;
776                 }
777         }
778
779         /** 
780          * Saves the current sequence to the given filename
781          * @param file  the filename to use
782          */
783         public static boolean saveAs(File saveFile) {
784                 try {
785                         MidiSystem.write(seq, 1, saveFile);
786                         file = saveFile;
787                         edited = false;
788                         gui.setStatus("Saved " + file.getAbsolutePath());
789                         return true;
790                 } catch (IOException e) {
791                         gui.setStatus("Failed in saving " + file.getAbsolutePath());
792                         return false;
793                 }
794         }
795
796         /** 
797          * Prints the given string to the System output.
798          */
799         private static void out(String text) {
800                 System.out.println(text);
801         }
802
803         /** 
804          * Releases all reserved devices and exits the program.
805          */
806         public static void quit() {
807                 if (gui != null) {
808                         if (promptOnUnsavedChanges()) return;
809                 }
810                 savePreferences();
811                 if (sequencer.isOpen()) sequencer.close();
812                 if (synthesizer.isOpen()) synthesizer.close();
813                 System.exit(0);
814         }
815         
816         /** 
817          * A Ccmparator for sorting lists of MidiEvents.
818          */
819         public static class MidiEventComparator implements Comparator {
820                 public int compare(Object o1, Object o2) {
821                         int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
822                         if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
823                         return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
824                 }
825         }
826
827         /** 
828          * The thread that updates the GUI during playback.
829          */
830         public static class PlayThread extends Thread {
831                 public void run() {
832                         // Updates the GUI with the current tick position.
833                         gui.update(sequencer.getTickPosition());
834
835                         // Puts the thread to sleep for as long as it takes
836                         // the sequencer to reach the next sixteenth.
837                         try {
838                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
839                                 sleep (10);
840                         } catch (InterruptedException e) {
841                                 Moosique.stop();
842                         }
843                 }
844         }
845
846         /** 
847          * A listener for detecting the end of the sequence.
848          */
849         public static class SongEndListener implements MetaEventListener {
850                 public void meta(MetaMessage event) {
851                         if (event.getType() == 47)
852                                 // End of sequence
853                                 stop();
854                 }
855         }
856 }