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 Map trackMute = new HashMap();
26 private static Map trackSolo = new HashMap();
27 private static Thread player;
29 private static File file = null;
30 private static long editPosition;
31 private static boolean makeGUI = true, initSound = true, edited = false, drawEmptyTracks = false;
32 public static final int DEFAULT_RESOLUTION = 96, DEFAULT_TRACKS = 4;
33 public static final int WHOLE_NOTE = 0, HALF_NOTE = 1, QUARTER_NOTE = 2, EIGHTH_NOTE = 3, SIXTEENTH_NOTE = 4;
36 * Starts the application.
38 * Parses command-line arguments, acquires MIDI devices and connects them,
39 * loads a sequence and creates the GUI.
41 public static void main (String[] args) {
42 out("\nMoosique version 1.0\n");
44 // Parses command-line arguments.
45 String fileArg = null;
46 for (int i = 0; i < args.length; i++) {
47 if (args[i].equals("-n")) makeGUI = false;
48 else if (fileArg == null) fileArg = args[i];
51 // Acquires MIDI devices and connects them.
52 System.out.print("Initializing MIDI devices.");
54 // Configures sequencer
55 sequencer = MidiSystem.getSequencer();
56 System.out.print(".");
58 sequencer.addMetaEventListener(new SongEndListener());
60 // Configures synthesizer
61 synthesizer = MidiSystem.getSynthesizer();
62 System.out.print(".");
65 // Configures receiver, transmitter and channels.
66 receiver = synthesizer.getReceiver();
67 sequencer.getTransmitter().setReceiver(receiver);
68 channels = synthesizer.getChannels();
70 } catch (MidiUnavailableException e) {
71 out("Failed, quitting.");
76 // Loads user preferences (work directory, last opened files etc).
79 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
80 if (fileArg != null) {
81 System.out.print("Loading MIDI sequence from " + fileArg + "...");
82 if (!load(new File(fileArg))) {
83 out("Failed, creating new sequence");
89 // Otherwise creates a new empty one.
93 // Builds GUI, unless n-flag is set.
95 System.out.print("Building GUI.");
97 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
98 } catch (Exception e) {}
99 gui = new MooGUI(seq, file);
102 System.out.print("Playing...");
104 while (sequencer.isRunning()) {}
118 ** ACCESSOR METHODS **
129 * Returns the currently active MidiChannel.
130 * @return the active MidiChannel
132 public static MidiChannel getActiveChannel() {
133 return activeChannel;
137 * Returns the MidiChannels of the selected synthesizer.
138 * @return the available MidiChannels
140 public static MidiChannel getChannel(int i) {
145 * Returns the current copy buffer.
146 * @return the current copy buffer
148 public static ArrayList getCopyBuffer() {
153 * Returns the current editing position of the sequencer.
154 * @return the tick position
156 public static long getEditPosition() {
164 public static MooGUI getGUI() {
169 * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
170 * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
172 public static int[] getPositionForTicks(long ticks) {
174 int measures, beats, ticks;
175 for (int i = 0; i < timeSignatures.length; i++) {
176 long tick = timeSignatures[i].getTick();
177 // Split the ticks in the interval into measures, beats and ticks.
180 int[] pos = {1, 1, 1};
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 the tempo in the given tick position.
210 * @param tick the tick position for which to return the tempo
211 * @return the tempo at the specified tick position
213 public static int getTempo(long tick) {
214 if (tempoChanges.size() == 0) return 120;
215 MidiEvent tempoEvent = (MidiEvent)tempoChanges.get(0);
216 if (tempoChanges.size() > 1) {
217 for (int i = 1; i < tempoChanges.size(); i++) {
218 MidiEvent nextTempoEvent = (MidiEvent)tempoChanges.get(i);
219 if (nextTempoEvent.getTick() < tick && nextTempoEvent.getTick() > tempoEvent.getTick())
220 tempoEvent = nextTempoEvent;
223 return decodeTempo(((MetaMessage)tempoEvent.getMessage()).getData());
227 * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
228 * @return the tick position.
230 public static long getTicksForPosition(int measures, int beats, int ticks) {
233 for (int i = 0; i < timeSignatures.length; i++) {
234 long tick = timeSignatures[i].getTick();
235 // Add the measures, beats and ticks in the interval.
242 * Returns the time signature in the given tick position.
243 * @param tick the tick position for which to return the time signature
244 * @return an array of two integers where [0] is the numerator and [1] the denominator
246 public static int[] getTimeSig(long tick) {
248 if (timeSignatures.size() == 0) return ts;
249 MidiEvent timeSigEvent = (MidiEvent)timeSignatures.get(0);
250 if (timeSignatures.size() > 1) {
251 for (int i = 1; i < timeSignatures.size(); i++) {
252 MidiEvent nextTimeSigEvent = (MidiEvent)timeSignatures.get(i);
253 if (nextTimeSigEvent.getTick() < tick && nextTimeSigEvent.getTick() > timeSigEvent.getTick())
254 timeSigEvent = nextTimeSigEvent;
257 return decodeTimeSig(((MetaMessage)timeSigEvent.getMessage()).getData());
261 * Returns true if the current sequence has been edited.
262 * @return the tick position
264 public static boolean isEdited() {
269 * Returns whether the given track should be drawn
270 * @return true if the given track should be drawn
272 public static boolean shouldBeDrawn(Track track) {
273 if (drawEmptyTracks) return true;
274 else return (!emptyTracks.contains(track));
285 ** MUTATOR METHODS **
296 * Fast forwards the current sequence the given number of measures.
297 * @param measures the number of measures to fast forward
299 public static void forward(long ticks) {
300 editPosition += ticks;
304 * Rewinds the current sequence the given number of measures.
305 * @param measures the number of measures to rewind
307 public static void rewind(long ticks) {
308 editPosition -= ticks;
312 * Sets the currently active MidiChannel.
313 * @param channel the number of the MidiChannel to activate
315 public static void setActiveChannel(int channel) {
316 activeChannel = channels[channel];
320 * Sets the current copy buffer.
321 * @param the copy buffer
323 public static void setCopyBuffer(ArrayList buffer) {
328 * Sets whether empty tracks should be drawn
329 * @param state true if empty tracks should be drawn
331 public static void setDrawEmptyTracks(boolean state) {
332 drawEmptyTracks = state;
336 * Sets the current sequence as edited, which implies prompts when loading a new sequence.
338 public static void setEdited() {
343 * Sets the current editing position of the sequencer.
344 * @param ticks the tick position
346 public static void setEditPosition(long ticks) {
347 editPosition = ticks;
351 * Sets the current editing position of the sequencer.
352 * @param ticks the tick position
354 public static void setTempo(int bpm) {
359 * Sets the current editing position of the sequencer.
360 * @param ticks the tick position
362 public static void setTimeSig(int bpm) {
367 * Sets the solo setting of the given track.
368 * @param on true for solo, false for not
370 public static void setTrackSolo(Track track, boolean on){
371 trackSolo.put(track, new Boolean(on));
375 * Sets the mute setting of the given track.
376 * @param on true for mute, false for not
378 public static void setTrackMute(Track track, boolean on){
379 trackMute.put(track, new Boolean(on));
383 * Sets the current playback volume.
384 * @param volume the volume, between 0 and 1
386 public void setVolume(long volume) {
387 for (int i = 0; i < channels.length; i++) {
388 channels[i].controlChange(7, (int)(volume * 127.0));
400 ** ENCODING / DECODING METHODS **
411 * Returns the byte array for the given tempo.
412 * @param tempo the tempo in beats per minute
413 * @return an array of bytes representing the given tempo
415 public static byte[] encodeTempo(int tempo) {
416 int microSecsPerQuarter = 60000000 / tempo;
417 byte[] b = new byte[3];
418 b[0] = (byte)(microSecsPerQuarter / 65536);
419 b[1] = (byte)((microSecsPerQuarter - (b[0] * 65536)) / 256);
420 b[2] = (byte)(microSecsPerQuarter - (b[0] * 65536) - (b[1] * 256));
425 * Returns the tempo for the given byte array.
426 * @param an array of three bytes representing the tempo
427 * @return the tempo in beats per minute
429 public static int decodeTempo(byte[] bytes) {
430 return 60000000 / (bytes[0] * 65536 + bytes[1] * 256 + bytes[2]); // bytes[0] & 0xFF ???
434 * Returns the byte array for the given time signature.
435 * @param numerator the numerator of the time signature
436 * @param denominator the denominator of the time signature
437 * @return an array of bytes representing the given time signature
439 public static byte[] encodeTimeSig(int numerator, int denominator) {
442 (byte)(Math.log(denominator) / Math.log(2)),
449 * Returns the time signature for the given byte array.
450 * @param an array of four bytes representing the time signature
451 * @return an array of two integers where [0] is the numerator and [1] the denominator
453 public static int[] decodeTimeSig(byte[] bytes) {
469 ** PLAYBACK METHODS **
480 * Starts playback of the current sequence.
482 public static void play() {
483 sequencer.setTickPosition(editPosition);
488 * Resumes playback of the current sequence.
490 public static void resume() {
493 sequencer.setSequence(seq);
494 } catch (InvalidMidiDataException e) {}
495 Track[] tracks = seq.getTracks();
499 for (int i = 0; i < tracks.length; i++) {
501 Object ob = trackSolo.get(tracks[i]);
502 if(ob instanceof Boolean){
503 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
506 ob = trackMute.get(tracks[i]);
507 if(ob instanceof Boolean){
508 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
512 // Disables input to volatile components
515 // Creates the visualisation thread and starts it.
516 player = new PlayThread();
521 * Pauses playback of the current sequence.
523 public static void pause() {
524 if (sequencer.isRunning()) {
527 if (player != null) player.interrupt();
531 * Stops playback of the current sequence.
533 public static void stop() {
534 if (sequencer.isRunning()) {
537 sequencer.setTickPosition(editPosition);
538 if (player != null) player.interrupt();
550 ** SYSTEM & IO METHODS **
561 * Replaces the current sequence with a new one, holding three empty tracks.
563 public static void clearSequence() {
565 // Creates a new sequence.
566 seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
567 Track[] tracks = seq.getTracks();
569 // Creates messages for default tempo (120) and time signature (4/4).
570 MetaMessage timeSigMsg = new MetaMessage();
571 MetaMessage tempoMsg = new MetaMessage();
573 timeSigMsg.setMessage(88, encodeTimeSig(4, 4), 4);
574 tempoMsg.setMessage(81, encodeTempo(120), 3);
575 } catch (InvalidMidiDataException e) {}
577 // Adds them to track 0.
578 tracks[0].add(new MidiEvent(timeSigMsg, (long)0));
579 tracks[0].add(new MidiEvent(tempoMsg, (long)0));
581 // Sets program and title for the tracks.
582 initializeTrack(tracks[1], 0, 24, "Guitar");
583 initializeTrack(tracks[2], 1, 33, "Bass");
584 initializeTrack(tracks[3], 9, 0, "Drums");
587 emptyTracks = new ArrayList();
588 timeSignatures = new ArrayList();
589 tempoChanges = new ArrayList();
590 trackSolo = new HashMap();
591 trackMute = new HashMap();
592 copyBuffer = new ArrayList();
593 } catch (InvalidMidiDataException e) {}
594 // Sends the sequence to the GUI.
595 if (gui != null) gui.setSequence(seq);
599 * Creates event in the given track for program change and title.
601 private static void initializeTrack(Track track, int channel, int program, String title) {
602 // Creates program change and title message.
603 ShortMessage programMsg = new ShortMessage();
604 MetaMessage titleMsg = new MetaMessage();
606 // Sets the data of the messages.
608 programMsg.setMessage(ShortMessage.PROGRAM_CHANGE, channel, program, 0);
609 titleMsg.setMessage(3, title.getBytes(), title.length());
610 } catch (InvalidMidiDataException e) {}
612 // Adds them to the track.
613 track.add(new MidiEvent(programMsg, (long)0));
614 track.add(new MidiEvent(titleMsg, (long)0));
618 * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
619 * @param track the track to convert
620 * @param quantize whether to round locations and durations in the track to nearest 16th
621 * @return a list of the created MooNotes
623 public static List convertTrack(Track track, boolean quantize) {
624 // Searches the track for NoteOn and NoteOff events
625 ArrayList noteOns = new ArrayList(track.size() / 2);
626 ArrayList noteOffs = new ArrayList(track.size() / 2);
627 ArrayList newMooNotes = new ArrayList();
629 for (int j = 0; j < track.size(); j++) {
630 event = track.get(j);
631 if (event.getMessage().getStatus() >= 144 &&
632 event.getMessage().getStatus() < 160) noteOns.add(event);
633 if (event.getMessage().getStatus() >= 128 &&
634 event.getMessage().getStatus() < 144) noteOffs.add(event);
636 noteOns.trimToSize();
637 noteOffs.trimToSize();
638 if (noteOns.size() == 0) emptyTracks.add(track);
640 // Sorts the note lists by tick position.
641 Comparator c = new MidiEventComparator();
642 Collections.sort(noteOns, c);
643 Collections.sort(noteOffs, c);
645 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
646 Iterator iOn = noteOns.iterator(), iOff;
647 MidiEvent on, off = null, nextOff;
648 ShortMessage onMsg, nextOffMsg;
649 while(iOn.hasNext()) {
650 on = (MidiEvent)iOn.next();
651 if (!(on instanceof MooNote)) {
652 onMsg = (ShortMessage)on.getMessage();
653 iOff = noteOffs.iterator();
654 while(iOff.hasNext()) {
655 nextOff = (MidiEvent)iOff.next();
656 nextOffMsg = (ShortMessage)nextOff.getMessage();
657 if(onMsg.getChannel() == nextOffMsg.getChannel() &&
658 onMsg.getData1() == nextOffMsg.getData1() &&
659 c.compare(nextOff, on) > 0) {
669 mn = new MooNote(on, off);
671 mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
678 if (quantize) quantize(newMooNotes, SIXTEENTH_NOTE, true, true);
683 * Loads a MIDI sequence from the given file.
684 * @param filename the filename to use
686 public static boolean load(File loadFile) {
687 // Loads sequence from file
688 try {seq = MidiSystem.getSequence(loadFile);}
689 catch (Exception e) {return false;}
693 Track[] tracks = seq.getTracks();
694 emptyTracks = new ArrayList();
695 timeSignatures = new ArrayList();
696 tempoChanges = new ArrayList();
697 trackMute = new HashMap();
698 trackSolo = new HashMap();
699 copyBuffer = new ArrayList();
701 // Searches track 0 for changes in tempo and time signature.
704 ArrayList ts = new ArrayList(), tc = new ArrayList();
705 for (int i = 0; i < tracks[0].size(); i++) {
706 event = tracks[0].get(i);
707 if (event.getMessage().getStatus() == MetaMessage.META) {
708 metaMsg = (MetaMessage)event.getMessage();
709 switch(metaMsg.getType()) {
710 case 81: tempoChanges.add(event); break;
711 case 88: timeSignatures.add(event);
717 for (int i = 0; i < tracks.length; i++) {
718 convertTrack(tracks[i], false);
721 // Sends sequence to GUI and sequencer, then returns
722 if (gui != null) gui.setSequence(seq);
724 sequencer.setSequence(seq);
725 } catch (InvalidMidiDataException e) {}
730 * Quantizes the given list of MIDI events
731 * @param notes a list of the notes to quantize
732 * @param resolution the note size to round each note to
733 * @param location whether the quantize should affect the location of the note
734 * @param duration whether the quantize should affect the duration of the note
736 public static void quantize(List notes, int resolution, boolean location, boolean duration) {
737 // Math.round(mn.getTick() / ticksPerSixteenth);
741 * Loads the user preferences.
743 public static void loadPreferences() {
748 * Saves the user preferences.
750 public static void savePreferences() {
757 public static boolean promptOnUnsavedChanges() {
758 if (!edited) return false;
759 int exitOption = JOptionPane.showConfirmDialog(gui,
760 "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
761 "File not saved - continue?",
762 JOptionPane.OK_CANCEL_OPTION,
763 JOptionPane.WARNING_MESSAGE);
764 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
769 * Saves the current sequence to the previously given filename.
771 public static boolean save() {
772 if (file == null) return false;
780 * Saves the current sequence to the given filename
781 * @param file the filename to use
783 public static boolean saveAs(File saveFile) {
785 MidiSystem.write(seq, 1, saveFile);
788 gui.setStatus("Saved " + file.getAbsolutePath());
790 } catch (IOException e) {
791 gui.setStatus("Failed in saving " + file.getAbsolutePath());
797 * Prints the given string to the System output.
799 private static void out(String text) {
800 System.out.println(text);
804 * Releases all reserved devices and exits the program.
806 public static void quit() {
808 if (promptOnUnsavedChanges()) return;
811 if (sequencer.isOpen()) sequencer.close();
812 if (synthesizer.isOpen()) synthesizer.close();
817 * A Ccmparator for sorting lists of MidiEvents.
819 public static class MidiEventComparator implements Comparator {
820 public int compare(Object o1, Object o2) {
821 int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
822 if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
823 return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
828 * The thread that updates the GUI during playback.
830 public static class PlayThread extends Thread {
832 // Updates the GUI with the current tick position.
833 gui.update(sequencer.getTickPosition());
835 // Puts the thread to sleep for as long as it takes
836 // the sequencer to reach the next sixteenth.
838 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
840 } catch (InterruptedException e) {
847 * A listener for detecting the end of the sequence.
849 public static class SongEndListener implements MetaEventListener {
850 public void meta(MetaMessage event) {
851 if (event.getType() == 47)