1 import javax.sound.midi.*;
7 * Moosique - The MIDI Tracker
9 * Main class that handles initiation, IO and sound.
11 * @author Einar Pehrson
14 public class Moosique {
16 // 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;
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;
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();
38 private static File file = null;
39 private static long editPosition;
40 private static Thread player;
43 private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
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;
50 * Starts the application.
52 * Parses command-line arguments, acquires MIDI devices and connects them,
53 * loads a sequence and creates the GUI.
55 public static void main (String[] args) {
56 out("\nMoosique version 1.0\n", true);
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];
65 // Acquires MIDI devices and connects them.
66 out("Initializing MIDI devices.", false);
68 // Configures sequencer.
69 sequencer = MidiSystem.getSequencer();
72 sequencer.addMetaEventListener(new SongEndListener());
74 // Configures synthesizer.
75 synthesizer = MidiSystem.getSynthesizer();
80 receiver = synthesizer.getReceiver();
81 sequencer.getTransmitter().setReceiver(receiver);
83 // Configures channels.
84 channels = synthesizer.getChannels();
86 } catch (MidiUnavailableException e) {
87 out("Failed, quitting.", true);
92 // Loads user preferences (work directory, last opened files etc).
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);
105 // Otherwise creates a new empty one.
109 // Builds GUI, unless n-flag is set.
111 out("Building GUI.", false);
113 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
114 } catch (Exception e) {}
115 gui = new MooGUI(seq, file);
118 out("Playing...", false);
120 while (sequencer.isRunning()) {}
134 ** ACCESSOR METHODS **
145 * Returns the currently active MidiChannel.
146 * @return the active MidiChannel
148 public static MidiChannel getActiveChannel() {
149 return activeChannel;
153 * Returns the MidiChannels of the selected synthesizer.
154 * @return the available MidiChannels
156 public static MidiChannel getChannel(int i) {
161 * Returns the current copy buffer.
162 * @return the current copy buffer
164 public static ArrayList getCopyBuffer() {
169 * Returns the current editing position of the sequencer.
170 * @return the tick position
172 public static long getEditPosition() {
180 public static MooGUI getGUI() {
185 * Returns the receiver of the current sequencer.
186 * @return the receiver
188 public static Receiver getReceiver() {
193 * Returns the current sequence.
194 * @return the current sequence
196 public static Sequence getSequence() {
201 * Returns the current sequencer.
202 * @return the current sequencer
204 public static Sequencer getSequencer() {
209 * Returns true if the current sequence has been edited.
210 * @return the tick position
212 public static boolean isEdited() {
217 * Returns whether the given track should be drawn
218 * @return true if the given track should be drawn
220 public static boolean shouldBeDrawn(Track track) {
221 if (drawEmptyTracks) return true;
222 else return (!emptyTracks.contains(track));
233 ** MUTATOR METHODS **
244 * Fast forwards the current sequence the given number of measures.
245 * @param measures the number of measures to fast forward
247 public static void forward(long ticks) {
248 editPosition += ticks;
252 * Rewinds the current sequence the given number of measures.
253 * @param measures the number of measures to rewind
255 public static void rewind(long ticks) {
256 editPosition -= ticks;
260 * Sets the currently active MidiChannel.
261 * @param channel the number of the MidiChannel to activate
263 public static void setActiveChannel(int channel) {
264 activeChannel = channels[channel];
268 * Sets the current copy buffer.
269 * @param the copy buffer
271 public static void setCopyBuffer(ArrayList buffer) {
276 * Sets whether empty tracks should be drawn
277 * @param state true if empty tracks should be drawn
279 public static void setDrawEmptyTracks(boolean state) {
280 drawEmptyTracks = state;
284 * Sets the current sequence as edited, which implies prompts when loading a new sequence.
286 public static void setEdited() {
291 * Sets the current editing position of the sequencer.
292 * @param ticks the tick position
294 public static void setEditPosition(long ticks) {
295 editPosition = ticks;
299 * Sets the solo setting of the given track.
300 * @param on true for solo, false for not
302 public static void setTrackSolo(Track track, boolean on){
303 trackSolo.put(track, new Boolean(on));
307 * Sets the mute setting of the given track.
308 * @param on true for mute, false for not
310 public static void setTrackMute(Track track, boolean on){
311 trackMute.put(track, new Boolean(on));
315 * Sets the current playback volume.
316 * @param volume the volume, between 0 and 1
318 public void setVolume(long volume) {
319 for (int i = 0; i < channels.length; i++) {
320 channels[i].controlChange(7, (int)(volume * 127.0));
332 ** SELECTION METHODS **
343 * Returns the current selection.
344 * @return the current selection
346 public static TreeSet getSelection() {
351 * Selects the given note
352 * @param the note to select
354 public static void selectNote(MooNoteElement elem) {
359 * Deselects the given note
360 * @param the note to deselect
362 public static void deselectNote(MooNoteElement elem) {
363 selection.remove(elem);
367 * Deselects all notes.
369 public static void deselectAllNotes() {
370 Iterator it = selection.iterator();
371 while(it.hasNext()) {
372 ((MooNoteElement)it.next()).deselect();
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
381 public static boolean isTheOnlySelected(MooNoteElement elem) {
382 Iterator it = selection.iterator();
383 while(it.hasNext()) {
384 if (!it.next().equals(elem)) return false;
397 ** PLAYBACK METHODS **
408 * Starts playback of the current sequence.
410 public static void play() {
411 sequencer.setTickPosition(editPosition);
416 * Resumes playback of the current sequence.
418 public static void resume() {
421 sequencer.setSequence(seq);
422 } catch (InvalidMidiDataException e) {}
423 Track[] tracks = seq.getTracks();
427 for (int i = 0; i < tracks.length; i++) {
429 Object ob = trackSolo.get(tracks[i]);
430 if(ob instanceof Boolean){
431 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
434 ob = trackMute.get(tracks[i]);
435 if(ob instanceof Boolean){
436 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
440 // Disables input to volatile components
443 // Creates the visualisation thread and starts it.
444 player = new PlayThread();
449 * Pauses playback of the current sequence.
451 public static void pause() {
452 if (sequencer.isRunning()) {
459 * Stops playback of the current sequence.
461 public static void stop() {
462 if (sequencer.isRunning()) {
465 sequencer.setTickPosition(editPosition);
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));
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
487 public static boolean record(Track track, int channel, boolean[] quants, int resolution) {
489 sequencer.recordEnable(track, channel);
490 sequencer.startRecording();
491 } catch(Exception e) {
496 quantizeResolution = resolution;
498 recordTrackView = gui.getView().getTrackView(track);
499 recordTrackView.enableKeyboardRecording();
500 Moosique.setEdited();
512 ** TEMPO & TIME SIGNATURE METHODS **
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
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));
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
541 public static int decodeTempo(byte[] bytes) {
542 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]);
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
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;
560 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
564 * Sets the tempo at the given tick position.
565 * @param ticks the tick position
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;
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());
587 // Sets the tempo of the event (found or created).
589 ((MetaMessage)tempoEvent.getMessage()).setMessage(81, encodeTempo(bpm), 3);
590 } catch (InvalidMidiDataException e) {}
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
599 public static byte[] encodeTimeSig(int numerator, int denominator) {
602 (byte)(Math.log(denominator) / Math.log(2)), // logarithm of denominator in base 2
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
613 public static int[] decodeTimeSig(byte[] bytes) {
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
626 public static int[] getTimeSig(long tick) {
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;
637 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
641 * Sets the time signature at the given tick position.
642 * @param ticks the tick position
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;
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());
664 // Sets the time signature of the event (found or created).
666 ((MetaMessage)timeSigEvent.getMessage()).setMessage(88, encodeTimeSig(numerator, denominator), 4);
667 } catch (InvalidMidiDataException e) {}
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.
675 public static int[] getPositionForTicks(long tickPosition) {
676 int ticksPerBeat = seq.getResolution();
677 long measures = 0, beats = 0, ticks = 0;
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;
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;
706 // Corrects any overflows.
707 if (ticks > ticksPerBeat) {
708 beats += Math.floor(ticks / ticksPerBeat);
709 ticks = ticks % ticksPerBeat;
711 if (beats > beatsPerMeasure) {
712 measures += Math.floor(beats / beatsPerMeasure);
713 beats = beats % beatsPerMeasure;
716 // Returns the calculated values.
717 int[] pos = {(int)measures, (int)beats, (int)ticks};
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.
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;
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;
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;
761 ** SYSTEM & IO METHODS **
772 * Replaces the current sequence with a new one, holding three empty tracks.
774 public static void clearSequence() {
775 // Reinitializes sequence variables
780 // Creates a new sequence.
781 seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
782 Track[] tracks = seq.getTracks();
784 // Sets default tempo (120) and time signature (4/4) at the beginning of the sequence.
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) {}
794 // Sends the sequence to the GUI.
795 if (gui != null) gui.setSequence(seq, null);
799 * Creates event in the given track for program change and title.
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();
806 // Sets the data of the messages.
808 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
809 titleMsg.setMessage(3, title.getBytes(), title.length());
810 } catch (InvalidMidiDataException e) {}
812 // Sends the program change to the channel
813 getChannel(channel).programChange(program);
815 // Adds them to the track.
816 track.add(new MidiEvent(programMsg, (long)0));
817 track.add(new MidiEvent(titleMsg, (long)0));
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
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();
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);
839 noteOns.trimToSize();
840 noteOffs.trimToSize();
841 if (noteOns.size() == 0) emptyTracks.add(track);
843 // Sorts the note lists by tick position.
844 Comparator c = new MidiEventComparator();
845 Collections.sort(noteOns, c);
846 Collections.sort(noteOffs, c);
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) {
872 mn = new MooNote(on, off);
874 mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
885 * Loads a MIDI sequence from the given file.
886 * @param filename the filename to use
888 public static boolean load(File loadFile) {
889 // Loads sequence from file
890 try {seq = MidiSystem.getSequence(loadFile);}
891 catch (Exception e) {return false;}
895 Track[] tracks = seq.getTracks();
898 // Searches track 0 for changes in tempo and time signature.
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);
911 Comparator c = new MidiEventComparator();
912 Collections.sort(tempoChanges, c);
913 Collections.sort(timeSignatures, c);
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));
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));
929 } catch (Exception e) {}
932 for (int i = 0; i < tracks.length; i++) {
933 convertTrack(tracks[i]);
936 // Sends sequence to GUI and sequencer, then returns
937 if (gui != null) gui.setSequence(seq, file);
939 sequencer.setSequence(seq);
940 } catch (InvalidMidiDataException e) {}
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
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);
960 int length = Math.round(mn.getDuration() / noteSize) * noteSize;
961 if (length < noteSize) length = noteSize;
962 mn.setDuration(length);
970 * Reinitializes sequence-specific collections.
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();
983 * Loads the user preferences.
985 public static void loadPreferences() {
990 * Saves the user preferences.
992 public static void savePreferences() {
997 * Prompts the user to save any unsaved changes.
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;
1011 * Saves the current sequence to the previously given filename.
1013 public static boolean save() {
1014 if (file == null) return false;
1022 * Saves the current sequence to the given filename
1023 * @param file the filename to use
1025 public static boolean saveAs(File saveFile) {
1027 MidiSystem.write(seq, 1, saveFile);
1030 gui.setStatus("Saved " + file.getAbsolutePath());
1032 } catch (IOException e) {
1033 gui.setStatus("Failed in saving " + file.getAbsolutePath());
1039 * Prints the given string to the System output.
1041 private static void out(String text, boolean newLine) {
1042 if (newLine) System.out.println(text);
1043 else System.out.print(text);
1047 * Advances the current progress counter by printing a "." to the System output.
1049 private static void advanceStatus() {
1054 * Releases all reserved devices and exits the program.
1056 public static void quit() {
1058 if (promptOnUnsavedChanges()) return;
1061 if (sequencer.isOpen()) sequencer.close();
1062 if (synthesizer.isOpen()) synthesizer.close();
1067 * A Ccmparator for sorting lists of MidiEvents.
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());
1078 * The thread that updates the GUI during playback.
1080 public static class PlayThread extends Thread {
1082 Thread currentThread = Thread.currentThread();
1083 while(currentThread == player) {
1084 gui.update(sequencer.getTickPosition());
1087 } catch (InterruptedException e) {
1095 * A listener for detecting the Meta event signifying the end of the sequence.
1097 public static class SongEndListener implements MetaEventListener {
1098 public void meta(MetaMessage event) {
1099 if (event.getType() == 47)