]> ruin.nu Git - moosique.git/blob - Moosique.java
Fixed exit if sequence unsaved prompt.
[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         private static ArrayList emptyTracks;
25         private static Map trackMute = new HashMap();
26         private static Map trackSolo = new HashMap();
27
28
29         private static String filename;
30         private static long editPosition;
31         private static boolean makeGUI = true, isEdited = false, drawEmptyTracks = false;
32         private static Thread player;
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 (fileArg == null) {fileArg = args[i];}
49                 }
50
51                 // Acquires MIDI devices and connects them.
52                 System.out.print("Initializing MIDI devices.");
53                 try {
54                         sequencer = MidiSystem.getSequencer();
55                         System.out.print(".");
56                         sequencer.open();
57                         synthesizer = MidiSystem.getSynthesizer();
58                         System.out.print(".");
59                         synthesizer.open();
60                         receiver = synthesizer.getReceiver();
61                         sequencer.getTransmitter().setReceiver(receiver);
62                         channels = synthesizer.getChannels();
63                         setActiveChannel(0);
64                 } catch (MidiUnavailableException e) {
65                         System.out.println("Failed, quitting.");
66                         // System.exit(1);
67                 }
68                 System.out.println("Done");
69
70                 //If a filename is given as the command-line argument, attempts to load a sequence from the file.
71                 if (fileArg != null) {
72                         System.out.print("Loading MIDI sequence from " + fileArg + "...");
73                         if (!load(fileArg)) {
74                                 System.out.println("Failed, creating new sequence");
75                                 clearSequence();
76                         } else {
77                                 System.out.println("Done");
78                         }
79                 } else {
80                         // Otherwise creates a new empty one.
81                         clearSequence();
82                 }
83
84                 // Builds GUI, unless n-flag is set.
85                 if (makeGUI) {
86                         System.out.print("Building GUI.");
87                         try {
88                                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
89                         } catch (Exception e) {}
90                         gui = new MooGUI(seq);
91                         System.out.println("Done");
92                 } else {
93                         System.out.print("Playing...");
94                         play();
95                         while (sequencer.isRunning()) {}
96                         System.out.println("Done");
97                         quit();
98                 }
99         }
100
101
102
103
104
105
106
107
108         /*                              ***
109          **       ACCESSOR METHODS      **
110          ***                            */
111
112
113
114
115
116
117
118
119         /** 
120          * Returns the currently active MidiChannel.
121          * @return the active MidiChannel
122          */
123         public static MidiChannel getActiveChannel() {
124                 return activeChannel;
125         }
126
127         /** 
128          * Returns the MidiChannels of the selected synthesizer.
129          * @return the available MidiChannels
130          */
131         public static MidiChannel getChannel(int i) {
132                 return channels[i];
133         }
134
135         /** 
136          * Returns the MidiChannels of the selected synthesizer.
137          * @return the available MidiChannels
138          */
139         public static MidiChannel[] getChannels() {
140                 return channels;
141         }
142
143         /** 
144          * Returns the current editing position of the sequencer.
145          * @return the tick position
146          */
147         public static long getEditPosition() {
148                 return editPosition;
149         }
150
151         /** 
152          * Returns the GUI.
153          * @return the GUI
154          */
155         public static MooGUI getGUI() {
156                 return gui;
157         }
158
159         /** 
160          * Returns the receiver of the current sequencer.
161          * @return the receiver
162          */
163         public static Receiver getReceiver() {
164                 return receiver;
165         }
166
167         /** 
168          * Returns the current sequence.
169          * @return the current sequence
170          */
171         public static Sequence getSequence() {
172                 return seq;
173         }
174
175         /** 
176          * Returns the current sequencer.
177          * @return the current sequencer
178          */
179         public static Sequencer getSequencer() {
180                 return sequencer;
181         }
182
183         /** 
184          * Returns the tempo of the current sequence.
185          * @return the tick position
186          */
187         public static int getTempo() {
188                 return 120;
189                 // if (tempoMsg == null) return 0;
190         }
191
192         /** 
193          * Returns the tempo of the current sequence.
194          * @return the tick position
195          */
196         public static int[] getTimeSig() {
197                 int[] ts = {4, 4};
198                 return ts;
199                 // if (timeSigMsg == null) return 0;
200         }
201
202         /** 
203          * Returns true if the current sequence has been edited.
204          * @return the tick position
205          */
206         public static boolean isEdited() {
207                 return isEdited;
208         }
209
210         /** 
211          * Returns whether the given track should be drawn
212          * @return true if the given track should be drawn
213          */
214         public static boolean shouldBeDrawn(Track track) {
215                 if (drawEmptyTracks) return true;
216                 else return (!emptyTracks.contains(track));
217         }
218
219
220
221
222
223
224
225
226         /*                              ***
227          **       MUTATOR METHODS       **
228          ***                            */
229
230
231
232
233
234
235
236
237         /** 
238          * Fast forwards the current sequence the given number of measures.
239          * @param measures      the number of measures to fast forward
240          */
241         public static void forward(long ticks) {
242                 editPosition += ticks;
243         }
244
245         /** 
246          * Rewinds the current sequence the given number of measures.
247          * @param measures      the number of measures to rewind
248          */
249         public static void rewind(long ticks) {
250                 editPosition -= ticks;
251         }
252
253         /** 
254          * Sets the currently active MidiChannel.
255          * @param channel       the number of the MidiChannel to activate
256          */
257         public static void setActiveChannel(int channel) {
258                 activeChannel = channels[channel];
259         }
260
261         /** 
262          * Sets whether empty tracks should be drawn
263          * @param state         true if empty tracks should be drawn
264          */
265         public static void setDrawEmptyTracks(boolean state) {
266                 drawEmptyTracks = state;
267         }
268
269         /** 
270          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
271          */
272         public static void setEdited() {
273                 isEdited = true;
274         }
275
276         /** 
277          * Sets the current editing position of the sequencer.
278          * @param ticks         the tick position
279          */
280         public static void setEditPosition(long ticks) {
281                 editPosition = ticks;
282         }
283
284         /** 
285          * Sets the current editing position of the sequencer.
286          * @param ticks         the tick position
287          */
288         public static void setTempo(int bpm) {
289                 // tempoMsg
290         }
291
292         /** 
293          * Sets the current editing position of the sequencer.
294          * @param ticks         the tick position
295          */
296         public static void setTimeSig(int bpm) {
297                 // timeSigMsg
298         }
299
300
301
302
303
304
305
306
307         /*                              ***
308          **       PLAYBACK METHODS      **
309          ***                            */
310
311
312
313
314
315
316
317
318         /** 
319          * Starts playback of the current sequence.
320          */
321         public static void play() {
322                 sequencer.setTickPosition(editPosition);
323                 resume();
324         }
325
326         /** 
327          * Resumes playback of the current sequence.
328          */
329         public static void resume() {
330                 gui.update(0);
331                 try {
332                         sequencer.setSequence(seq);
333                 } catch (InvalidMidiDataException e) {}
334                 Track[] tracks = seq.getTracks();
335
336                 for (int i = 0; i < tracks.length; i++) {
337
338                         Object ob = trackSolo.get(tracks[i]);
339                         if(ob instanceof Boolean){
340                                 System.out.println("Track solo " + i + "= "+ ob);
341                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
342                         }
343
344                         ob = trackMute.get(tracks[i]);
345                         if(ob instanceof Boolean){
346                                 System.out.println("Track mute " + i + "= "+ ob);
347                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
348                         }
349                 }
350
351                 sequencer.start();
352
353                 // Disables input to volatile components
354                 // gui.disable();
355
356                 // Creates the visualisation thread and starts it.
357                 player = new Thread () {
358                         public void run() {
359                                 while(sequencer.isRunning()) {
360                                         // Updates the GUI with the current tick position.
361                                         gui.update(sequencer.getTickPosition());
362
363                                         // Puts the thread to sleep for as long as it takes
364                                         // the sequencer to reach the next sixteenth.
365                                         try {
366                                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
367                                                 sleep (10);
368                                         } catch (InterruptedException e) {
369                                                 Moosique.stop();
370                                         }
371                                 }
372                                 Moosique.stop();
373                         }
374                 };
375                 player.start();
376         }
377
378         /** 
379          * Pauses playback of the current sequence.
380          */
381         public static void pause() {
382                 if (sequencer.isRunning()) {
383                         sequencer.stop();
384                 }
385                 if (player != null) player.interrupt();
386         }
387
388         /** 
389          * Stops playback of the current sequence.
390          */
391         public static void stop() {
392                 if (sequencer.isRunning()) {
393                         sequencer.stop();
394                 }
395                 sequencer.setTickPosition(editPosition);
396                 if (player != null) player.interrupt();
397                 gui.update((long)0);
398         }
399
400
401
402
403
404
405
406
407         /*                              ***
408          **       SYSTEM & IO METHODS   **
409          ***                            */
410
411
412
413
414
415
416
417
418         /** 
419          * Replaces the current sequence with a new one, holding three empty tracks.
420          */
421         public static void clearSequence() {
422                 // Creates a new sequence and sends it to the sequencer.
423                 try {
424                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
425                         sequencer.setSequence(seq);
426                         filename = null;
427                         emptyTracks = new ArrayList();
428                 } catch (InvalidMidiDataException e) {}
429                 // Sends sequence to GUI.
430                 if (gui != null) gui.setSequence(seq);
431         }
432
433         /** 
434          * Loads a MIDI sequence from the given file.
435          * @param filename      the filename to use
436          */
437         public static boolean load(String file) {
438                 // Loads sequence from file
439                 filename = file;
440                 try {
441                         seq = MidiSystem.getSequence(new File(filename));
442                 } catch (InvalidMidiDataException e) {
443                         return false;
444                 } catch (IOException e) {
445                         return false;
446                 }
447                 isEdited = false;
448
449                 Track[] tracks = seq.getTracks();
450                 emptyTracks = new ArrayList();
451                 trackMute = new HashMap();
452                 trackSolo = new HashMap();
453
454                 // Searches track 0 for changes in tempo and time signature.
455                 MidiEvent event;
456                 MetaMessage metaMsg;
457                 ArrayList ts = new ArrayList(), tc = new ArrayList();
458                 for (int i = 0; i < tracks[0].size(); i++) {
459                         event = tracks[0].get(i);
460                         if (event.getMessage().getStatus() == MetaMessage.META) {
461                                 metaMsg = (MetaMessage)event.getMessage();
462                                 switch(metaMsg.getType()) {
463                                         case 81: tc.add(event); break;
464                                         case 88: ts.add(event);
465                                 }
466                         }
467                 }
468                 // timeSignatures = ts.toArray(timeSignatures);
469                 // tempoChanges = tc.toArray(tempoChanges);
470
471                 // Wraps each NoteOn event with its NoteOff event in a MooNote
472                 ArrayList noteOns, noteOffs;
473                 for (int i = 0; i < tracks.length; i++) {
474                         // Searches the sequence for NoteOn and NoteOff events
475                         noteOns = new ArrayList(tracks[i].size() / 2);
476                         noteOffs = new ArrayList(tracks[i].size() / 2);
477                         for (int j = 0; j < tracks[i].size(); j++) {
478                                 event = tracks[i].get(j);
479                                 if (event.getMessage().getStatus() >= 144 &&
480                                     event.getMessage().getStatus() < 160) noteOns.add(event);
481                                 if (event.getMessage().getStatus() >= 128 &&
482                                     event.getMessage().getStatus() < 144) noteOffs.add(event);
483                         }
484                         noteOns.trimToSize();
485                         noteOffs.trimToSize();
486                         if (noteOns.size() == 0) emptyTracks.add(tracks[i]);
487                         
488                         // Sorts the note lists by tick position.
489                         Comparator c = new Comparator() {
490                                 public int compare(Object o1, Object o2) {
491                                         return (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
492                                 }
493                         };
494                         Collections.sort(noteOns, c);
495                         Collections.sort(noteOffs, c);
496
497                         // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
498                         Iterator iOn = noteOns.iterator(), iOff;
499                         MidiEvent on, off = null, nextOff;
500                         ShortMessage onMsg, nextOffMsg;
501                         while(iOn.hasNext()) {
502                                 on = (MidiEvent)iOn.next();
503                                 onMsg = (ShortMessage)on.getMessage();
504                                 iOff = noteOffs.iterator();
505                                 while(iOff.hasNext()) {
506                                         nextOff = (MidiEvent)iOff.next();
507                                         nextOffMsg = (ShortMessage)nextOff.getMessage();
508                                         if(onMsg.getChannel() == nextOffMsg.getChannel() &&
509                                            onMsg.getData1() == nextOffMsg.getData1() &&
510                                            c.compare(nextOff, on) > 0) {
511                                                 off = nextOff;
512                                                 iOff.remove();
513                                                 break;
514                                         }
515                                                 
516                                 }
517                                 tracks[i].remove(on);
518                                 if (off != null) {
519                                         tracks[i].add(new MooNote(on, off));
520                                 } else {
521                                         tracks[i].add(new MooNote(on));
522                                 }
523                                 iOn.remove();
524                         }
525                 }
526                 // Sends sequence to GUI and sequencer, then returns
527                 if (gui != null) gui.setSequence(seq);
528                 try {
529                         sequencer.setSequence(seq);
530                 } catch (InvalidMidiDataException e) {}
531                 return true;
532         }
533
534         /** 
535          * Saves the current sequence to the previously given filename.
536          */
537         public static boolean save() {
538                 if (filename == null) return false;
539                 else {
540                         saveAs(filename);
541                         return true;
542                 }
543         }
544
545         /** 
546          * Saves the current sequence to the given filename
547          * @param file  the filename to use
548          */
549         public static boolean saveAs(String file) {
550                 try {
551                         MidiSystem.write(seq, 1, new File(file));
552                         filename = file;
553                         gui.setStatus("Saved " + file);
554                         return true;
555                 } catch (IOException e) {
556                         gui.setStatus("Failed in saving " + file);
557                         return false;
558                 }
559         }
560
561         /** 
562          * Releases all reserved devices and exits the program.
563          */
564         public static void quit() {
565                 if (isEdited && gui != null) {
566                         int exitOption = JOptionPane.showConfirmDialog(gui,
567                                 "The current sequence has been edited, but not saved.\nDo you wish to quit anyway?",
568                                 "File not saved - really quit?", 
569                                 JOptionPane.OK_CANCEL_OPTION, 
570                                 JOptionPane.WARNING_MESSAGE);
571                         if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return;
572                 }
573                 if (sequencer.isOpen()) sequencer.close();
574                 if (synthesizer.isOpen()) synthesizer.close();
575                 System.exit(0);
576         }
577         
578         public static void setTrackSolo(Track track, boolean on){
579                 trackSolo.put(track, new Boolean(on));  
580         }
581
582         public static void setTrackMute(Track track, boolean on){
583                 trackMute.put(track, new Boolean(on));  
584         }
585 }