]> ruin.nu Git - moosique.git/blob - Moosique.java
no message
[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         // GUI and MIDI device variables
17         private static MooGUI gui;
18         private static Sequence seq;
19         private static Sequencer sequencer;
20         private static Synthesizer synthesizer;
21         private static Receiver receiver;
22         private static MidiChannel[] channels;
23         private static MidiChannel activeChannel;
24
25         // Recording variables
26         private static Track recordTrack = null;
27         private static MooTrackView recordTrackView = null;
28         private static boolean[] quantizers = {false, false, false};
29         private static int quantizeResolution;
30
31         // Collections
32         private static ArrayList copyBuffer, emptyTracks, timeSignatures, tempoChanges;
33         private static TreeSet selection;
34         private static Map trackMute = new HashMap();
35         private static Map trackSolo = new HashMap();
36
37         // Various...
38         private static File file = null;
39         private static long editPosition;
40         private static Thread player;
41
42         // Preferences
43         private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
44
45         // Constants
46         public static final int DEFAULT_RESOLUTION = 96, DEFAULT_TRACKS = 4;
47         public static final int WHOLE_NOTE = 16, HALF_NOTE = 8, QUARTER_NOTE = 4, EIGHTH_NOTE = 2, SIXTEENTH_NOTE = 1;
48
49         /** 
50          * Starts the application.
51          * 
52          * Parses command-line arguments, acquires MIDI devices and connects them,
53          * loads a sequence and creates the GUI.
54          */
55         public static void main (String[] args) {
56                 out("\nMoosique version 1.0\n", true);
57
58                 // Parses command-line arguments.
59                 String fileArg = null;
60                 for (int i = 0; i < args.length; i++) {
61                         if (args[i].equals("-n")) makeGUI = false;
62                         else if (fileArg == null) fileArg = args[i];
63                 }
64
65                 // Acquires MIDI devices and connects them.
66                 out("Initializing MIDI devices.", false);
67                 try {
68                         // Configures sequencer.
69                         sequencer = MidiSystem.getSequencer();
70                         advanceStatus();
71                         sequencer.open();
72                         sequencer.addMetaEventListener(new SongEndListener());
73
74                         // Configures synthesizer.
75                         synthesizer = MidiSystem.getSynthesizer();
76                         advanceStatus();
77                         synthesizer.open();
78
79                         // Connects devices.
80                         receiver = synthesizer.getReceiver();
81                         sequencer.getTransmitter().setReceiver(receiver);
82
83                         // Configures channels.
84                         channels = synthesizer.getChannels();
85                         setActiveChannel(0);
86                 } catch (MidiUnavailableException e) {
87                         out("Failed, quitting.", true);
88                         System.exit(1);
89                 }
90                 out("Done", true);
91
92                 // Loads user preferences (work directory, last opened files etc).
93                 loadPreferences();
94
95                 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
96                 if (fileArg != null) {
97                         out("Loading MIDI sequence from " + fileArg + "...", false);
98                         if (!load(new File(fileArg))) {
99                                 out("Failed, creating new sequence", true);
100                                 clearSequence();
101                         } else {
102                                 out("Done", true);
103                         }
104                 } else {
105                         // Otherwise creates a new empty one.
106                         clearSequence();
107                 }
108
109                 // Builds GUI, unless n-flag is set.
110                 if (makeGUI) {
111                         out("Building GUI.", false);
112                         try {
113                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
114                         } catch (Exception e) {}
115                         gui = new MooGUI(seq, file);
116                         out("Done", true);
117                 } else {
118                         out("Playing...", false);
119                         play();
120                         while (sequencer.isRunning()) {}
121                         out("Done", true);
122                         quit();
123                 }
124         }
125
126
127
128
129
130
131
132
133         /*                              ***
134          **       ACCESSOR METHODS       **
135          ***                              */
136
137
138
139
140
141
142
143
144         /** 
145          * Returns the currently active MidiChannel.
146          * @return the active MidiChannel
147          */
148         public static MidiChannel getActiveChannel() {
149                 return activeChannel;
150         }
151
152         /** 
153          * Returns the MidiChannels of the selected synthesizer.
154          * @return the available MidiChannels
155          */
156         public static MidiChannel getChannel(int i) {
157                 return channels[i];
158         }
159
160         /** 
161          * Returns the current copy buffer.
162          * @return the current copy buffer
163          */
164         public static ArrayList getCopyBuffer() {
165                 return copyBuffer;
166         }
167
168         /** 
169          * Returns the current editing position of the sequencer.
170          * @return the tick position
171          */
172         public static long getEditPosition() {
173                 return editPosition;
174         }
175
176         /** 
177          * Returns the GUI.
178          * @return the GUI
179          */
180         public static MooGUI getGUI() {
181                 return gui;
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 true if the current sequence has been edited.
210          * @return the tick position
211          */
212         public static boolean isEdited() {
213                 return edited;
214         }
215
216         /** 
217          * Returns whether the given track should be drawn
218          * @return true if the given track should be drawn
219          */
220         public static boolean shouldBeDrawn(Track track) {
221                 if (drawEmptyTracks) return true;
222                 else return (!emptyTracks.contains(track));
223         }
224
225
226
227
228
229
230
231
232         /*                              ***
233          **       MUTATOR METHODS        **
234          ***                              */
235
236
237
238
239
240
241
242
243         /** 
244          * Fast forwards the current sequence the given number of measures.
245          * @param measures      the number of measures to fast forward
246          */
247         public static void forward(long ticks) {
248                 editPosition += ticks;
249         }
250
251         /** 
252          * Rewinds the current sequence the given number of measures.
253          * @param measures      the number of measures to rewind
254          */
255         public static void rewind(long ticks) {
256                 editPosition -= ticks;
257         }
258
259         /** 
260          * Sets the currently active MidiChannel.
261          * @param channel       the number of the MidiChannel to activate
262          */
263         public static void setActiveChannel(int channel) {
264                 activeChannel = channels[channel];
265         }
266
267         /** 
268          * Sets the current copy buffer.
269          * @param the copy buffer
270          */
271         public static void setCopyBuffer(ArrayList buffer) {
272                 copyBuffer = buffer;
273         }
274
275         /** 
276          * Sets whether empty tracks should be drawn
277          * @param state         true if empty tracks should be drawn
278          */
279         public static void setDrawEmptyTracks(boolean state) {
280                 drawEmptyTracks = state;
281         }
282
283         /** 
284          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
285          */
286         public static void setEdited() {
287                 edited = true;
288         }
289
290         /** 
291          * Sets the current editing position of the sequencer.
292          * @param ticks         the tick position
293          */
294         public static void setEditPosition(long ticks) {
295                 editPosition = ticks;
296         }
297
298         /** 
299          * Sets the solo setting of the given track.
300          * @param on    true for solo, false for not
301          */
302         public static void setTrackSolo(Track track, boolean on){
303                 trackSolo.put(track, new Boolean(on));  
304         }
305
306         /** 
307          * Sets the mute setting of the given track.
308          * @param on    true for mute, false for not
309          */
310         public static void setTrackMute(Track track, boolean on){
311                 trackMute.put(track, new Boolean(on));  
312         }
313
314         /** 
315          * Sets the current playback volume.
316          * @param volume        the volume, between 0 and 1
317          */
318         public void setVolume(long volume) {
319                 for (int i = 0; i < channels.length; i++) {
320                         channels[i].controlChange(7, (int)(volume * 127.0));
321                 }
322         }
323
324
325
326
327
328
329
330
331         /*                              ***
332          **       SELECTION METHODS      **
333          ***                              */
334
335
336
337
338
339
340
341
342         /** 
343          * Returns the current selection.
344          * @return the current selection
345          */
346         public static TreeSet getSelection() {
347                 return selection;
348         }
349
350         /**
351          * Selects the given note
352          * @param the note to select
353          */
354         public static void selectNote(MooNoteElement elem) {
355                 selection.add(elem);
356         }
357
358         /**
359          * Deselects the given note
360          * @param the note to deselect
361          */
362         public static void deselectNote(MooNoteElement elem) {
363                 selection.remove(elem);
364         }
365
366         /**
367          * Deselects all notes.
368          */
369         public static void deselectAllNotes() {
370                 Iterator it = selection.iterator();
371                 while(it.hasNext()) {
372                         ((MooNoteElement)it.next()).deselect();
373                 }
374                 selection.clear();
375         }
376
377         /**
378          * Determines if the given MooNoteElement is the only one in the track view that is selected.
379          * @return if the given element is the only selected one
380          */
381         public static boolean isTheOnlySelected(MooNoteElement elem) {
382                 Iterator it = selection.iterator();
383                 while(it.hasNext()) {
384                         if (!it.next().equals(elem)) return false;
385                 }
386                 return true;
387         }
388
389
390
391
392
393
394
395
396         /*                              ***
397          **       PLAYBACK METHODS       **
398          ***                              */
399
400
401
402
403
404
405
406
407         /** 
408          * Starts playback of the current sequence.
409          */
410         public static void play() {
411                 sequencer.setTickPosition(editPosition);
412                 resume();
413         }
414
415         /** 
416          * Resumes playback of the current sequence.
417          */
418         public static void resume() {
419                 gui.update(0);
420                 try {
421                         sequencer.setSequence(seq);
422                 } catch (InvalidMidiDataException e) {}
423                 Track[] tracks = seq.getTracks();
424
425                 sequencer.start();
426
427                 for (int i = 0; i < tracks.length; i++) {
428
429                         Object ob = trackSolo.get(tracks[i]);
430                         if(ob instanceof Boolean){
431                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
432                         }
433
434                         ob = trackMute.get(tracks[i]);
435                         if(ob instanceof Boolean){
436                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
437                         }
438                 }
439
440                 // Disables input to volatile components
441                 // gui.disable();
442
443                 // Creates the visualisation thread and starts it.
444                 player = new PlayThread();
445                 player.start();
446         }
447
448         /** 
449          * Pauses playback of the current sequence.
450          */
451         public static void pause() {
452                 if (sequencer.isRunning()) {
453                         sequencer.stop();
454                 }
455                 player = null;
456         }
457
458         /** 
459          * Stops playback of the current sequence.
460          */
461         public static void stop() {
462                 if (sequencer.isRunning()) {
463                         sequencer.stop();
464                 }
465                 sequencer.setTickPosition(editPosition);
466                 player = null;
467                 gui.update((long)0);
468
469                 if (recordTrack != null && recordTrackView != null) {
470                         recordTrackView.disableKeyboardRecording();
471                         sequencer.stopRecording();
472                         sequencer.recordDisable(recordTrack);
473                         if (quantizers[0]) recordTrackView.placeNewNotes(quantize(
474                                 convertTrack(recordTrack), quantizeResolution, quantizers[1], quantizers[2]));
475                         else recordTrackView.placeNewNotes(Moosique.convertTrack(recordTrack));
476                 }
477         }
478
479         /** 
480          * Enables recording to the given track.
481          * @param track         the track in which to store the recorded data
482          * @param tempo         the channel from which to record data
483          * @param quantizers    an array of booleans where 0 = quantize?, 1 = location, 2 = duration
484          * @param resolution    the note size to round each note to
485          * @return if the recording was initialised successfully
486          */
487         public static boolean record(Track track, int channel, boolean[] quants, int resolution) {
488                 try {
489                         sequencer.recordEnable(track, channel);
490                         sequencer.startRecording();
491                 } catch(Exception e) {
492                         e.printStackTrace();
493                         return false;
494                 }
495                 quantizers = quants;
496                 quantizeResolution = resolution;
497                 recordTrack = track;
498                 recordTrackView = gui.getView().getTrackView(track);
499                 recordTrackView.enableKeyboardRecording();
500                 Moosique.setEdited(); 
501                 return true;
502         }
503
504
505
506
507
508
509
510
511         /*                                  ***
512          **  TEMPO & TIME SIGNATURE METHODS  **
513          ***                                  */
514
515
516
517
518
519
520
521
522         /** 
523          * Returns the byte array for the given tempo.
524          * @param tempo the tempo in beats per minute
525          * @return an array of bytes representing the given tempo
526          */
527         public static byte[] encodeTempo(int tempo) {
528                 int microSecsPerQuarter = 60000000 / tempo;
529                 byte[] b = new byte[3];
530                 b[0] = (byte)(microSecsPerQuarter / 65536);
531                 b[1] = (byte)((microSecsPerQuarter - (b[0] * 65536)) / 256);
532                 b[2] = (byte)(microSecsPerQuarter - (b[0] * 65536) - (b[1] * 256));
533                 return b;
534         }
535
536         /** 
537          * Returns the tempo for the given byte array.
538          * @param an array of three bytes representing the tempo
539          * @return the tempo in beats per minute
540          */
541         public static int decodeTempo(byte[] bytes) {
542                 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]);
543         }
544
545         /** 
546          * Returns the tempo in the given tick position.
547          * @param tick the tick position for which to return the tempo
548          * @return the tempo at the specified tick position
549          */
550         public static int getTempo(long tick) {
551                 if (tempoChanges.size() == 0) return 120;
552                 MidiEvent tempoEvent = (MidiEvent)tempoChanges.get(0);
553                 if (tempoChanges.size() > 1) {
554                         for (int i = 1; i < tempoChanges.size(); i++) {
555                                 MidiEvent nextTempoEvent = (MidiEvent)tempoChanges.get(i);
556                                 if (nextTempoEvent.getTick() < tick && nextTempoEvent.getTick() > tempoEvent.getTick())
557                                         tempoEvent = nextTempoEvent;
558                         }
559                 }
560                 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
561         }
562
563         /** 
564          * Sets the tempo at the given tick position.
565          * @param ticks         the tick position
566          */
567         public static void setTempo(long tick, int bpm) {
568                 // Checks for a tempo event at the given tick position. 
569                 MidiEvent tempoEvent = null;
570                 Iterator it = tempoChanges.iterator();
571                 while(it.hasNext()) {
572                         MidiEvent nextTempoEvent = (MidiEvent)it.next();
573                         if (nextTempoEvent.getTick() == tick) {
574                                 tempoEvent = nextTempoEvent;
575                                 break;
576                         }
577                 }
578
579                 // If none was found, creates and adds a new one.
580                 if (tempoEvent == null) {
581                         tempoEvent = new MidiEvent(new MetaMessage(), tick);
582                         (seq.getTracks())[0].add(tempoEvent);
583                         tempoChanges.add(tempoEvent);
584                         Collections.sort(tempoChanges, new MidiEventComparator());
585                 }
586
587                 // Sets the tempo of the event (found or created).
588                 try {
589                         ((MetaMessage)tempoEvent.getMessage()).setMessage(81, encodeTempo(bpm), 3);
590                 } catch (InvalidMidiDataException e) {}
591         }
592
593         /** 
594          * Returns the byte array for the given time signature.
595          * @param numerator     the numerator of the time signature
596          * @param denominator   the denominator of the time signature
597          * @return an array of bytes representing the given time signature
598          */
599         public static byte[] encodeTimeSig(int numerator, int denominator) {
600                 byte[] b = {
601                         (byte)numerator,
602                         (byte)(Math.log(denominator) / Math.log(2)), // logarithm of denominator in base 2
603                         (byte)96,
604                         (byte)8
605                 };
606                 return b;
607         }
608         /** 
609          * Returns the time signature for the given byte array.
610          * @param an array of four bytes representing the time signature
611          * @return an array of two integers where [0] is the numerator and [1] the denominator
612          */
613         public static int[] decodeTimeSig(byte[] bytes) {
614                 int[] t = {
615                         (int)bytes[0],
616                         (int)(1 << bytes[1])
617                 };
618                 return t;
619         }
620
621         /** 
622          * Returns the time signature in the given tick position.
623          * @param tick the tick position for which to return the time signature
624          * @return an array of two integers where [0] is the numerator and [1] the denominator
625          */
626         public static int[] getTimeSig(long tick) {
627                 int[] ts = {4, 4};
628                 if (timeSignatures.size() == 0) return ts;
629                 MidiEvent timeSigEvent = (MidiEvent)timeSignatures.get(0);
630                 if (timeSignatures.size() > 1) {
631                         for (int i = 1; i < timeSignatures.size(); i++) {
632                                 MidiEvent nextTimeSigEvent = (MidiEvent)timeSignatures.get(i);
633                                 if (nextTimeSigEvent.getTick() <= tick && nextTimeSigEvent.getTick() > timeSigEvent.getTick())
634                                         timeSigEvent = nextTimeSigEvent;
635                         }
636                 }
637                 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
638         }
639
640         /** 
641          * Sets the time signature at the given tick position.
642          * @param ticks         the tick position
643          */
644         public static void setTimeSig(long tick, int numerator, int denominator) {
645                 // Checks for a time signature event at the given tick position. 
646                 MidiEvent timeSigEvent = null;
647                 Iterator it = timeSignatures.iterator();
648                 while(it.hasNext()) {
649                         MidiEvent nextTimeSigEvent = (MidiEvent)it.next();
650                         if (nextTimeSigEvent.getTick() == tick) {
651                                 timeSigEvent = nextTimeSigEvent;
652                                 break;
653                         }
654                 }
655
656                 // If none was found, creates and adds a new one.
657                 if (timeSigEvent == null) {
658                         timeSigEvent = new MidiEvent(new MetaMessage(), tick);
659                         (seq.getTracks())[0].add(timeSigEvent);
660                         timeSignatures.add(timeSigEvent);
661                         Collections.sort(timeSignatures, new MidiEventComparator());
662                 }
663
664                 // Sets the time signature of the event (found or created).
665                 try {
666                         ((MetaMessage)timeSigEvent.getMessage()).setMessage(88, encodeTimeSig(numerator, denominator), 4);
667                 } catch (InvalidMidiDataException e) {}
668         }
669         
670         /**
671          * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
672          * @param tickPosition the tick position for which to calculate the position
673          * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
674          */
675         public static int[] getPositionForTicks(long tickPosition) {
676                 int ticksPerBeat = seq.getResolution();
677                 long measures = 0, beats = 0, ticks = 0;
678
679                 // Counts for each time signature change up to the last one before the given tick position.
680                 Iterator it = timeSignatures.iterator();
681                 MidiEvent lastTSEvent = (MidiEvent)it.next();
682                 while(it.hasNext()) {
683                         MidiEvent nextTSEvent = (MidiEvent)it.next();
684                         if (nextTSEvent.getTick() > tickPosition) break;
685                         long tickDiff = nextTSEvent.getTick() - lastTSEvent.getTick();
686                         int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
687                         int beatsPerMeasure = ts[0] * (4 / ts[1]);
688                         long thisTSMeasures = tickDiff / (beatsPerMeasure * ticksPerBeat);
689                         measures += thisTSMeasures;
690                         long thisTSBeats = (tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat) / ticksPerBeat;
691                         beats += thisTSBeats;
692                         ticks += tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat - thisTSBeats * ticksPerBeat;
693                         lastTSEvent = nextTSEvent;
694                 }
695
696                 // Counts from the given tick position to the last time signature change before it.
697                 long tickDiff = tickPosition - lastTSEvent.getTick();
698                 int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
699                 int beatsPerMeasure = ts[0] * (4 / ts[1]);
700                 long thisTSMeasures = tickDiff / (beatsPerMeasure * ticksPerBeat);
701                 measures += thisTSMeasures;
702                 long thisTSBeats = (tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat) / ticksPerBeat;
703                 beats += thisTSBeats;
704                 ticks += tickDiff - thisTSMeasures * beatsPerMeasure * ticksPerBeat - thisTSBeats * ticksPerBeat;
705
706                 // Corrects any overflows.
707                 if (ticks > ticksPerBeat) {
708                         beats += Math.floor(ticks / ticksPerBeat);
709                         ticks = ticks % ticksPerBeat;
710                 }
711                 if (beats > beatsPerMeasure) {
712                         measures += Math.floor(beats / beatsPerMeasure);
713                         beats = beats % beatsPerMeasure;
714                 }
715
716                 // Returns the calculated values.
717                 int[] pos = {(int)measures, (int)beats, (int)ticks};
718                 return pos;
719         }
720
721         /** 
722          * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
723          * @param measures      the measure of the current position
724          * @param beats         the beat of the current position
725          * @param tick          the tick of the current position
726          * @return the tick position.
727          */
728         public static long getTicksForPosition(int measures, int beats, int ticks) {
729                 int res = seq.getResolution();
730                 int[] lastTSPos = new int[3];
731                 long tickPosition = 0;
732
733                 // Counts for each time signature change up to the last one before the given tick position.
734                 Iterator it = timeSignatures.iterator();
735                 MidiEvent lastTSEvent = (MidiEvent)it.next();
736                 while(it.hasNext()) {
737                         MidiEvent nextTSEvent = (MidiEvent)it.next();
738                         int[] nextTSPos = getPositionForTicks(nextTSEvent.getTick());
739                         if (nextTSPos[0] >= measures) break;
740                         lastTSPos = nextTSPos;
741                         lastTSEvent = nextTSEvent;
742                 }
743
744                 // Counts from the given tick position to the last time signature change before it.
745                 int measdiff = measures - lastTSPos[0];
746                 int[] ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
747                 int beatsPerMeasure = ts[0] * (4 / ts[1]);
748                 tickPosition = lastTSEvent.getTick() + (beatsPerMeasure * measures + beats) * res + ticks;
749
750                 return tickPosition;
751         }
752
753
754
755
756
757
758
759
760         /*                              ***
761          **       SYSTEM & IO METHODS    **
762          ***                              */
763
764
765
766
767
768
769
770
771         /** 
772          * Replaces the current sequence with a new one, holding three empty tracks.
773          */
774         public static void clearSequence() {
775                 // Reinitializes sequence variables
776                 file = null;
777                 reinitializeLists();
778
779                 try {
780                         // Creates a new sequence.
781                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
782                         Track[] tracks = seq.getTracks();
783
784                         // Sets default tempo (120) and time signature (4/4) at the beginning of the sequence.
785                         setTempo(0, 120);
786                         setTimeSig(0, 4, 4);
787
788                         // Sets program and title for the tracks.
789                         initializeTrack(tracks[1], 0, 24, "Guitar");
790                         initializeTrack(tracks[2], 1, 33, "Bass");
791                         initializeTrack(tracks[3], 9, 0, "Drums");
792                 } catch (InvalidMidiDataException e) {}
793
794                 // Sends the sequence to the GUI.
795                 if (gui != null) gui.setSequence(seq, null);
796         }
797
798         /** 
799          * Creates event in the given track for program change and title.
800          */
801         private static void initializeTrack(Track track, int channel, int program, String title) {
802                         // Creates program change and title message.
803                         ShortMessage programMsg = new ShortMessage();
804                         MetaMessage titleMsg = new MetaMessage();
805
806                         // Sets the data of the messages.
807                         try {
808                                 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
809                                 titleMsg.setMessage(3, title.getBytes(), title.length());
810                         } catch (InvalidMidiDataException e) {}
811
812                         // Sends the program change to the channel
813                         getChannel(channel).programChange(program);
814
815                         // Adds them to the track.
816                         track.add(new MidiEvent(programMsg, (long)0));
817                         track.add(new MidiEvent(titleMsg, (long)0));
818         }
819         
820         /** 
821          * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
822          * @param track         the track to convert
823          * @param quantize      whether to round locations and durations in the track to nearest 16th
824          * @return a list of the created MooNotes
825          */
826         public static List convertTrack(Track track) {
827                 // Searches the track for NoteOn and NoteOff events
828                 ArrayList noteOns = new ArrayList(track.size() / 2);
829                 ArrayList noteOffs = new ArrayList(track.size() / 2);
830                 ArrayList newMooNotes = new ArrayList();
831                 MidiEvent event;
832                 for (int j = 0; j < track.size(); j++) {
833                         event = track.get(j);
834                         if (event.getMessage().getStatus() >= 144 &&
835                             event.getMessage().getStatus() < 160) noteOns.add(event);
836                         if (event.getMessage().getStatus() >= 128 &&
837                             event.getMessage().getStatus() < 144) noteOffs.add(event);
838                 }
839                 noteOns.trimToSize();
840                 noteOffs.trimToSize();
841                 if (noteOns.size() == 0) emptyTracks.add(track);
842                 
843                 // Sorts the note lists by tick position.
844                 Comparator c = new MidiEventComparator();
845                 Collections.sort(noteOns, c);
846                 Collections.sort(noteOffs, c);
847
848                 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
849                 Iterator iOn = noteOns.iterator(), iOff;
850                 MidiEvent on, off = null, nextOff;
851                 ShortMessage onMsg, nextOffMsg;
852                 while(iOn.hasNext()) {
853                         on = (MidiEvent)iOn.next();
854                         if (!(on instanceof MooNote)) {
855                                 onMsg = (ShortMessage)on.getMessage();
856                                 iOff = noteOffs.iterator();
857                                 while(iOff.hasNext()) {
858                                         nextOff = (MidiEvent)iOff.next();
859                                         nextOffMsg = (ShortMessage)nextOff.getMessage();
860                                         if(onMsg.getChannel() == nextOffMsg.getChannel() &&
861                                            onMsg.getData1() == nextOffMsg.getData1() &&
862                                            c.compare(nextOff, on) > 0) {
863                                                 off = nextOff;
864                                                 iOff.remove();
865                                                 break;
866                                         }
867                                                 
868                                 }
869                                 track.remove(on);
870                                 MooNote mn;
871                                 if (off != null) {
872                                         mn = new MooNote(on, off);
873                                 } else {
874                                         mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
875                                 }
876                                 track.add(mn);
877                                 newMooNotes.add(mn);
878                                 iOn.remove();
879                         }
880                 }
881                 return newMooNotes;
882         }
883
884         /** 
885          * Loads a MIDI sequence from the given file.
886          * @param filename      the filename to use
887          */
888         public static boolean load(File loadFile) {
889                 // Loads sequence from file
890                 try {seq = MidiSystem.getSequence(loadFile);}
891                 catch (Exception e) {return false;}
892                 file = loadFile;
893                 edited = false;
894
895                 Track[] tracks = seq.getTracks();
896                 reinitializeLists();
897
898                 // Searches track 0 for changes in tempo and time signature.
899                 MidiEvent event;
900                 MetaMessage metaMsg;
901                 for (int i = 0; i < tracks[0].size(); i++) {
902                         event = tracks[0].get(i);
903                         if (event.getMessage().getStatus() == MetaMessage.META) {
904                                 metaMsg = (MetaMessage)event.getMessage();
905                                 switch(metaMsg.getType()) {
906                                         case 81: tempoChanges.add(event); break;
907                                         case 88: timeSignatures.add(event);
908                                 }
909                         }
910                 }
911                 Comparator c = new MidiEventComparator();
912                 Collections.sort(tempoChanges, c);
913                 Collections.sort(timeSignatures, c);
914
915                 try {
916                         // If no time signature specified at tick 0, adds the standard one.
917                         if (timeSignatures.size() == 0 || ((MidiEvent)timeSignatures.get(0)).getTick() != 0) {
918                                 MetaMessage timeSigMsg = new MetaMessage();
919                                 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
920                                 timeSignatures.add(0, new MidiEvent(timeSigMsg, (long)0));
921                         }
922
923                         // If no tempo specified at tick 0, adds the standard one.
924                         if (tempoChanges.size() == 0 || ((MidiEvent)tempoChanges.get(0)).getTick() != 0) {
925                                 MetaMessage tempoMsg = new MetaMessage();
926                                 tempoMsg.setMessage(81, encodeTempo(120), 3);
927                                 tempoChanges.add(0, new MidiEvent(tempoMsg, (long)0));
928                         }
929                 } catch (Exception e) {}
930
931                 // Converts tracks.
932                 for (int i = 0; i < tracks.length; i++) {
933                         convertTrack(tracks[i]);
934                 }
935
936                 // Sends sequence to GUI and sequencer, then returns
937                 if (gui != null) gui.setSequence(seq, file);
938                 try {
939                         sequencer.setSequence(seq);
940                 } catch (InvalidMidiDataException e) {}
941                 return true;
942         }
943
944         /** 
945          * Quantizes the given list of MIDI events
946          * @param notes         a list of the notes to quantize
947          * @param resolution    the note size to round each note to
948          * @param location      whether the quantize should affect the location of the note
949          * @param duration      whether the quantize should affect the duration of the note
950          */
951         public static List quantize(List notes, int resolution, boolean location, boolean duration) {
952                 Iterator it = notes.iterator();
953                 int noteSize = resolution * seq.getResolution() / 4;
954                 while(it.hasNext()) {
955                         MidiEvent note = (MidiEvent)it.next();
956                         if (note instanceof MooNote) {
957                                 MooNote mn = (MooNote)note;
958                                 if (location) mn.setTick(Math.round(mn.getTick() / noteSize) * noteSize);
959                                 if (duration) {
960                                         int length = Math.round(mn.getDuration() / noteSize) * noteSize;
961                                         if (length < noteSize) length = noteSize;
962                                         mn.setDuration(length);
963                                 }
964                         }
965                 }
966                 return notes;
967         }
968
969         /** 
970          * Reinitializes sequence-specific collections.
971          */
972         private static void reinitializeLists() {
973                 emptyTracks     = new ArrayList();
974                 timeSignatures  = new ArrayList();
975                 tempoChanges    = new ArrayList();
976                 copyBuffer      = new ArrayList();
977                 trackSolo       = new HashMap();
978                 trackMute       = new HashMap();
979                 selection       = new TreeSet();
980         }
981
982         /** 
983          * Loads the user preferences.
984          */
985         public static void loadPreferences() {
986                 
987         }
988
989         /** 
990          * Saves the user preferences.
991          */
992         public static void savePreferences() {
993                 
994         }
995
996         /** 
997          * Prompts the user to save any unsaved changes.
998          */
999         public static boolean promptOnUnsavedChanges() {
1000                 if (!edited) return false;
1001                 int exitOption = JOptionPane.showConfirmDialog(gui,
1002                         "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
1003                         "File not saved - continue?", 
1004                         JOptionPane.OK_CANCEL_OPTION, 
1005                         JOptionPane.WARNING_MESSAGE);
1006                 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
1007                 return false;
1008         }
1009
1010         /** 
1011          * Saves the current sequence to the previously given filename.
1012          */
1013         public static boolean save() {
1014                 if (file == null) return false;
1015                 else {
1016                         saveAs(file);
1017                         return true;
1018                 }
1019         }
1020
1021         /** 
1022          * Saves the current sequence to the given filename
1023          * @param file  the filename to use
1024          */
1025         public static boolean saveAs(File saveFile) {
1026                 try {
1027                         MidiSystem.write(seq, 1, saveFile);
1028                         file = saveFile;
1029                         edited = false;
1030                         gui.setStatus("Saved " + file.getAbsolutePath());
1031                         return true;
1032                 } catch (IOException e) {
1033                         gui.setStatus("Failed in saving " + file.getAbsolutePath());
1034                         return false;
1035                 }
1036         }
1037
1038         /** 
1039          * Prints the given string to the System output.
1040          */
1041         private static void out(String text, boolean newLine) {
1042                 if (newLine) System.out.println(text);
1043                 else System.out.print(text);
1044         }
1045
1046         /** 
1047          * Advances the current progress counter by printing a "." to the System output.
1048          */
1049         private static void advanceStatus() {
1050                 out(".", false);
1051         }
1052
1053         /** 
1054          * Releases all reserved devices and exits the program.
1055          */
1056         public static void quit() {
1057                 if (gui != null) {
1058                         if (promptOnUnsavedChanges()) return;
1059                 }
1060                 savePreferences();
1061                 if (sequencer.isOpen()) sequencer.close();
1062                 if (synthesizer.isOpen()) synthesizer.close();
1063                 System.exit(0);
1064         }
1065         
1066         /** 
1067          * A Ccmparator for sorting lists of MidiEvents.
1068          */
1069         public static class MidiEventComparator implements Comparator {
1070                 public int compare(Object o1, Object o2) {
1071                         int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
1072                         if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
1073                         return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
1074                 }
1075         }
1076
1077         /** 
1078          * The thread that updates the GUI during playback.
1079          */
1080         public static class PlayThread extends Thread {
1081                 public void run() {
1082                         Thread currentThread = Thread.currentThread();
1083                         while(currentThread == player) {
1084                                 gui.update(sequencer.getTickPosition());
1085                                 try {
1086                                         sleep(10);
1087                                 } catch (InterruptedException e) {
1088                                         Moosique.stop();
1089                                 }
1090                         }
1091                 }
1092         }
1093
1094         /** 
1095          * A listener for detecting the Meta event signifying the end of the sequence.
1096          */
1097         public static class SongEndListener implements MetaEventListener {
1098                 public void meta(MetaMessage event) {
1099                         if (event.getType() == 47)
1100                                 stop();
1101                 }
1102         }
1103 }