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;
23 private static MidiEvent[] timeSignatures, tempoChanges;
25 private static ArrayList copyBuffer, emptyTracks;
26 private static Map trackMute = new HashMap();
27 private static Map trackSolo = new HashMap();
28 private static Thread player;
30 private static String filename;
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;
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 System.out.println("\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 (args[i].equals("-m")) initSound = false;
49 else if (fileArg == null) fileArg = args[i];
52 // Acquires MIDI devices and connects them.
53 System.out.print("Initializing MIDI devices.");
55 sequencer = MidiSystem.getSequencer();
56 System.out.print(".");
58 synthesizer = MidiSystem.getSynthesizer();
59 System.out.print(".");
61 receiver = synthesizer.getReceiver();
62 sequencer.getTransmitter().setReceiver(receiver);
63 channels = synthesizer.getChannels();
65 } catch (MidiUnavailableException e) {
66 System.out.println("Failed, quitting.");
69 System.out.println("Done");
71 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
72 if (fileArg != null) {
73 System.out.print("Loading MIDI sequence from " + fileArg + "...");
75 System.out.println("Failed, creating new sequence");
78 System.out.println("Done");
81 // Otherwise creates a new empty one.
85 // Builds GUI, unless n-flag is set.
87 System.out.print("Building GUI.");
89 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
90 } catch (Exception e) {}
91 gui = new MooGUI(seq);
92 System.out.println("Done");
94 System.out.print("Playing...");
96 while (sequencer.isRunning()) {}
97 System.out.println("Done");
110 ** ACCESSOR METHODS **
121 * Returns the currently active MidiChannel.
122 * @return the active MidiChannel
124 public static MidiChannel getActiveChannel() {
125 return activeChannel;
129 * Returns the MidiChannels of the selected synthesizer.
130 * @return the available MidiChannels
132 public static MidiChannel getChannel(int i) {
137 * Returns the MidiChannels of the selected synthesizer.
138 * @return the available MidiChannels
140 public static MidiChannel[] getChannels() {
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[] getPositionForTick(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 of the current sequence.
210 * @return the tick position
212 public static int getTempo() {
214 // if (tempoMsg == null) return 0;
218 * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
219 * @return the tick position.
221 public static long getTicksForPosition(int measures, int beats, int ticks) {
224 for (int i = 0; i < timeSignatures.length; i++) {
225 long tick = timeSignatures[i].getTick();
226 // Add the measures, beats and ticks in the interval.
233 * Returns the tempo of the current sequence.
234 * @return the tick position
236 public static int[] getTimeSig() {
239 // if (timeSigMsg == null) return 0;
243 * Returns true if the current sequence has been edited.
244 * @return the tick position
246 public static boolean isEdited() {
251 * Returns whether the given track should be drawn
252 * @return true if the given track should be drawn
254 public static boolean shouldBeDrawn(Track track) {
255 if (drawEmptyTracks) return true;
256 else return (!emptyTracks.contains(track));
267 ** MUTATOR METHODS **
278 * Fast forwards the current sequence the given number of measures.
279 * @param measures the number of measures to fast forward
281 public static void forward(long ticks) {
282 editPosition += ticks;
286 * Rewinds the current sequence the given number of measures.
287 * @param measures the number of measures to rewind
289 public static void rewind(long ticks) {
290 editPosition -= ticks;
294 * Sets the currently active MidiChannel.
295 * @param channel the number of the MidiChannel to activate
297 public static void setActiveChannel(int channel) {
298 activeChannel = channels[channel];
302 * Sets the current copy buffer.
303 * @param the copy buffer
305 public static void setCopyBuffer(ArrayList buffer) {
310 * Sets whether empty tracks should be drawn
311 * @param state true if empty tracks should be drawn
313 public static void setDrawEmptyTracks(boolean state) {
314 drawEmptyTracks = state;
318 * Sets the current sequence as edited, which implies prompts when loading a new sequence.
320 public static void setEdited() {
325 * Sets the current editing position of the sequencer.
326 * @param ticks the tick position
328 public static void setEditPosition(long ticks) {
329 editPosition = ticks;
333 * Sets the current editing position of the sequencer.
334 * @param ticks the tick position
336 public static void setTempo(int bpm) {
341 * Sets the current editing position of the sequencer.
342 * @param ticks the tick position
344 public static void setTimeSig(int bpm) {
348 public static void setTrackSolo(Track track, boolean on){
349 trackSolo.put(track, new Boolean(on));
352 public static void setTrackMute(Track track, boolean on){
353 trackMute.put(track, new Boolean(on));
364 ** PLAYBACK METHODS **
375 * Starts playback of the current sequence.
377 public static void play() {
378 sequencer.setTickPosition(editPosition);
383 * Resumes playback of the current sequence.
385 public static void resume() {
388 sequencer.setSequence(seq);
389 } catch (InvalidMidiDataException e) {}
390 Track[] tracks = seq.getTracks();
394 for (int i = 0; i < tracks.length; i++) {
396 Object ob = trackSolo.get(tracks[i]);
397 if(ob instanceof Boolean){
398 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
401 ob = trackMute.get(tracks[i]);
402 if(ob instanceof Boolean){
403 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
408 // Disables input to volatile components
411 // Creates the visualisation thread and starts it.
412 player = new Thread () {
414 while(sequencer.isRunning()) {
415 // Updates the GUI with the current tick position.
416 gui.update(sequencer.getTickPosition());
418 // Puts the thread to sleep for as long as it takes
419 // the sequencer to reach the next sixteenth.
421 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
423 } catch (InterruptedException e) {
434 * Pauses playback of the current sequence.
436 public static void pause() {
437 if (sequencer.isRunning()) {
440 if (player != null) player.interrupt();
444 * Stops playback of the current sequence.
446 public static void stop() {
447 if (sequencer.isRunning()) {
450 sequencer.setTickPosition(editPosition);
451 if (player != null) player.interrupt();
463 ** SYSTEM & IO METHODS **
474 * Replaces the current sequence with a new one, holding three empty tracks.
476 public static void clearSequence() {
477 // Creates a new sequence and sends it to the sequencer.
479 seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
480 sequencer.setSequence(seq);
482 emptyTracks = new ArrayList();
483 trackSolo = new HashMap();
484 trackMute = new HashMap();
485 copyBuffer = new ArrayList();
486 } catch (InvalidMidiDataException e) {}
487 // Sends sequence to GUI.
488 if (gui != null) gui.setSequence(seq);
492 * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
494 public static void convertTrack(Track track) {
495 // Searches the track for NoteOn and NoteOff events
496 ArrayList noteOns = new ArrayList(track.size() / 2);
497 ArrayList noteOffs = new ArrayList(track.size() / 2);
499 for (int j = 0; j < track.size(); j++) {
500 event = track.get(j);
501 if (event.getMessage().getStatus() >= 144 &&
502 event.getMessage().getStatus() < 160) noteOns.add(event);
503 if (event.getMessage().getStatus() >= 128 &&
504 event.getMessage().getStatus() < 144) noteOffs.add(event);
506 noteOns.trimToSize();
507 noteOffs.trimToSize();
508 if (noteOns.size() == 0) emptyTracks.add(track);
510 // Sorts the note lists by tick position.
511 Comparator c = new MidiEventComparator();
512 Collections.sort(noteOns, c);
513 Collections.sort(noteOffs, c);
515 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
516 Iterator iOn = noteOns.iterator(), iOff;
517 MidiEvent on, off = null, nextOff;
518 ShortMessage onMsg, nextOffMsg;
519 while(iOn.hasNext()) {
520 on = (MidiEvent)iOn.next();
521 onMsg = (ShortMessage)on.getMessage();
522 iOff = noteOffs.iterator();
523 while(iOff.hasNext()) {
524 nextOff = (MidiEvent)iOff.next();
525 nextOffMsg = (ShortMessage)nextOff.getMessage();
526 if(onMsg.getChannel() == nextOffMsg.getChannel() &&
527 onMsg.getData1() == nextOffMsg.getData1() &&
528 c.compare(nextOff, on) > 0) {
537 track.add(new MooNote(on, off));
539 track.add(new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48)));
546 * Loads a MIDI sequence from the given file.
547 * @param filename the filename to use
549 public static boolean load(String file) {
550 // Loads sequence from file
553 seq = MidiSystem.getSequence(new File(filename));
554 } catch (InvalidMidiDataException e) {
556 } catch (IOException e) {
561 Track[] tracks = seq.getTracks();
562 emptyTracks = new ArrayList();
563 trackMute = new HashMap();
564 trackSolo = new HashMap();
565 copyBuffer = new ArrayList();
567 // Searches track 0 for changes in tempo and time signature.
570 ArrayList ts = new ArrayList(), tc = new ArrayList();
571 for (int i = 0; i < tracks[0].size(); i++) {
572 event = tracks[0].get(i);
573 if (event.getMessage().getStatus() == MetaMessage.META) {
574 metaMsg = (MetaMessage)event.getMessage();
575 switch(metaMsg.getType()) {
576 case 81: tc.add(event); break;
577 case 88: ts.add(event);
581 // timeSignatures = ts.toArray(timeSignatures);
582 // tempoChanges = tc.toArray(tempoChanges);
585 for (int i = 0; i < tracks.length; i++) {
586 convertTrack(tracks[i]);
588 // Sends sequence to GUI and sequencer, then returns
589 if (gui != null) gui.setSequence(seq);
591 sequencer.setSequence(seq);
592 } catch (InvalidMidiDataException e) {}
599 public static boolean promptOnUnsavedChanges() {
600 if (!edited) return false;
601 int exitOption = JOptionPane.showConfirmDialog(gui,
602 "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
603 "File not saved - continue?",
604 JOptionPane.OK_CANCEL_OPTION,
605 JOptionPane.WARNING_MESSAGE);
606 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
611 * Saves the current sequence to the previously given filename.
613 public static boolean save() {
614 if (filename == null) return false;
622 * Saves the current sequence to the given filename
623 * @param file the filename to use
625 public static boolean saveAs(String file) {
627 MidiSystem.write(seq, 1, new File(file));
630 gui.setStatus("Saved " + file);
632 } catch (IOException e) {
633 gui.setStatus("Failed in saving " + file);
639 * Releases all reserved devices and exits the program.
641 public static void quit() {
643 if (promptOnUnsavedChanges()) return;
645 if (sequencer.isOpen()) sequencer.close();
646 if (synthesizer.isOpen()) synthesizer.close();
651 * A Comparator for sorting lists of MidiEvents.
653 public static class MidiEventComparator implements Comparator {
654 public int compare(Object o1, Object o2) {
655 return (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());