1 import javax.sound.midi.*;
7 * Moosique - The MIDI Tracker
9 * Main class that handles initiation, IO and sound.
11 * @author Einar Pehrson
14 public class Moosique {
16 private static MooGUI gui;
17 private static Sequence seq;
18 private static Sequencer sequencer;
19 private static Synthesizer synthesizer;
20 private static Receiver receiver;
21 private static MidiChannel[] channels;
22 private static MidiChannel activeChannel;
24 private static ArrayList copyBuffer, emptyTracks, timeSignatures, tempoChanges;
25 private static TreeSet selection;
26 private static Map trackMute = new HashMap();
27 private static Map trackSolo = new HashMap();
28 private static Thread player;
30 private static File file = null;
31 private static long editPosition;
32 private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
33 public static final int DEFAULT_RESOLUTION = 96, DEFAULT_TRACKS = 4;
34 public static final int WHOLE_NOTE = 0, HALF_NOTE = 1, QUARTER_NOTE = 2, EIGHTH_NOTE = 3, SIXTEENTH_NOTE = 4;
37 * Starts the application.
39 * Parses command-line arguments, acquires MIDI devices and connects them,
40 * loads a sequence and creates the GUI.
42 public static void main (String[] args) {
43 out("\nMoosique version 1.0\n", true);
45 // Parses command-line arguments.
46 String fileArg = null;
47 for (int i = 0; i < args.length; i++) {
48 if (args[i].equals("-n")) makeGUI = false;
49 else if (fileArg == null) fileArg = args[i];
52 // Acquires MIDI devices and connects them.
53 out("Initializing MIDI devices.", false);
55 // Configures sequencer
56 sequencer = MidiSystem.getSequencer();
59 sequencer.addMetaEventListener(new SongEndListener());
61 // Configures synthesizer
62 synthesizer = MidiSystem.getSynthesizer();
66 // Configures receiver, transmitter and channels.
67 receiver = synthesizer.getReceiver();
68 sequencer.getTransmitter().setReceiver(receiver);
69 channels = synthesizer.getChannels();
71 } catch (MidiUnavailableException e) {
72 out("Failed, quitting.", true);
77 // Loads user preferences (work directory, last opened files etc).
80 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
81 if (fileArg != null) {
82 out("Loading MIDI sequence from " + fileArg + "...", false);
83 if (!load(new File(fileArg))) {
84 out("Failed, creating new sequence", true);
90 // Otherwise creates a new empty one.
94 // Builds GUI, unless n-flag is set.
96 out("Building GUI.", false);
98 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
99 } catch (Exception e) {}
100 gui = new MooGUI(seq, file);
103 out("Playing...", false);
105 while (sequencer.isRunning()) {}
119 ** ACCESSOR METHODS **
130 * Returns the currently active MidiChannel.
131 * @return the active MidiChannel
133 public static MidiChannel getActiveChannel() {
134 return activeChannel;
138 * Returns the MidiChannels of the selected synthesizer.
139 * @return the available MidiChannels
141 public static MidiChannel getChannel(int i) {
146 * Returns the current copy buffer.
147 * @return the current copy buffer
149 public static ArrayList getCopyBuffer() {
154 * Returns the current editing position of the sequencer.
155 * @return the tick position
157 public static long getEditPosition() {
165 public static MooGUI getGUI() {
170 * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
171 * @param tickPosition the tick position for which to calculate the position
172 * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
174 public static int[] getPositionForTicks(long tickPosition) {
175 int ticksPerBeat = seq.getResolution(), sigs = timeSignatures.size(), beatsPerMeasure = 4;
176 long measures = 0, beats = 0, ticks = 0;
179 Iterator it = timeSignatures.iterator();
180 MidiEvent lastTSEvent = (MidiEvent)it.next();
181 if (lastTSEvent.getTick() != 0) tickPos += (int)lastTSEvent.getTick();
182 while(it.hasNext()) {
183 MidiEvent nextTSEvent = (MidiEvent)it.next();
184 long tickDiff = nextTSEvent.getTick() - lastTSEvent.getTick();
185 ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
186 beatsPerMeasure = ts[0] * (4 / ts[1]);
187 tickPos += ((beatsPerMeasure * measures + beats) * res + ticks);
192 MidiEvent TSEvent = (MidiEvent)timeSignatures.get(0);
193 int[] ts = decodeTimeSig(((MetaMessage)TSEvent.getMessage()).getData());
194 beatsPerMeasure = ts[0] * (4 / ts[1]);
196 measures = tickPosition / (beatsPerMeasure * ticksPerBeat);
197 beats = (tickPosition - measures * beatsPerMeasure * ticksPerBeat) / ticksPerBeat;
198 ticks = tickPosition - measures * beatsPerMeasure * ticksPerBeat - beats * ticksPerBeat;
200 int[] pos = {(int)measures + 1, (int)beats + 1, (int)ticks + 1};
205 * Returns the receiver of the current sequencer.
206 * @return the receiver
208 public static Receiver getReceiver() {
213 * Returns the current sequence.
214 * @return the current sequence
216 public static Sequence getSequence() {
221 * Returns the current sequencer.
222 * @return the current sequencer
224 public static Sequencer getSequencer() {
229 * Returns the tempo in the given tick position.
230 * @param tick the tick position for which to return the tempo
231 * @return the tempo at the specified tick position
233 public static int getTempo(long tick) {
234 if (tempoChanges.size() == 0) return 120;
235 MidiEvent tempoEvent = (MidiEvent)tempoChanges.get(0);
236 if (tempoChanges.size() > 1) {
237 for (int i = 1; i < tempoChanges.size(); i++) {
238 MidiEvent nextTempoEvent = (MidiEvent)tempoChanges.get(i);
239 if (nextTempoEvent.getTick() < tick && nextTempoEvent.getTick() > tempoEvent.getTick())
240 tempoEvent = nextTempoEvent;
243 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
247 * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
248 * @return the tick position.
250 public static long getTicksForPosition(int measures, int beats, int ticks) {
251 int res = seq.getResolution();
253 switch (timeSignatures.size()) {
255 tickPos = (4 * measures + beats) * res + ticks;
257 MidiEvent TSEvent = (MidiEvent)timeSignatures.get(0);
258 int[] ts = decodeTimeSig(((MetaMessage)TSEvent.getMessage()).getData());
259 int beatsPerMeasure = ts[0] * (4 / ts[1]);
260 tickPos = (beatsPerMeasure * measures + beats) * res + ticks;
262 Iterator it = timeSignatures.iterator();
263 MidiEvent lastTSEvent = (MidiEvent)it.next();
264 if (lastTSEvent.getTick() != 0) tickPos += (int)lastTSEvent.getTick();
265 while(it.hasNext()) {
266 MidiEvent nextTSEvent = (MidiEvent)it.next();
267 long tickDiff = nextTSEvent.getTick() - lastTSEvent.getTick();
268 ts = decodeTimeSig(((MetaMessage)lastTSEvent.getMessage()).getData());
269 beatsPerMeasure = ts[0] * (4 / ts[1]);
270 tickPos += ((beatsPerMeasure * measures + beats) * res + ticks);
277 * Returns the time signature in the given tick position.
278 * @param tick the tick position for which to return the time signature
279 * @return an array of two integers where [0] is the numerator and [1] the denominator
281 public static int[] getTimeSig(long tick) {
283 if (timeSignatures.size() == 0) return ts;
284 MidiEvent timeSigEvent = (MidiEvent)timeSignatures.get(0);
285 if (timeSignatures.size() > 1) {
286 for (int i = 1; i < timeSignatures.size(); i++) {
287 MidiEvent nextTimeSigEvent = (MidiEvent)timeSignatures.get(i);
288 if (nextTimeSigEvent.getTick() < tick && nextTimeSigEvent.getTick() > timeSigEvent.getTick())
289 timeSigEvent = nextTimeSigEvent;
292 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
296 * Returns true if the current sequence has been edited.
297 * @return the tick position
299 public static boolean isEdited() {
304 * Returns whether the given track should be drawn
305 * @return true if the given track should be drawn
307 public static boolean shouldBeDrawn(Track track) {
308 if (drawEmptyTracks) return true;
309 else return (!emptyTracks.contains(track));
320 ** MUTATOR METHODS **
331 * Fast forwards the current sequence the given number of measures.
332 * @param measures the number of measures to fast forward
334 public static void forward(long ticks) {
335 editPosition += ticks;
339 * Rewinds the current sequence the given number of measures.
340 * @param measures the number of measures to rewind
342 public static void rewind(long ticks) {
343 editPosition -= ticks;
347 * Sets the currently active MidiChannel.
348 * @param channel the number of the MidiChannel to activate
350 public static void setActiveChannel(int channel) {
351 activeChannel = channels[channel];
355 * Sets the current copy buffer.
356 * @param the copy buffer
358 public static void setCopyBuffer(ArrayList buffer) {
363 * Sets whether empty tracks should be drawn
364 * @param state true if empty tracks should be drawn
366 public static void setDrawEmptyTracks(boolean state) {
367 drawEmptyTracks = state;
371 * Sets the current sequence as edited, which implies prompts when loading a new sequence.
373 public static void setEdited() {
378 * Sets the current editing position of the sequencer.
379 * @param ticks the tick position
381 public static void setEditPosition(long ticks) {
382 editPosition = ticks;
386 * Sets the current editing position of the sequencer.
387 * @param ticks the tick position
389 public static void setTempo(int bpm) {
394 * Sets the current editing position of the sequencer.
395 * @param ticks the tick position
397 public static void setTimeSig(int bpm) {
402 * Sets the solo setting of the given track.
403 * @param on true for solo, false for not
405 public static void setTrackSolo(Track track, boolean on){
406 trackSolo.put(track, new Boolean(on));
410 * Sets the mute setting of the given track.
411 * @param on true for mute, false for not
413 public static void setTrackMute(Track track, boolean on){
414 trackMute.put(track, new Boolean(on));
418 * Sets the current playback volume.
419 * @param volume the volume, between 0 and 1
421 public void setVolume(long volume) {
422 for (int i = 0; i < channels.length; i++) {
423 channels[i].controlChange(7, (int)(volume * 127.0));
435 ** ENCODING / DECODING METHODS **
446 * Returns the byte array for the given tempo.
447 * @param tempo the tempo in beats per minute
448 * @return an array of bytes representing the given tempo
450 public static byte[] encodeTempo(int tempo) {
451 int microSecsPerQuarter = 60000000 / tempo;
452 byte[] b = new byte[3];
453 b[0] = (byte)(microSecsPerQuarter / 65536);
454 b[1] = (byte)((microSecsPerQuarter - (b[0] * 65536)) / 256);
455 b[2] = (byte)(microSecsPerQuarter - (b[0] * 65536) - (b[1] * 256));
460 * Returns the tempo for the given byte array.
461 * @param an array of three bytes representing the tempo
462 * @return the tempo in beats per minute
464 public static int decodeTempo(byte[] bytes) {
465 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]);
469 * Returns the byte array for the given time signature.
470 * @param numerator the numerator of the time signature
471 * @param denominator the denominator of the time signature
472 * @return an array of bytes representing the given time signature
474 public static byte[] encodeTimeSig(int numerator, int denominator) {
477 (byte)(Math.log(denominator) / Math.log(2)), // logarithm of denominator in base 2
484 * Returns the time signature for the given byte array.
485 * @param an array of four bytes representing the time signature
486 * @return an array of two integers where [0] is the numerator and [1] the denominator
488 public static int[] decodeTimeSig(byte[] bytes) {
504 ** SELECTION METHODS **
515 * Returns the current selection.
516 * @return the current selection
518 public static TreeSet getSelection() {
523 * Selects the given note
524 * @param the note to select
526 public static void selectNote(MooNoteElement elem) {
531 * Deselects the given note
532 * @param the note to deselect
534 public static void deselectNote(MooNoteElement elem) {
535 selection.remove(elem);
539 * Deselects all notes.
541 public static void deselectAllNotes() {
542 Iterator it = selection.iterator();
543 while(it.hasNext()) {
544 ((MooNoteElement)it.next()).deselect();
550 * Determines if the given MooNoteElement is the only one in the track view that is selected.
551 * @return if the given element is the only selected one
553 public static boolean isTheOnlySelected(MooNoteElement elem) {
554 Iterator it = selection.iterator();
555 while(it.hasNext()) {
556 if (!it.next().equals(elem)) return false;
569 ** PLAYBACK METHODS **
580 * Starts playback of the current sequence.
582 public static void play() {
583 sequencer.setTickPosition(editPosition);
588 * Resumes playback of the current sequence.
590 public static void resume() {
593 sequencer.setSequence(seq);
594 } catch (InvalidMidiDataException e) {}
595 Track[] tracks = seq.getTracks();
599 for (int i = 0; i < tracks.length; i++) {
601 Object ob = trackSolo.get(tracks[i]);
602 if(ob instanceof Boolean){
603 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
606 ob = trackMute.get(tracks[i]);
607 if(ob instanceof Boolean){
608 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
612 // Disables input to volatile components
615 // Creates the visualisation thread and starts it.
616 player = new PlayThread();
621 * Pauses playback of the current sequence.
623 public static void pause() {
624 if (sequencer.isRunning()) {
631 * Stops playback of the current sequence.
633 public static void stop() {
634 if (sequencer.isRunning()) {
637 sequencer.setTickPosition(editPosition);
650 ** SYSTEM & IO METHODS **
661 * Replaces the current sequence with a new one, holding three empty tracks.
663 public static void clearSequence() {
665 // Creates a new sequence.
666 seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
667 Track[] tracks = seq.getTracks();
669 // Creates messages for default tempo (120) and time signature (4/4), and adds them to track 0.
670 MetaMessage timeSigMsg = new MetaMessage();
671 MetaMessage tempoMsg = new MetaMessage();
673 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
674 tempoMsg.setMessage(81, encodeTempo(120), 3);
675 } catch (InvalidMidiDataException e) {}
676 tracks[0].add(new MidiEvent(timeSigMsg, (long)0));
677 tracks[0].add(new MidiEvent(tempoMsg, (long)0));
679 // Sets program and title for the tracks.
680 initializeTrack(tracks[1], 0, 24, "Guitar");
681 initializeTrack(tracks[2], 1, 33, "Bass");
682 initializeTrack(tracks[3], 9, 0, "Drums");
683 } catch (InvalidMidiDataException e) {}
685 // Reinitializes sequence variables
687 reinitializeVariables();
689 // Sends the sequence to the GUI.
690 if (gui != null) gui.setSequence(seq, null);
694 * Creates event in the given track for program change and title.
696 private static void initializeTrack(Track track, int channel, int program, String title) {
697 // Creates program change and title message.
698 ShortMessage programMsg = new ShortMessage();
699 MetaMessage titleMsg = new MetaMessage();
701 // Sets the data of the messages.
703 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
704 titleMsg.setMessage(3, title.getBytes(), title.length());
705 } catch (InvalidMidiDataException e) {}
707 // Sends the program change to the channel
708 getChannel(channel).programChange(program);
710 // Adds them to the track.
711 track.add(new MidiEvent(programMsg, (long)0));
712 track.add(new MidiEvent(titleMsg, (long)0));
716 * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
717 * @param track the track to convert
718 * @param quantize whether to round locations and durations in the track to nearest 16th
719 * @return a list of the created MooNotes
721 public static List convertTrack(Track track, boolean quantize) {
722 // Searches the track for NoteOn and NoteOff events
723 ArrayList noteOns = new ArrayList(track.size() / 2);
724 ArrayList noteOffs = new ArrayList(track.size() / 2);
725 ArrayList newMooNotes = new ArrayList();
727 for (int j = 0; j < track.size(); j++) {
728 event = track.get(j);
729 if (event.getMessage().getStatus() >= 144 &&
730 event.getMessage().getStatus() < 160) noteOns.add(event);
731 if (event.getMessage().getStatus() >= 128 &&
732 event.getMessage().getStatus() < 144) noteOffs.add(event);
734 noteOns.trimToSize();
735 noteOffs.trimToSize();
736 if (noteOns.size() == 0) emptyTracks.add(track);
738 // Sorts the note lists by tick position.
739 Comparator c = new MidiEventComparator();
740 Collections.sort(noteOns, c);
741 Collections.sort(noteOffs, c);
743 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
744 Iterator iOn = noteOns.iterator(), iOff;
745 MidiEvent on, off = null, nextOff;
746 ShortMessage onMsg, nextOffMsg;
747 while(iOn.hasNext()) {
748 on = (MidiEvent)iOn.next();
749 if (!(on instanceof MooNote)) {
750 onMsg = (ShortMessage)on.getMessage();
751 iOff = noteOffs.iterator();
752 while(iOff.hasNext()) {
753 nextOff = (MidiEvent)iOff.next();
754 nextOffMsg = (ShortMessage)nextOff.getMessage();
755 if(onMsg.getChannel() == nextOffMsg.getChannel() &&
756 onMsg.getData1() == nextOffMsg.getData1() &&
757 c.compare(nextOff, on) > 0) {
767 mn = new MooNote(on, off);
769 mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
776 if (quantize) quantize(newMooNotes, SIXTEENTH_NOTE, true, true);
781 * Loads a MIDI sequence from the given file.
782 * @param filename the filename to use
784 public static boolean load(File loadFile) {
785 // Loads sequence from file
786 try {seq = MidiSystem.getSequence(loadFile);}
787 catch (Exception e) {return false;}
791 Track[] tracks = seq.getTracks();
792 reinitializeVariables();
794 // Searches track 0 for changes in tempo and time signature.
797 for (int i = 0; i < tracks[0].size(); i++) {
798 event = tracks[0].get(i);
799 if (event.getMessage().getStatus() == MetaMessage.META) {
800 metaMsg = (MetaMessage)event.getMessage();
801 switch(metaMsg.getType()) {
802 case 81: tempoChanges.add(event); break;
803 case 88: timeSignatures.add(event);
807 Comparator c = new MidiEventComparator();
808 Collections.sort(tempoChanges, c);
809 Collections.sort(timeSignatures, c);
812 for (int i = 0; i < tracks.length; i++) {
813 convertTrack(tracks[i], false);
816 // Sends sequence to GUI and sequencer, then returns
817 if (gui != null) gui.setSequence(seq, file);
819 sequencer.setSequence(seq);
820 } catch (InvalidMidiDataException e) {}
825 * Quantizes the given list of MIDI events
826 * @param notes a list of the notes to quantize
827 * @param resolution the note size to round each note to
828 * @param location whether the quantize should affect the location of the note
829 * @param duration whether the quantize should affect the duration of the note
831 public static void quantize(List notes, int resolution, boolean location, boolean duration) {
832 // Math.round(mn.getTick() / ticksPerSixteenth);
836 * Reinitializes sequence-specific variables.
838 private static void reinitializeVariables() {
839 emptyTracks = new ArrayList();
840 timeSignatures = new ArrayList();
841 tempoChanges = new ArrayList();
842 trackSolo = new HashMap();
843 trackMute = new HashMap();
844 copyBuffer = new ArrayList();
845 selection = new TreeSet();
849 * Loads the user preferences.
851 public static void loadPreferences() {
856 * Saves the user preferences.
858 public static void savePreferences() {
863 * Prompts the user to save any unsaved changes.
865 public static boolean promptOnUnsavedChanges() {
866 if (!edited) return false;
867 int exitOption = JOptionPane.showConfirmDialog(gui,
868 "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
869 "File not saved - continue?",
870 JOptionPane.OK_CANCEL_OPTION,
871 JOptionPane.WARNING_MESSAGE);
872 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
877 * Saves the current sequence to the previously given filename.
879 public static boolean save() {
880 if (file == null) return false;
888 * Saves the current sequence to the given filename
889 * @param file the filename to use
891 public static boolean saveAs(File saveFile) {
893 MidiSystem.write(seq, 1, saveFile);
896 gui.setStatus("Saved " + file.getAbsolutePath());
898 } catch (IOException e) {
899 gui.setStatus("Failed in saving " + file.getAbsolutePath());
905 * Prints the given string to the System output.
907 private static void out(String text, boolean newLine) {
908 if (newLine) System.out.println(text);
909 else System.out.print(text);
913 * Advances the current progress counter by printing a "." to the System output.
915 private static void advanceStatus() {
920 * Releases all reserved devices and exits the program.
922 public static void quit() {
924 if (promptOnUnsavedChanges()) return;
927 if (sequencer.isOpen()) sequencer.close();
928 if (synthesizer.isOpen()) synthesizer.close();
933 * A Ccmparator for sorting lists of MidiEvents.
935 public static class MidiEventComparator implements Comparator {
936 public int compare(Object o1, Object o2) {
937 int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
938 if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
939 return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
944 * The thread that updates the GUI during playback.
946 public static class PlayThread extends Thread {
948 Thread currentThread = Thread.currentThread();
949 while(currentThread == player) {
950 gui.update(sequencer.getTickPosition());
953 } catch (InterruptedException e) {
961 * A listener for detecting the Meta event signifying the end of the sequence.
963 public static class SongEndListener implements MetaEventListener {
964 public void meta(MetaMessage event) {
965 if (event.getType() == 47)