]> ruin.nu Git - moosique.git/blob - Moosique.java
Fixed recording!!!
[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
35         /** 
36          * Starts the application.
37          * 
38          * Parses command-line arguments, acquires MIDI devices and connects them,
39          * loads a sequence and creates the GUI.
40          */
41         public static void main (String[] args) {
42                 System.out.println("\nMoosique version 1.0\n");
43
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];
50                 }
51
52                 // Acquires MIDI devices and connects them.
53                 System.out.print("Initializing MIDI devices.");
54                 try {
55                         sequencer = MidiSystem.getSequencer();
56                         System.out.print(".");
57                         sequencer.open();
58                         synthesizer = MidiSystem.getSynthesizer();
59                         System.out.print(".");
60                         synthesizer.open();
61                         receiver = synthesizer.getReceiver();
62                         sequencer.getTransmitter().setReceiver(receiver);
63                         channels = synthesizer.getChannels();
64                         setActiveChannel(0);
65                 } catch (MidiUnavailableException e) {
66                         System.out.println("Failed, quitting.");
67                         System.exit(1);
68                 }
69                 System.out.println("Done");
70
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 + "...");
74                         if (!load(fileArg)) {
75                                 System.out.println("Failed, creating new sequence");
76                                 clearSequence();
77                         } else {
78                                 System.out.println("Done");
79                         }
80                 } else {
81                         // Otherwise creates a new empty one.
82                         clearSequence();
83                 }
84
85                 // Builds GUI, unless n-flag is set.
86                 if (makeGUI) {
87                         System.out.print("Building GUI.");
88                         try {
89                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
90                         } catch (Exception e) {}
91                         gui = new MooGUI(seq);
92                         System.out.println("Done");
93                 } else {
94                         System.out.print("Playing...");
95                         play();
96                         while (sequencer.isRunning()) {}
97                         System.out.println("Done");
98                         quit();
99                 }
100         }
101
102
103
104
105
106
107
108
109         /*                              ***
110          **       ACCESSOR METHODS       **
111          ***                              */
112
113
114
115
116
117
118
119
120         /** 
121          * Returns the currently active MidiChannel.
122          * @return the active MidiChannel
123          */
124         public static MidiChannel getActiveChannel() {
125                 return activeChannel;
126         }
127
128         /** 
129          * Returns the MidiChannels of the selected synthesizer.
130          * @return the available MidiChannels
131          */
132         public static MidiChannel getChannel(int i) {
133                 return channels[i];
134         }
135
136         /** 
137          * Returns the MidiChannels of the selected synthesizer.
138          * @return the available MidiChannels
139          */
140         public static MidiChannel[] getChannels() {
141                 return channels;
142         }
143
144         /** 
145          * Returns the current copy buffer.
146          * @return the current copy buffer
147          */
148         public static ArrayList getCopyBuffer() {
149                 return copyBuffer;
150         }
151
152         /** 
153          * Returns the current editing position of the sequencer.
154          * @return the tick position
155          */
156         public static long getEditPosition() {
157                 return editPosition;
158         }
159
160         /** 
161          * Returns the GUI.
162          * @return the GUI
163          */
164         public static MooGUI getGUI() {
165                 return gui;
166         }
167
168         /** 
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.
171          */
172         public static int[] getPositionForTick(long ticks) {
173                 /*
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.
178                 }
179                 */
180                 int[] pos = {1, 1, 1};
181                 return pos;
182         }
183
184         /** 
185          * Returns the receiver of the current sequencer.
186          * @return the receiver
187          */
188         public static Receiver getReceiver() {
189                 return receiver;
190         }
191
192         /** 
193          * Returns the current sequence.
194          * @return the current sequence
195          */
196         public static Sequence getSequence() {
197                 return seq;
198         }
199
200         /** 
201          * Returns the current sequencer.
202          * @return the current sequencer
203          */
204         public static Sequencer getSequencer() {
205                 return sequencer;
206         }
207
208         /** 
209          * Returns the tempo of the current sequence.
210          * @return the tick position
211          */
212         public static int getTempo() {
213                 return 120;
214                 // if (tempoMsg == null) return 0;
215         }
216
217         /** 
218          * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
219          * @return the tick position.
220          */
221         public static long getTicksForPosition(int measures, int beats, int ticks) {
222                 long tickPos = 0;
223                 /*
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.
227                 }
228                 */
229                 return tickPos;
230         }
231
232         /** 
233          * Returns the tempo of the current sequence.
234          * @return the tick position
235          */
236         public static int[] getTimeSig() {
237                 int[] ts = {4, 4};
238                 return ts;
239                 // if (timeSigMsg == null) return 0;
240         }
241
242         /** 
243          * Returns true if the current sequence has been edited.
244          * @return the tick position
245          */
246         public static boolean isEdited() {
247                 return edited;
248         }
249
250         /** 
251          * Returns whether the given track should be drawn
252          * @return true if the given track should be drawn
253          */
254         public static boolean shouldBeDrawn(Track track) {
255                 if (drawEmptyTracks) return true;
256                 else return (!emptyTracks.contains(track));
257         }
258
259
260
261
262
263
264
265
266         /*                              ***
267          **       MUTATOR METHODS        **
268          ***                              */
269
270
271
272
273
274
275
276
277         /** 
278          * Fast forwards the current sequence the given number of measures.
279          * @param measures      the number of measures to fast forward
280          */
281         public static void forward(long ticks) {
282                 editPosition += ticks;
283         }
284
285         /** 
286          * Rewinds the current sequence the given number of measures.
287          * @param measures      the number of measures to rewind
288          */
289         public static void rewind(long ticks) {
290                 editPosition -= ticks;
291         }
292
293         /** 
294          * Sets the currently active MidiChannel.
295          * @param channel       the number of the MidiChannel to activate
296          */
297         public static void setActiveChannel(int channel) {
298                 activeChannel = channels[channel];
299         }
300
301         /** 
302          * Sets the current copy buffer.
303          * @param the copy buffer
304          */
305         public static void setCopyBuffer(ArrayList buffer) {
306                 copyBuffer = buffer;
307         }
308
309         /** 
310          * Sets whether empty tracks should be drawn
311          * @param state         true if empty tracks should be drawn
312          */
313         public static void setDrawEmptyTracks(boolean state) {
314                 drawEmptyTracks = state;
315         }
316
317         /** 
318          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
319          */
320         public static void setEdited() {
321                 edited = true;
322         }
323
324         /** 
325          * Sets the current editing position of the sequencer.
326          * @param ticks         the tick position
327          */
328         public static void setEditPosition(long ticks) {
329                 editPosition = ticks;
330         }
331
332         /** 
333          * Sets the current editing position of the sequencer.
334          * @param ticks         the tick position
335          */
336         public static void setTempo(int bpm) {
337                 // tempoMsg
338         }
339
340         /** 
341          * Sets the current editing position of the sequencer.
342          * @param ticks         the tick position
343          */
344         public static void setTimeSig(int bpm) {
345                 // timeSigMsg
346         }
347         
348         public static void setTrackSolo(Track track, boolean on){
349                 trackSolo.put(track, new Boolean(on));  
350         }
351
352         public static void setTrackMute(Track track, boolean on){
353                 trackMute.put(track, new Boolean(on));  
354         }
355
356
357
358
359
360
361
362
363         /*                              ***
364          **       PLAYBACK METHODS       **
365          ***                              */
366
367
368
369
370
371
372
373
374         /** 
375          * Starts playback of the current sequence.
376          */
377         public static void play() {
378                 sequencer.setTickPosition(editPosition);
379                 resume();
380         }
381
382         /** 
383          * Resumes playback of the current sequence.
384          */
385         public static void resume() {
386                 gui.update(0);
387                 try {
388                         sequencer.setSequence(seq);
389                 } catch (InvalidMidiDataException e) {}
390                 Track[] tracks = seq.getTracks();
391
392                 sequencer.start();
393
394                 for (int i = 0; i < tracks.length; i++) {
395
396                         Object ob = trackSolo.get(tracks[i]);
397                         if(ob instanceof Boolean){
398                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
399                         }
400
401                         ob = trackMute.get(tracks[i]);
402                         if(ob instanceof Boolean){
403                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
404                         }
405                 }
406
407
408                 // Disables input to volatile components
409                 // gui.disable();
410
411                 // Creates the visualisation thread and starts it.
412                 player = new Thread () {
413                         public void run() {
414                                 while(sequencer.isRunning()) {
415                                         // Updates the GUI with the current tick position.
416                                         gui.update(sequencer.getTickPosition());
417
418                                         // Puts the thread to sleep for as long as it takes
419                                         // the sequencer to reach the next sixteenth.
420                                         try {
421                                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
422                                                 sleep (10);
423                                         } catch (InterruptedException e) {
424                                                 Moosique.stop();
425                                         }
426                                 }
427                                 Moosique.stop();
428                         }
429                 };
430                 player.start();
431         }
432
433         /** 
434          * Pauses playback of the current sequence.
435          */
436         public static void pause() {
437                 if (sequencer.isRunning()) {
438                         sequencer.stop();
439                 }
440                 if (player != null) player.interrupt();
441         }
442
443         /** 
444          * Stops playback of the current sequence.
445          */
446         public static void stop() {
447                 if (sequencer.isRunning()) {
448                         sequencer.stop();
449                 }
450                 sequencer.setTickPosition(editPosition);
451                 if (player != null) player.interrupt();
452                 gui.update((long)0);
453         }
454
455
456
457
458
459
460
461
462         /*                              ***
463          **       SYSTEM & IO METHODS    **
464          ***                              */
465
466
467
468
469
470
471
472
473         /** 
474          * Replaces the current sequence with a new one, holding three empty tracks.
475          */
476         public static void clearSequence() {
477                 // Creates a new sequence and sends it to the sequencer.
478                 try {
479                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
480                         sequencer.setSequence(seq);
481                         filename = null;
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);
489         }
490         
491         /** 
492          * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
493          */
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);
498                 MidiEvent event;
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);
505                 }
506                 noteOns.trimToSize();
507                 noteOffs.trimToSize();
508                 if (noteOns.size() == 0) emptyTracks.add(track);
509                 
510                 // Sorts the note lists by tick position.
511                 Comparator c = new MidiEventComparator();
512                 Collections.sort(noteOns, c);
513                 Collections.sort(noteOffs, c);
514
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) {
529                                         off = nextOff;
530                                         iOff.remove();
531                                         break;
532                                 }
533                                         
534                         }
535                         track.remove(on);
536                         if (off != null) {
537                                 track.add(new MooNote(on, off));
538                         } else {
539                                 track.add(new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48)));
540                         }
541                         iOn.remove();
542                 }
543         }
544
545         /** 
546          * Loads a MIDI sequence from the given file.
547          * @param filename      the filename to use
548          */
549         public static boolean load(String file) {
550                 // Loads sequence from file
551                 filename = file;
552                 try {
553                         seq = MidiSystem.getSequence(new File(filename));
554                 } catch (InvalidMidiDataException e) {
555                         return false;
556                 } catch (IOException e) {
557                         return false;
558                 }
559                 edited = false;
560
561                 Track[] tracks = seq.getTracks();
562                 emptyTracks = new ArrayList();
563                 trackMute = new HashMap();
564                 trackSolo = new HashMap();
565                 copyBuffer = new ArrayList();
566
567                 // Searches track 0 for changes in tempo and time signature.
568                 MidiEvent event;
569                 MetaMessage metaMsg;
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);
578                                 }
579                         }
580                 }
581                 // timeSignatures = ts.toArray(timeSignatures);
582                 // tempoChanges = tc.toArray(tempoChanges);
583
584                 // Converts tracks.
585                 for (int i = 0; i < tracks.length; i++) {
586                         convertTrack(tracks[i]);
587                 }
588                 // Sends sequence to GUI and sequencer, then returns
589                 if (gui != null) gui.setSequence(seq);
590                 try {
591                         sequencer.setSequence(seq);
592                 } catch (InvalidMidiDataException e) {}
593                 return true;
594         }
595
596         /** 
597          * Prompts the user .
598          */
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;
607                 return false;
608         }
609
610         /** 
611          * Saves the current sequence to the previously given filename.
612          */
613         public static boolean save() {
614                 if (filename == null) return false;
615                 else {
616                         saveAs(filename);
617                         return true;
618                 }
619         }
620
621         /** 
622          * Saves the current sequence to the given filename
623          * @param file  the filename to use
624          */
625         public static boolean saveAs(String file) {
626                 try {
627                         MidiSystem.write(seq, 1, new File(file));
628                         filename = file;
629                         edited = false;
630                         gui.setStatus("Saved " + file);
631                         return true;
632                 } catch (IOException e) {
633                         gui.setStatus("Failed in saving " + file);
634                         return false;
635                 }
636         }
637
638         /** 
639          * Releases all reserved devices and exits the program.
640          */
641         public static void quit() {
642                 if (gui != null) {
643                         if (promptOnUnsavedChanges()) return;
644                 }
645                 if (sequencer.isOpen()) sequencer.close();
646                 if (synthesizer.isOpen()) synthesizer.close();
647                 System.exit(0);
648         }
649         
650         /** 
651          * A Comparator for sorting lists of MidiEvents.
652          */
653         public static class MidiEventComparator implements Comparator {
654                 public int compare(Object o1, Object o2) {
655                         return (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
656                 }
657         }
658 }