]> 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
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         /** 
349          * Sets the solo setting of the given track.
350          * @param on    true for solo, false for not
351          */
352         public static void setTrackSolo(Track track, boolean on){
353                 trackSolo.put(track, new Boolean(on));  
354         }
355
356         /** 
357          * Sets the mute setting of the given track.
358          * @param on    true for mute, false for not
359          */
360         public static void setTrackMute(Track track, boolean on){
361                 trackMute.put(track, new Boolean(on));  
362         }
363
364         /** 
365          * Sets the current playback volume.
366          * @param volume        the volume, between 0 and 1
367          */
368         public void setVolume(long volume) {
369                 for (int i = 0; i < channels.length; i++) {
370                         channels[i].controlChange(7, (int)(volume * 127.0));
371                 }
372         }
373
374
375
376
377
378
379
380
381         /*                              ***
382          **       PLAYBACK METHODS       **
383          ***                              */
384
385
386
387
388
389
390
391
392         /** 
393          * Starts playback of the current sequence.
394          */
395         public static void play() {
396                 sequencer.setTickPosition(editPosition);
397                 resume();
398         }
399
400         /** 
401          * Resumes playback of the current sequence.
402          */
403         public static void resume() {
404                 gui.update(0);
405                 try {
406                         sequencer.setSequence(seq);
407                 } catch (InvalidMidiDataException e) {}
408                 Track[] tracks = seq.getTracks();
409
410                 sequencer.start();
411
412                 for (int i = 0; i < tracks.length; i++) {
413
414                         Object ob = trackSolo.get(tracks[i]);
415                         if(ob instanceof Boolean){
416                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
417                         }
418
419                         ob = trackMute.get(tracks[i]);
420                         if(ob instanceof Boolean){
421                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
422                         }
423                 }
424
425
426                 // Disables input to volatile components
427                 // gui.disable();
428
429                 // Creates the visualisation thread and starts it.
430                 player = new Thread () {
431                         public void run() {
432                                 while(sequencer.isRunning()) {
433                                         // Updates the GUI with the current tick position.
434                                         gui.update(sequencer.getTickPosition());
435
436                                         // Puts the thread to sleep for as long as it takes
437                                         // the sequencer to reach the next sixteenth.
438                                         try {
439                                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
440                                                 sleep (10);
441                                         } catch (InterruptedException e) {
442                                                 Moosique.stop();
443                                         }
444                                 }
445                                 Moosique.stop();
446                         }
447                 };
448                 player.start();
449         }
450
451         /** 
452          * Pauses playback of the current sequence.
453          */
454         public static void pause() {
455                 if (sequencer.isRunning()) {
456                         sequencer.stop();
457                 }
458                 if (player != null) player.interrupt();
459         }
460
461         /** 
462          * Stops playback of the current sequence.
463          */
464         public static void stop() {
465                 if (sequencer.isRunning()) {
466                         sequencer.stop();
467                 }
468                 sequencer.setTickPosition(editPosition);
469                 if (player != null) player.interrupt();
470                 gui.update((long)0);
471         }
472
473
474
475
476
477
478
479
480         /*                              ***
481          **       SYSTEM & IO METHODS    **
482          ***                              */
483
484
485
486
487
488
489
490
491         /** 
492          * Replaces the current sequence with a new one, holding three empty tracks.
493          */
494         public static void clearSequence() {
495                 // Creates a new sequence and sends it to the sequencer.
496                 try {
497                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
498                         sequencer.setSequence(seq);
499                         filename = null;
500                         emptyTracks = new ArrayList();
501                         trackSolo = new HashMap();
502                         trackMute = new HashMap();
503                         copyBuffer = new ArrayList();
504                 } catch (InvalidMidiDataException e) {}
505                 // Sends sequence to GUI.
506                 if (gui != null) gui.setSequence(seq);
507         }
508         
509         /** 
510          * Wraps each NoteOn event in the track with its NoteOff event in a MooNote.
511          */
512         public static void convertTrack(Track track) {
513                 // Searches the track for NoteOn and NoteOff events
514                 ArrayList noteOns = new ArrayList(track.size() / 2);
515                 ArrayList noteOffs = new ArrayList(track.size() / 2);
516                 MidiEvent event;
517                 for (int j = 0; j < track.size(); j++) {
518                         event = track.get(j);
519                         if (event.getMessage().getStatus() >= 144 &&
520                             event.getMessage().getStatus() < 160) noteOns.add(event);
521                         if (event.getMessage().getStatus() >= 128 &&
522                             event.getMessage().getStatus() < 144) noteOffs.add(event);
523                 }
524                 noteOns.trimToSize();
525                 noteOffs.trimToSize();
526                 if (noteOns.size() == 0) emptyTracks.add(track);
527                 
528                 // Sorts the note lists by tick position.
529                 Comparator c = new MidiEventComparator();
530                 Collections.sort(noteOns, c);
531                 Collections.sort(noteOffs, c);
532
533                 // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
534                 Iterator iOn = noteOns.iterator(), iOff;
535                 MidiEvent on, off = null, nextOff;
536                 ShortMessage onMsg, nextOffMsg;
537                 while(iOn.hasNext()) {
538                         on = (MidiEvent)iOn.next();
539                         onMsg = (ShortMessage)on.getMessage();
540                         iOff = noteOffs.iterator();
541                         while(iOff.hasNext()) {
542                                 nextOff = (MidiEvent)iOff.next();
543                                 nextOffMsg = (ShortMessage)nextOff.getMessage();
544                                 if(onMsg.getChannel() == nextOffMsg.getChannel() &&
545                                    onMsg.getData1() == nextOffMsg.getData1() &&
546                                    c.compare(nextOff, on) > 0) {
547                                         off = nextOff;
548                                         iOff.remove();
549                                         break;
550                                 }
551                                         
552                         }
553                         track.remove(on);
554                         if (off != null) {
555                                 track.add(new MooNote(on, off));
556                         } else {
557                                 track.add(new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48)));
558                         }
559                         iOn.remove();
560                 }
561         }
562
563         /** 
564          * Loads a MIDI sequence from the given file.
565          * @param filename      the filename to use
566          */
567         public static boolean load(String file) {
568                 // Loads sequence from file
569                 filename = file;
570                 try {
571                         seq = MidiSystem.getSequence(new File(filename));
572                 } catch (InvalidMidiDataException e) {
573                         return false;
574                 } catch (IOException e) {
575                         return false;
576                 }
577                 edited = false;
578
579                 Track[] tracks = seq.getTracks();
580                 emptyTracks = new ArrayList();
581                 trackMute = new HashMap();
582                 trackSolo = new HashMap();
583                 copyBuffer = new ArrayList();
584
585                 // Searches track 0 for changes in tempo and time signature.
586                 MidiEvent event;
587                 MetaMessage metaMsg;
588                 ArrayList ts = new ArrayList(), tc = new ArrayList();
589                 for (int i = 0; i < tracks[0].size(); i++) {
590                         event = tracks[0].get(i);
591                         if (event.getMessage().getStatus() == MetaMessage.META) {
592                                 metaMsg = (MetaMessage)event.getMessage();
593                                 switch(metaMsg.getType()) {
594                                         case 81: tc.add(event); break;
595                                         case 88: ts.add(event);
596                                 }
597                         }
598                 }
599                 // timeSignatures = ts.toArray(timeSignatures);
600                 // tempoChanges = tc.toArray(tempoChanges);
601
602                 // Converts tracks.
603                 for (int i = 0; i < tracks.length; i++) {
604                         convertTrack(tracks[i]);
605                 }
606
607                 // Sends sequence to GUI and sequencer, then returns
608                 if (gui != null) gui.setSequence(seq);
609                 try {
610                         sequencer.setSequence(seq);
611                 } catch (InvalidMidiDataException e) {}
612                 return true;
613         }
614
615         /** 
616          * Prompts the user .
617          */
618         public static boolean promptOnUnsavedChanges() {
619                 if (!edited) return false;
620                 int exitOption = JOptionPane.showConfirmDialog(gui,
621                         "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
622                         "File not saved - continue?", 
623                         JOptionPane.OK_CANCEL_OPTION, 
624                         JOptionPane.WARNING_MESSAGE);
625                 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
626                 return false;
627         }
628
629         /** 
630          * Saves the current sequence to the previously given filename.
631          */
632         public static boolean save() {
633                 if (filename == null) return false;
634                 else {
635                         saveAs(filename);
636                         return true;
637                 }
638         }
639
640         /** 
641          * Saves the current sequence to the given filename
642          * @param file  the filename to use
643          */
644         public static boolean saveAs(String file) {
645                 try {
646                         MidiSystem.write(seq, 1, new File(file));
647                         filename = file;
648                         edited = false;
649                         gui.setStatus("Saved " + file);
650                         return true;
651                 } catch (IOException e) {
652                         gui.setStatus("Failed in saving " + file);
653                         return false;
654                 }
655         }
656
657         /** 
658          * Releases all reserved devices and exits the program.
659          */
660         public static void quit() {
661                 if (gui != null) {
662                         if (promptOnUnsavedChanges()) return;
663                 }
664                 if (sequencer.isOpen()) sequencer.close();
665                 if (synthesizer.isOpen()) synthesizer.close();
666                 System.exit(0);
667         }
668         
669         /** 
670          * A Comparator for sorting lists of MidiEvents.
671          */
672         public static class MidiEventComparator implements Comparator {
673                 public int compare(Object o1, Object o2) {
674                         return (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
675                 }
676         }
677 }