]> ruin.nu Git - moosique.git/blob - Moosique.java
no message
[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 MidiEvent[] timeSignatures, tempoChanges;
24
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;
29
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;
34         public static final int WHOLE_NOTE = 0, HALF_NOTE = 1, QUARTER_NOTE = 2, EIGHTH_NOTE = 3, SIXTEENTH_NOTE = 4;
35
36         /** 
37          * Starts the application.
38          * 
39          * Parses command-line arguments, acquires MIDI devices and connects them,
40          * loads a sequence and creates the GUI.
41          */
42         public static void main (String[] args) {
43                 System.out.println("\nMoosique version 1.0\n");
44
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];
50                 }
51
52                 // Acquires MIDI devices and connects them.
53                 System.out.print("Initializing MIDI devices.");
54                 try {
55                         // Configures sequencer
56                         sequencer = MidiSystem.getSequencer();
57                         System.out.print(".");
58                         sequencer.open();
59                         sequencer.addMetaEventListener(new SongEndListener());
60
61                         // Configures synthesizer
62                         synthesizer = MidiSystem.getSynthesizer();
63                         System.out.print(".");
64                         synthesizer.open();
65
66                         // Configures receiver, transmitter and channels.
67                         receiver = synthesizer.getReceiver();
68                         sequencer.getTransmitter().setReceiver(receiver);
69                         channels = synthesizer.getChannels();
70                         setActiveChannel(0);
71                 } catch (MidiUnavailableException e) {
72                         System.out.println("Failed, quitting.");
73                         System.exit(1);
74                 }
75                 System.out.println("Done");
76
77                 // Loads user preferences (work directory, last opened files etc).
78                 loadPreferences();
79
80                 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
81                 if (fileArg != null) {
82                         System.out.print("Loading MIDI sequence from " + fileArg + "...");
83                         if (!load(fileArg)) {
84                                 System.out.println("Failed, creating new sequence");
85                                 clearSequence();
86                         } else {
87                                 System.out.println("Done");
88                         }
89                 } else {
90                         // Otherwise creates a new empty one.
91                         clearSequence();
92                 }
93
94                 // Builds GUI, unless n-flag is set.
95                 if (makeGUI) {
96                         System.out.print("Building GUI.");
97                         try {
98                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
99                         } catch (Exception e) {}
100                         gui = new MooGUI(seq);
101                         System.out.println("Done");
102                 } else {
103                         System.out.print("Playing...");
104                         play();
105                         while (sequencer.isRunning()) {}
106                         System.out.println("Done");
107                         quit();
108                 }
109         }
110
111
112
113
114
115
116
117
118         /*                              ***
119          **       ACCESSOR METHODS       **
120          ***                              */
121
122
123
124
125
126
127
128
129         /** 
130          * Returns the currently active MidiChannel.
131          * @return the active MidiChannel
132          */
133         public static MidiChannel getActiveChannel() {
134                 return activeChannel;
135         }
136
137         /** 
138          * Returns the MidiChannels of the selected synthesizer.
139          * @return the available MidiChannels
140          */
141         public static MidiChannel getChannel(int i) {
142                 return channels[i];
143         }
144
145         /** 
146          * Returns the MidiChannels of the selected synthesizer.
147          * @return the available MidiChannels
148          */
149         public static MidiChannel[] getChannels() {
150                 return channels;
151         }
152
153         /** 
154          * Returns the current copy buffer.
155          * @return the current copy buffer
156          */
157         public static ArrayList getCopyBuffer() {
158                 return copyBuffer;
159         }
160
161         /** 
162          * Returns the current editing position of the sequencer.
163          * @return the tick position
164          */
165         public static long getEditPosition() {
166                 return editPosition;
167         }
168
169         /** 
170          * Returns the GUI.
171          * @return the GUI
172          */
173         public static MooGUI getGUI() {
174                 return gui;
175         }
176
177         /** 
178          * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
179          * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
180          */
181         public static int[] getPositionForTicks(long ticks) {
182                 /*
183                 int measures, beats, ticks;
184                 for (int i = 0; i < timeSignatures.length; i++) {
185                         long tick = timeSignatures[i].getTick();
186                         // Split the ticks in the interval into measures, beats and ticks.
187                 }
188                 */
189                 int[] pos = {1, 1, 1};
190                 return pos;
191         }
192
193         /** 
194          * Returns the receiver of the current sequencer.
195          * @return the receiver
196          */
197         public static Receiver getReceiver() {
198                 return receiver;
199         }
200
201         /** 
202          * Returns the current sequence.
203          * @return the current sequence
204          */
205         public static Sequence getSequence() {
206                 return seq;
207         }
208
209         /** 
210          * Returns the current sequencer.
211          * @return the current sequencer
212          */
213         public static Sequencer getSequencer() {
214                 return sequencer;
215         }
216
217         /** 
218          * Returns the tempo of the current sequence.
219          * @return the tick position
220          */
221         public static int getTempo() {
222                 return 120;
223                 // if (tempoMsg == null) return 0;
224         }
225
226         /** 
227          * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
228          * @return the tick position.
229          */
230         public static long getTicksForPosition(int measures, int beats, int ticks) {
231                 long tickPos = 0;
232                 /*
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.
236                 }
237                 */
238                 return tickPos;
239         }
240
241         /** 
242          * Returns the tempo of the current sequence.
243          * @return the tick position
244          */
245         public static int[] getTimeSig() {
246                 int[] ts = {4, 4};
247                 return ts;
248                 // if (timeSigMsg == null) return 0;
249         }
250
251         /** 
252          * Returns true if the current sequence has been edited.
253          * @return the tick position
254          */
255         public static boolean isEdited() {
256                 return edited;
257         }
258
259         /** 
260          * Returns whether the given track should be drawn
261          * @return true if the given track should be drawn
262          */
263         public static boolean shouldBeDrawn(Track track) {
264                 if (drawEmptyTracks) return true;
265                 else return (!emptyTracks.contains(track));
266         }
267
268
269
270
271
272
273
274
275         /*                              ***
276          **       MUTATOR METHODS        **
277          ***                              */
278
279
280
281
282
283
284
285
286         /** 
287          * Fast forwards the current sequence the given number of measures.
288          * @param measures      the number of measures to fast forward
289          */
290         public static void forward(long ticks) {
291                 editPosition += ticks;
292         }
293
294         /** 
295          * Rewinds the current sequence the given number of measures.
296          * @param measures      the number of measures to rewind
297          */
298         public static void rewind(long ticks) {
299                 editPosition -= ticks;
300         }
301
302         /** 
303          * Sets the currently active MidiChannel.
304          * @param channel       the number of the MidiChannel to activate
305          */
306         public static void setActiveChannel(int channel) {
307                 activeChannel = channels[channel];
308         }
309
310         /** 
311          * Sets the current copy buffer.
312          * @param the copy buffer
313          */
314         public static void setCopyBuffer(ArrayList buffer) {
315                 copyBuffer = buffer;
316         }
317
318         /** 
319          * Sets whether empty tracks should be drawn
320          * @param state         true if empty tracks should be drawn
321          */
322         public static void setDrawEmptyTracks(boolean state) {
323                 drawEmptyTracks = state;
324         }
325
326         /** 
327          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
328          */
329         public static void setEdited() {
330                 edited = true;
331         }
332
333         /** 
334          * Sets the current editing position of the sequencer.
335          * @param ticks         the tick position
336          */
337         public static void setEditPosition(long ticks) {
338                 editPosition = ticks;
339         }
340
341         /** 
342          * Sets the current editing position of the sequencer.
343          * @param ticks         the tick position
344          */
345         public static void setTempo(int bpm) {
346                 // tempoMsg
347         }
348
349         /** 
350          * Sets the current editing position of the sequencer.
351          * @param ticks         the tick position
352          */
353         public static void setTimeSig(int bpm) {
354                 // timeSigMsg
355         }
356         
357         /** 
358          * Sets the solo setting of the given track.
359          * @param on    true for solo, false for not
360          */
361         public static void setTrackSolo(Track track, boolean on){
362                 trackSolo.put(track, new Boolean(on));  
363         }
364
365         /** 
366          * Sets the mute setting of the given track.
367          * @param on    true for mute, false for not
368          */
369         public static void setTrackMute(Track track, boolean on){
370                 trackMute.put(track, new Boolean(on));  
371         }
372
373         /** 
374          * Sets the current playback volume.
375          * @param volume        the volume, between 0 and 1
376          */
377         public void setVolume(long volume) {
378                 for (int i = 0; i < channels.length; i++) {
379                         channels[i].controlChange(7, (int)(volume * 127.0));
380                 }
381         }
382
383
384
385
386
387
388
389
390         /*                              ***
391          **       PLAYBACK METHODS       **
392          ***                              */
393
394
395
396
397
398
399
400
401         /** 
402          * Starts playback of the current sequence.
403          */
404         public static void play() {
405                 sequencer.setTickPosition(editPosition);
406                 resume();
407         }
408
409         /** 
410          * Resumes playback of the current sequence.
411          */
412         public static void resume() {
413                 gui.update(0);
414                 try {
415                         sequencer.setSequence(seq);
416                 } catch (InvalidMidiDataException e) {}
417                 Track[] tracks = seq.getTracks();
418
419                 sequencer.start();
420
421                 for (int i = 0; i < tracks.length; i++) {
422
423                         Object ob = trackSolo.get(tracks[i]);
424                         if(ob instanceof Boolean){
425                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
426                         }
427
428                         ob = trackMute.get(tracks[i]);
429                         if(ob instanceof Boolean){
430                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
431                         }
432                 }
433
434                 // Disables input to volatile components
435                 // gui.disable();
436
437                 // Creates the visualisation thread and starts it.
438                 player = new PlayThread();
439                 player.start();
440         }
441
442         /** 
443          * Pauses playback of the current sequence.
444          */
445         public static void pause() {
446                 if (sequencer.isRunning()) {
447                         sequencer.stop();
448                 }
449                 if (player != null) player.interrupt();
450         }
451
452         /** 
453          * Stops playback of the current sequence.
454          */
455         public static void stop() {
456                 if (sequencer.isRunning()) {
457                         sequencer.stop();
458                 }
459                 sequencer.setTickPosition(editPosition);
460                 if (player != null) player.interrupt();
461                 gui.update((long)0);
462         }
463
464
465
466
467
468
469
470
471         /*                              ***
472          **       SYSTEM & IO METHODS    **
473          ***                              */
474
475
476
477
478
479
480
481
482         /** 
483          * Replaces the current sequence with a new one, holding three empty tracks.
484          */
485         public static void clearSequence() {
486                 // Creates a new sequence and sends it to the sequencer.
487                 try {
488                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
489                         sequencer.setSequence(seq);
490                         filename = null;
491                         emptyTracks = new ArrayList();
492                         trackSolo = new HashMap();
493                         trackMute = new HashMap();
494                         copyBuffer = new ArrayList();
495                 } catch (InvalidMidiDataException e) {}
496                 // Sends sequence to GUI.
497                 if (gui != null) gui.setSequence(seq);
498         }
499         
500         /** 
501          * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
502          * @param track         the track to convert
503          * @param quantize      whether to round locations and durations in the track to nearest 16th
504          * @return a list of the created MooNotes
505          */
506         public static List convertTrack(Track track, boolean quantize) {
507                 // Searches the track for NoteOn and NoteOff events
508                 ArrayList noteOns = new ArrayList(track.size() / 2);
509                 ArrayList noteOffs = new ArrayList(track.size() / 2);
510                 ArrayList newMooNotes = new ArrayList();
511                 MidiEvent event;
512                 for (int j = 0; j < track.size(); j++) {
513                         event = track.get(j);
514                         if (event.getMessage().getStatus() >= 144 &&
515                             event.getMessage().getStatus() < 160) noteOns.add(event);
516                         if (event.getMessage().getStatus() >= 128 &&
517                             event.getMessage().getStatus() < 144) noteOffs.add(event);
518                 }
519                 noteOns.trimToSize();
520                 noteOffs.trimToSize();
521                 if (noteOns.size() == 0) emptyTracks.add(track);
522                 
523                 // Sorts the note lists by tick position.
524                 Comparator c = new MidiEventComparator();
525                 Collections.sort(noteOns, c);
526                 Collections.sort(noteOffs, c);
527
528                 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
529                 Iterator iOn = noteOns.iterator(), iOff;
530                 MidiEvent on, off = null, nextOff;
531                 ShortMessage onMsg, nextOffMsg;
532                 while(iOn.hasNext()) {
533                         on = (MidiEvent)iOn.next();
534                         if (!(on instanceof MooNote)) {
535                                 onMsg = (ShortMessage)on.getMessage();
536                                 iOff = noteOffs.iterator();
537                                 while(iOff.hasNext()) {
538                                         nextOff = (MidiEvent)iOff.next();
539                                         nextOffMsg = (ShortMessage)nextOff.getMessage();
540                                         if(onMsg.getChannel() == nextOffMsg.getChannel() &&
541                                            onMsg.getData1() == nextOffMsg.getData1() &&
542                                            c.compare(nextOff, on) > 0) {
543                                                 off = nextOff;
544                                                 iOff.remove();
545                                                 break;
546                                         }
547                                                 
548                                 }
549                                 track.remove(on);
550                                 MooNote mn;
551                                 if (off != null) {
552                                         mn = new MooNote(on, off);
553                                 } else {
554                                         mn = new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48));
555                                 }
556                                 track.add(mn);
557                                 newMooNotes.add(mn);
558                                 iOn.remove();
559                         }
560                 }
561                 if (quantize) quantize(newMooNotes, SIXTEENTH_NOTE, true, true);
562                 return newMooNotes;
563         }
564
565         /** 
566          * Loads a MIDI sequence from the given file.
567          * @param filename      the filename to use
568          */
569         public static boolean load(String file) {
570                 // Loads sequence from file
571                 filename = file;
572                 try {seq = MidiSystem.getSequence(new File(filename));}
573                 catch (Exception e) {return false;}
574                 edited = false;
575
576                 Track[] tracks = seq.getTracks();
577                 emptyTracks = new ArrayList();
578                 trackMute = new HashMap();
579                 trackSolo = new HashMap();
580                 copyBuffer = new ArrayList();
581
582                 // Searches track 0 for changes in tempo and time signature.
583                 MidiEvent event;
584                 MetaMessage metaMsg;
585                 ArrayList ts = new ArrayList(), tc = new ArrayList();
586                 for (int i = 0; i < tracks[0].size(); i++) {
587                         event = tracks[0].get(i);
588                         if (event.getMessage().getStatus() == MetaMessage.META) {
589                                 metaMsg = (MetaMessage)event.getMessage();
590                                 switch(metaMsg.getType()) {
591                                         case 81: tc.add(event); break;
592                                         case 88: ts.add(event);
593                                 }
594                         }
595                 }
596                 // timeSignatures = ts.toArray(timeSignatures);
597                 // tempoChanges = tc.toArray(tempoChanges);
598
599                 // Converts tracks.
600                 for (int i = 0; i < tracks.length; i++) {
601                         convertTrack(tracks[i], false);
602                 }
603
604                 // Sends sequence to GUI and sequencer, then returns
605                 if (gui != null) gui.setSequence(seq);
606                 try {
607                         sequencer.setSequence(seq);
608                 } catch (InvalidMidiDataException e) {}
609                 return true;
610         }
611
612         /** 
613          * Quantizes the given list of MIDI events
614          * @param notes         a list of the notes to quantize
615          * @param resolution    the note size to round each note to
616          * @param location      whether the quantize should affect the location of the note
617          * @param duration      whether the quantize should affect the duration of the note
618          */
619         public static void quantize(List notes, int resolution, boolean location, boolean duration) {
620                 // Math.round(mn.getTick() / ticksPerSixteenth);
621         }
622
623         /** 
624          * Loads the user preferences.
625          */
626         public static void loadPreferences() {
627                 
628         }
629
630         /** 
631          * Saves the user preferences.
632          */
633         public static void savePreferences() {
634                 
635         }
636
637         /** 
638          * Prompts the user .
639          */
640         public static boolean promptOnUnsavedChanges() {
641                 if (!edited) return false;
642                 int exitOption = JOptionPane.showConfirmDialog(gui,
643                         "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
644                         "File not saved - continue?", 
645                         JOptionPane.OK_CANCEL_OPTION, 
646                         JOptionPane.WARNING_MESSAGE);
647                 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
648                 return false;
649         }
650
651         /** 
652          * Saves the current sequence to the previously given filename.
653          */
654         public static boolean save() {
655                 if (filename == null) return false;
656                 else {
657                         saveAs(filename);
658                         return true;
659                 }
660         }
661
662         /** 
663          * Saves the current sequence to the given filename
664          * @param file  the filename to use
665          */
666         public static boolean saveAs(String file) {
667                 try {
668                         MidiSystem.write(seq, 1, new File(file));
669                         filename = file;
670                         edited = false;
671                         gui.setStatus("Saved " + file);
672                         return true;
673                 } catch (IOException e) {
674                         gui.setStatus("Failed in saving " + file);
675                         return false;
676                 }
677         }
678
679         /** 
680          * Releases all reserved devices and exits the program.
681          */
682         public static void quit() {
683                 if (gui != null) {
684                         if (promptOnUnsavedChanges()) return;
685                 }
686                 savePreferences();
687                 if (sequencer.isOpen()) sequencer.close();
688                 if (synthesizer.isOpen()) synthesizer.close();
689                 System.exit(0);
690         }
691         
692         /** 
693          * A Ccmparator for sorting lists of MidiEvents.
694          */
695         public static class MidiEventComparator implements Comparator {
696                 public int compare(Object o1, Object o2) {
697                         int diff = (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
698                         if (diff != 0 || !(((MidiEvent)o1).getMessage() instanceof ShortMessage) || !(((MidiEvent)o2).getMessage() instanceof ShortMessage)) return diff;
699                         return (((ShortMessage)((MidiEvent)o1).getMessage()).getData1() - ((ShortMessage)((MidiEvent)o2).getMessage()).getData1());
700                 }
701         }
702
703         /** 
704          * The thread that updates the GUI during playback.
705          */
706         public static class PlayThread extends Thread {
707                 public void run() {
708                         // Updates the GUI with the current tick position.
709                         gui.update(sequencer.getTickPosition());
710
711                         // Puts the thread to sleep for as long as it takes
712                         // the sequencer to reach the next sixteenth.
713                         try {
714                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
715                                 sleep (10);
716                         } catch (InterruptedException e) {
717                                 Moosique.stop();
718                         }
719                 }
720         }
721
722         /** 
723          * A listener for detecting the end of the sequence.
724          */
725         public static class SongEndListener implements MetaEventListener {
726                 public void meta(MetaMessage event) {
727                         if (event.getType() == 47)
728                                 // End of sequence
729                                 stop();
730                 }
731         }
732 }