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