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