]> ruin.nu Git - moosique.git/blob - Moosique.java
68bfd1799fd4ed00e07d1e2cc6e9aa891a3b285d
[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         private static Track recordTrack = null;
24         private static MooTrackView recordTrackView = null;
25
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;
31
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;
37
38         /** 
39          * Starts the application.
40          * 
41          * Parses command-line arguments, acquires MIDI devices and connects them,
42          * loads a sequence and creates the GUI.
43          */
44         public static void main (String[] args) {
45                 out("\nMoosique version 1.0\n", true);
46
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];
52                 }
53
54                 // Acquires MIDI devices and connects them.
55                 out("Initializing MIDI devices.", false);
56                 try {
57                         // Configures sequencer.
58                         sequencer = MidiSystem.getSequencer();
59                         advanceStatus();
60                         sequencer.open();
61                         sequencer.addMetaEventListener(new SongEndListener());
62
63                         // Configures synthesizer.
64                         synthesizer = MidiSystem.getSynthesizer();
65                         advanceStatus();
66                         synthesizer.open();
67
68                         // Connects devices.
69                         receiver = synthesizer.getReceiver();
70                         sequencer.getTransmitter().setReceiver(receiver);
71
72                         // Configures channels.
73                         channels = synthesizer.getChannels();
74                         setActiveChannel(0);
75                 } catch (MidiUnavailableException e) {
76                         out("Failed, quitting.", true);
77                         System.exit(1);
78                 }
79                 out("Done", true);
80
81                 // Loads user preferences (work directory, last opened files etc).
82                 loadPreferences();
83
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);
89                                 clearSequence();
90                         } else {
91                                 out("Done", true);
92                         }
93                 } else {
94                         // Otherwise creates a new empty one.
95                         clearSequence();
96                 }
97
98                 // Builds GUI, unless n-flag is set.
99                 if (makeGUI) {
100                         out("Building GUI.", false);
101                         try {
102                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
103                         } catch (Exception e) {}
104                         gui = new MooGUI(seq, file);
105                         out("Done", true);
106                 } else {
107                         out("Playing...", false);
108                         play();
109                         while (sequencer.isRunning()) {}
110                         out("Done", true);
111                         quit();
112                 }
113         }
114
115
116
117
118
119
120
121
122         /*                              ***
123          **       ACCESSOR METHODS       **
124          ***                              */
125
126
127
128
129
130
131
132
133         /** 
134          * Returns the currently active MidiChannel.
135          * @return the active MidiChannel
136          */
137         public static MidiChannel getActiveChannel() {
138                 return activeChannel;
139         }
140
141         /** 
142          * Returns the MidiChannels of the selected synthesizer.
143          * @return the available MidiChannels
144          */
145         public static MidiChannel getChannel(int i) {
146                 return channels[i];
147         }
148
149         /** 
150          * Returns the current copy buffer.
151          * @return the current copy buffer
152          */
153         public static ArrayList getCopyBuffer() {
154                 return copyBuffer;
155         }
156
157         /** 
158          * Returns the current editing position of the sequencer.
159          * @return the tick position
160          */
161         public static long getEditPosition() {
162                 return editPosition;
163         }
164
165         /** 
166          * Returns the GUI.
167          * @return the GUI
168          */
169         public static MooGUI getGUI() {
170                 return gui;
171         }
172
173         /** 
174          * Returns the receiver of the current sequencer.
175          * @return the receiver
176          */
177         public static Receiver getReceiver() {
178                 return receiver;
179         }
180
181         /** 
182          * Returns the current sequence.
183          * @return the current sequence
184          */
185         public static Sequence getSequence() {
186                 return seq;
187         }
188
189         /** 
190          * Returns the current sequencer.
191          * @return the current sequencer
192          */
193         public static Sequencer getSequencer() {
194                 return sequencer;
195         }
196
197         /** 
198          * Returns true if the current sequence has been edited.
199          * @return the tick position
200          */
201         public static boolean isEdited() {
202                 return edited;
203         }
204
205         /** 
206          * Returns whether the given track should be drawn
207          * @return true if the given track should be drawn
208          */
209         public static boolean shouldBeDrawn(Track track) {
210                 if (drawEmptyTracks) return true;
211                 else return (!emptyTracks.contains(track));
212         }
213
214
215
216
217
218
219
220
221         /*                              ***
222          **       MUTATOR METHODS        **
223          ***                              */
224
225
226
227
228
229
230
231
232         /** 
233          * Fast forwards the current sequence the given number of measures.
234          * @param measures      the number of measures to fast forward
235          */
236         public static void forward(long ticks) {
237                 editPosition += ticks;
238         }
239
240         /** 
241          * Rewinds the current sequence the given number of measures.
242          * @param measures      the number of measures to rewind
243          */
244         public static void rewind(long ticks) {
245                 editPosition -= ticks;
246         }
247
248         /** 
249          * Sets the currently active MidiChannel.
250          * @param channel       the number of the MidiChannel to activate
251          */
252         public static void setActiveChannel(int channel) {
253                 activeChannel = channels[channel];
254         }
255
256         /** 
257          * Sets the current copy buffer.
258          * @param the copy buffer
259          */
260         public static void setCopyBuffer(ArrayList buffer) {
261                 copyBuffer = buffer;
262         }
263
264         /** 
265          * Sets whether empty tracks should be drawn
266          * @param state         true if empty tracks should be drawn
267          */
268         public static void setDrawEmptyTracks(boolean state) {
269                 drawEmptyTracks = state;
270         }
271
272         /** 
273          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
274          */
275         public static void setEdited() {
276                 edited = true;
277         }
278
279         /** 
280          * Sets the current editing position of the sequencer.
281          * @param ticks         the tick position
282          */
283         public static void setEditPosition(long ticks) {
284                 editPosition = ticks;
285         }
286
287         /** 
288          * Sets the current editing position of the sequencer.
289          * @param ticks         the tick position
290          */
291         public static void setTempo(int bpm) {
292                 // tempoMsg
293         }
294
295         /** 
296          * Sets the current editing position of the sequencer.
297          * @param ticks         the tick position
298          */
299         public static void setTimeSig(int bpm) {
300                 // timeSigMsg
301         }
302         
303         /** 
304          * Sets the solo setting of the given track.
305          * @param on    true for solo, false for not
306          */
307         public static void setTrackSolo(Track track, boolean on){
308                 trackSolo.put(track, new Boolean(on));  
309         }
310
311         /** 
312          * Sets the mute setting of the given track.
313          * @param on    true for mute, false for not
314          */
315         public static void setTrackMute(Track track, boolean on){
316                 trackMute.put(track, new Boolean(on));  
317         }
318
319         /** 
320          * Sets the current playback volume.
321          * @param volume        the volume, between 0 and 1
322          */
323         public void setVolume(long volume) {
324                 for (int i = 0; i < channels.length; i++) {
325                         channels[i].controlChange(7, (int)(volume * 127.0));
326                 }
327         }
328
329
330
331
332
333
334
335
336         /*                              ***
337          **       SELECTION METHODS      **
338          ***                              */
339
340
341
342
343
344
345
346
347         /** 
348          * Returns the current selection.
349          * @return the current selection
350          */
351         public static TreeSet getSelection() {
352                 return selection;
353         }
354
355         /**
356          * Selects the given note
357          * @param the note to select
358          */
359         public static void selectNote(MooNoteElement elem) {
360                 selection.add(elem);
361         }
362
363         /**
364          * Deselects the given note
365          * @param the note to deselect
366          */
367         public static void deselectNote(MooNoteElement elem) {
368                 selection.remove(elem);
369         }
370
371         /**
372          * Deselects all notes.
373          */
374         public static void deselectAllNotes() {
375                 Iterator it = selection.iterator();
376                 while(it.hasNext()) {
377                         ((MooNoteElement)it.next()).deselect();
378                 }
379                 selection.clear();
380         }
381
382         /**
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
385          */
386         public static boolean isTheOnlySelected(MooNoteElement elem) {
387                 Iterator it = selection.iterator();
388                 while(it.hasNext()) {
389                         if (!it.next().equals(elem)) return false;
390                 }
391                 return true;
392         }
393
394
395
396
397
398
399
400
401         /*                              ***
402          **       PLAYBACK METHODS       **
403          ***                              */
404
405
406
407
408
409
410
411
412         /** 
413          * Starts playback of the current sequence.
414          */
415         public static void play() {
416                 sequencer.setTickPosition(editPosition);
417                 resume();
418         }
419
420         /** 
421          * Resumes playback of the current sequence.
422          */
423         public static void resume() {
424                 gui.update(0);
425                 try {
426                         sequencer.setSequence(seq);
427                 } catch (InvalidMidiDataException e) {}
428                 Track[] tracks = seq.getTracks();
429
430                 sequencer.start();
431
432                 for (int i = 0; i < tracks.length; i++) {
433
434                         Object ob = trackSolo.get(tracks[i]);
435                         if(ob instanceof Boolean){
436                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
437                         }
438
439                         ob = trackMute.get(tracks[i]);
440                         if(ob instanceof Boolean){
441                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
442                         }
443                 }
444
445                 // Disables input to volatile components
446                 // gui.disable();
447
448                 // Creates the visualisation thread and starts it.
449                 player = new PlayThread();
450                 player.start();
451         }
452
453         /** 
454          * Pauses playback of the current sequence.
455          */
456         public static void pause() {
457                 if (sequencer.isRunning()) {
458                         sequencer.stop();
459                 }
460                 player = null;
461         }
462
463         /** 
464          * Stops playback of the current sequence.
465          */
466         public static void stop() {
467                 if (sequencer.isRunning()) {
468                         sequencer.stop();
469                 }
470                 sequencer.setTickPosition(editPosition);
471                 player = null;
472                 gui.update((long)0);
473
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));
481                 }
482         }
483
484         /** 
485          * Enables recording to the given track.
486          */
487         public static boolean record(Track track) {
488 /*              try {
489                         sequencer.recordEnable(track, channel);
490                         sequencer.startRecording();
491                 } catch(Exception e) {
492                         return false;
493                 }
494                 recordTrackView = gui.getView().getTrackView(track);
495                 recordTrackView.enableKeyboardRecording();
496                 Moosique.setEdited(); 
497 */
498                 return true;
499         }
500
501
502
503
504
505
506
507
508         /*                                  ***
509          **  TEMPO & TIME SIGNATURE METHODS  **
510          ***                                  */
511
512
513
514
515
516
517
518
519         /** 
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
523          */
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));
530                 return b;
531         }
532
533         /** 
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
537          */
538         public static int decodeTempo(byte[] bytes) {
539                 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]);
540         }
541
542         /** 
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
546          */
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;
555                         }
556                 }
557                 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
558         }
559
560         /** 
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
565          */
566         public static byte[] encodeTimeSig(int numerator, int denominator) {
567                 byte[] b = {
568                         (byte)numerator,
569                         (byte)(Math.log(denominator) / Math.log(2)), // logarithm of denominator in base 2
570                         (byte)96,
571                         (byte)8
572                 };
573                 return b;
574         }
575         /** 
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
579          */
580         public static int[] decodeTimeSig(byte[] bytes) {
581                 int[] t = {
582                         (int)bytes[0],
583                         (int)(1 << bytes[1])
584                 };
585                 return t;
586         }
587
588         /** 
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
592          */
593         public static int[] getTimeSig(long tick) {
594                 int[] ts = {4, 4};
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;
602                         }
603                 }
604                 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
605         }
606
607         /**
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.
611          */
612         public static int[] getPositionForTicks(long tickPosition) {
613                 int ticksPerBeat = seq.getResolution();
614                 long measures = 0, beats = 0, ticks = 0;
615
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;
631                 }
632
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;
642
643                 // Corrects any overflows.
644                 if (ticks > ticksPerBeat) {
645                         beats += Math.floor(ticks / ticksPerBeat);
646                         ticks = ticks % ticksPerBeat;
647                 }
648                 if (beats > beatsPerMeasure) {
649                         measures += Math.floor(beats / beatsPerMeasure);
650                         beats = beats % beatsPerMeasure;
651                 }
652
653                 // Returns the calculated values.
654                 int[] pos = {(int)measures, (int)beats, (int)ticks};
655                 return pos;
656         }
657
658         /** 
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.
664          */
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;
669
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;
679                 }
680
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;
686
687                 return tickPosition;
688         }
689
690
691
692
693
694
695
696
697         /*                              ***
698          **       SYSTEM & IO METHODS    **
699          ***                              */
700
701
702
703
704
705
706
707
708         /** 
709          * Replaces the current sequence with a new one, holding three empty tracks.
710          */
711         public static void clearSequence() {
712                 try {
713                         // Creates a new sequence.
714                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
715                         Track[] tracks = seq.getTracks();
716
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();
720                         try {
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));
726
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) {}
732
733                 // Reinitializes sequence variables
734                 file = null;
735                 reinitializeLists();
736
737                 // Sends the sequence to the GUI.
738                 if (gui != null) gui.setSequence(seq, null);
739         }
740
741         /** 
742          * Creates event in the given track for program change and title.
743          */
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();
748
749                         // Sets the data of the messages.
750                         try {
751                                 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
752                                 titleMsg.setMessage(3, title.getBytes(), title.length());
753                         } catch (InvalidMidiDataException e) {}
754
755                         // Sends the program change to the channel
756                         getChannel(channel).programChange(program);
757
758                         // Adds them to the track.
759                         track.add(new MidiEvent(programMsg, (long)0));
760                         track.add(new MidiEvent(titleMsg, (long)0));
761         }
762         
763         /** 
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
768          */
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();
774                 MidiEvent event;
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);
781                 }
782                 noteOns.trimToSize();
783                 noteOffs.trimToSize();
784                 if (noteOns.size() == 0) emptyTracks.add(track);
785                 
786                 // Sorts the note lists by tick position.
787                 Comparator c = new MidiEventComparator();
788                 Collections.sort(noteOns, c);
789                 Collections.sort(noteOffs, c);
790
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) {
806                                                 off = nextOff;
807                                                 iOff.remove();
808                                                 break;
809                                         }
810                                                 
811                                 }
812                                 track.remove(on);
813                                 MooNote mn;
814                                 if (off != null) {
815                                         mn = new MooNote(on, off);
816                                 } else {
817                                         mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
818                                 }
819                                 track.add(mn);
820                                 newMooNotes.add(mn);
821                                 iOn.remove();
822                         }
823                 }
824                 return newMooNotes;
825         }
826
827         /** 
828          * Loads a MIDI sequence from the given file.
829          * @param filename      the filename to use
830          */
831         public static boolean load(File loadFile) {
832                 // Loads sequence from file
833                 try {seq = MidiSystem.getSequence(loadFile);}
834                 catch (Exception e) {return false;}
835                 file = loadFile;
836                 edited = false;
837
838                 Track[] tracks = seq.getTracks();
839                 reinitializeLists();
840
841                 // Searches track 0 for changes in tempo and time signature.
842                 MidiEvent event;
843                 MetaMessage metaMsg;
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);
851                                 }
852                         }
853                 }
854                 Comparator c = new MidiEventComparator();
855                 Collections.sort(tempoChanges, c);
856                 Collections.sort(timeSignatures, c);
857
858                 try {
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));
864                         }
865
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));
871                         }
872                 } catch (Exception e) {}
873
874                 // Converts tracks.
875                 for (int i = 0; i < tracks.length; i++) {
876                         convertTrack(tracks[i]);
877                 }
878
879                 // Sends sequence to GUI and sequencer, then returns
880                 if (gui != null) gui.setSequence(seq, file);
881                 try {
882                         sequencer.setSequence(seq);
883                 } catch (InvalidMidiDataException e) {}
884                 return true;
885         }
886
887         /** 
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
893          */
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);
902                                 if (duration) {
903                                         int length = Math.round(mn.getDuration() / noteSize) * noteSize;
904                                         if (length < noteSize) length = noteSize;
905                                         mn.setDuration(length);
906                                 }
907                         }
908                 }
909                 return notes;
910         }
911
912         /** 
913          * Reinitializes sequence-specific collections.
914          */
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();
923         }
924
925         /** 
926          * Loads the user preferences.
927          */
928         public static void loadPreferences() {
929                 
930         }
931
932         /** 
933          * Saves the user preferences.
934          */
935         public static void savePreferences() {
936                 
937         }
938
939         /** 
940          * Prompts the user to save any unsaved changes.
941          */
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;
950                 return false;
951         }
952
953         /** 
954          * Saves the current sequence to the previously given filename.
955          */
956         public static boolean save() {
957                 if (file == null) return false;
958                 else {
959                         saveAs(file);
960                         return true;
961                 }
962         }
963
964         /** 
965          * Saves the current sequence to the given filename
966          * @param file  the filename to use
967          */
968         public static boolean saveAs(File saveFile) {
969                 try {
970                         MidiSystem.write(seq, 1, saveFile);
971                         file = saveFile;
972                         edited = false;
973                         gui.setStatus("Saved " + file.getAbsolutePath());
974                         return true;
975                 } catch (IOException e) {
976                         gui.setStatus("Failed in saving " + file.getAbsolutePath());
977                         return false;
978                 }
979         }
980
981         /** 
982          * Prints the given string to the System output.
983          */
984         private static void out(String text, boolean newLine) {
985                 if (newLine) System.out.println(text);
986                 else System.out.print(text);
987         }
988
989         /** 
990          * Advances the current progress counter by printing a "." to the System output.
991          */
992         private static void advanceStatus() {
993                 out(".", false);
994         }
995
996         /** 
997          * Releases all reserved devices and exits the program.
998          */
999         public static void quit() {
1000                 if (gui != null) {
1001                         if (promptOnUnsavedChanges()) return;
1002                 }
1003                 savePreferences();
1004                 if (sequencer.isOpen()) sequencer.close();
1005                 if (synthesizer.isOpen()) synthesizer.close();
1006                 System.exit(0);
1007         }
1008         
1009         /** 
1010          * A Ccmparator for sorting lists of MidiEvents.
1011          */
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());
1017                 }
1018         }
1019
1020         /** 
1021          * The thread that updates the GUI during playback.
1022          */
1023         public static class PlayThread extends Thread {
1024                 public void run() {
1025                         Thread currentThread = Thread.currentThread();
1026                         while(currentThread == player) {
1027                                 gui.update(sequencer.getTickPosition());
1028                                 try {
1029                                         sleep(10);
1030                                 } catch (InterruptedException e) {
1031                                         Moosique.stop();
1032                                 }
1033                         }
1034                 }
1035         }
1036
1037         /** 
1038          * A listener for detecting the Meta event signifying the end of the sequence.
1039          */
1040         public static class SongEndListener implements MetaEventListener {
1041                 public void meta(MetaMessage event) {
1042                         if (event.getType() == 47)
1043                                 stop();
1044                 }
1045         }
1046 }