]> ruin.nu Git - moosique.git/blob - Moosique.java
Fixed drag select in track view.
[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 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 editing position of the sequencer.
146          * @return the tick position
147          */
148         public static long getEditPosition() {
149                 return editPosition;
150         }
151
152         /** 
153          * Returns the GUI.
154          * @return the GUI
155          */
156         public static MooGUI getGUI() {
157                 return gui;
158         }
159
160         /** 
161          * Calculates the position (measures, beats, ticks) in the current sequence for the given tick position.
162          * @return an array of integers where index 0 is measures, 1 is beats and 2 is ticks.
163          */
164         public static int[] getPositionForTick(long ticks) {
165                 /*
166                 int measures, beats, ticks;
167                 for (int i = 0; i < timeSignatures.length; i++) {
168                         long tick = timeSignatures[i].getTick();
169                         // Split the ticks in the interval into measures, beats and ticks.
170                 }
171                 */
172                 int[] pos = {1, 1, 1};
173                 return pos;
174         }
175
176         /** 
177          * Returns the receiver of the current sequencer.
178          * @return the receiver
179          */
180         public static Receiver getReceiver() {
181                 return receiver;
182         }
183
184         /** 
185          * Returns the current sequence.
186          * @return the current sequence
187          */
188         public static Sequence getSequence() {
189                 return seq;
190         }
191
192         /** 
193          * Returns the current sequencer.
194          * @return the current sequencer
195          */
196         public static Sequencer getSequencer() {
197                 return sequencer;
198         }
199
200         /** 
201          * Returns the tempo of the current sequence.
202          * @return the tick position
203          */
204         public static int getTempo() {
205                 return 120;
206                 // if (tempoMsg == null) return 0;
207         }
208
209         /** 
210          * Calculates the tick position in the current sequence for the given position (measures, beats, ticks).
211          * @return the tick position.
212          */
213         public static long getTicksForPosition(int measures, int beats, int ticks) {
214                 long tickPos = 0;
215                 /*
216                 for (int i = 0; i < timeSignatures.length; i++) {
217                         long tick = timeSignatures[i].getTick();
218                         // Add the measures, beats and ticks in the interval.
219                 }
220                 */
221                 return tickPos;
222         }
223
224         /** 
225          * Returns the tempo of the current sequence.
226          * @return the tick position
227          */
228         public static int[] getTimeSig() {
229                 int[] ts = {4, 4};
230                 return ts;
231                 // if (timeSigMsg == null) return 0;
232         }
233
234         /** 
235          * Returns true if the current sequence has been edited.
236          * @return the tick position
237          */
238         public static boolean isEdited() {
239                 return edited;
240         }
241
242         /** 
243          * Returns whether the given track should be drawn
244          * @return true if the given track should be drawn
245          */
246         public static boolean shouldBeDrawn(Track track) {
247                 if (drawEmptyTracks) return true;
248                 else return (!emptyTracks.contains(track));
249         }
250
251
252
253
254
255
256
257
258         /*                              ***
259          **       MUTATOR METHODS        **
260          ***                              */
261
262
263
264
265
266
267
268
269         /** 
270          * Fast forwards the current sequence the given number of measures.
271          * @param measures      the number of measures to fast forward
272          */
273         public static void forward(long ticks) {
274                 editPosition += ticks;
275         }
276
277         /** 
278          * Rewinds the current sequence the given number of measures.
279          * @param measures      the number of measures to rewind
280          */
281         public static void rewind(long ticks) {
282                 editPosition -= ticks;
283         }
284
285         /** 
286          * Sets the currently active MidiChannel.
287          * @param channel       the number of the MidiChannel to activate
288          */
289         public static void setActiveChannel(int channel) {
290                 activeChannel = channels[channel];
291         }
292
293         /** 
294          * Sets whether empty tracks should be drawn
295          * @param state         true if empty tracks should be drawn
296          */
297         public static void setDrawEmptyTracks(boolean state) {
298                 drawEmptyTracks = state;
299         }
300
301         /** 
302          * Sets the current sequence as edited, which implies prompts when loading a new sequence.
303          */
304         public static void setEdited() {
305                 edited = true;
306         }
307
308         /** 
309          * Sets the current editing position of the sequencer.
310          * @param ticks         the tick position
311          */
312         public static void setEditPosition(long ticks) {
313                 editPosition = ticks;
314         }
315
316         /** 
317          * Sets the current editing position of the sequencer.
318          * @param ticks         the tick position
319          */
320         public static void setTempo(int bpm) {
321                 // tempoMsg
322         }
323
324         /** 
325          * Sets the current editing position of the sequencer.
326          * @param ticks         the tick position
327          */
328         public static void setTimeSig(int bpm) {
329                 // timeSigMsg
330         }
331         
332         public static void setTrackSolo(Track track, boolean on){
333                 trackSolo.put(track, new Boolean(on));  
334         }
335
336         public static void setTrackMute(Track track, boolean on){
337                 trackMute.put(track, new Boolean(on));  
338         }
339
340
341
342
343
344
345
346
347         /*                              ***
348          **       PLAYBACK METHODS       **
349          ***                              */
350
351
352
353
354
355
356
357
358         /** 
359          * Starts playback of the current sequence.
360          */
361         public static void play() {
362                 sequencer.setTickPosition(editPosition);
363                 resume();
364         }
365
366         /** 
367          * Resumes playback of the current sequence.
368          */
369         public static void resume() {
370                 gui.update(0);
371                 try {
372                         sequencer.setSequence(seq);
373                 } catch (InvalidMidiDataException e) {}
374                 Track[] tracks = seq.getTracks();
375
376                 sequencer.start();
377
378                 for (int i = 0; i < tracks.length; i++) {
379
380                         Object ob = trackSolo.get(tracks[i]);
381                         if(ob instanceof Boolean){
382                                 sequencer.setTrackSolo(i,((Boolean)ob).booleanValue());
383                         }
384
385                         ob = trackMute.get(tracks[i]);
386                         if(ob instanceof Boolean){
387                                 sequencer.setTrackMute(i,((Boolean)ob).booleanValue());
388                         }
389                 }
390
391
392                 // Disables input to volatile components
393                 // gui.disable();
394
395                 // Creates the visualisation thread and starts it.
396                 player = new Thread () {
397                         public void run() {
398                                 while(sequencer.isRunning()) {
399                                         // Updates the GUI with the current tick position.
400                                         gui.update(sequencer.getTickPosition());
401
402                                         // Puts the thread to sleep for as long as it takes
403                                         // the sequencer to reach the next sixteenth.
404                                         try {
405                                                 //sleep((long)((15000 / getTempo()) * (tickDiff / ticksPerSixteenth)));
406                                                 sleep (10);
407                                         } catch (InterruptedException e) {
408                                                 Moosique.stop();
409                                         }
410                                 }
411                                 Moosique.stop();
412                         }
413                 };
414                 player.start();
415         }
416
417         /** 
418          * Pauses playback of the current sequence.
419          */
420         public static void pause() {
421                 if (sequencer.isRunning()) {
422                         sequencer.stop();
423                 }
424                 if (player != null) player.interrupt();
425         }
426
427         /** 
428          * Stops playback of the current sequence.
429          */
430         public static void stop() {
431                 if (sequencer.isRunning()) {
432                         sequencer.stop();
433                 }
434                 sequencer.setTickPosition(editPosition);
435                 if (player != null) player.interrupt();
436                 gui.update((long)0);
437         }
438
439
440
441
442
443
444
445
446         /*                              ***
447          **       SYSTEM & IO METHODS    **
448          ***                              */
449
450
451
452
453
454
455
456
457         /** 
458          * Replaces the current sequence with a new one, holding three empty tracks.
459          */
460         public static void clearSequence() {
461                 // Creates a new sequence and sends it to the sequencer.
462                 try {
463                         seq = new Sequence(Sequence.PPQ, DEFAULT_RESOLUTION, DEFAULT_TRACKS);
464                         sequencer.setSequence(seq);
465                         filename = null;
466                         emptyTracks = new ArrayList();
467                         trackSolo = new HashMap();
468                         trackMute = new HashMap();
469                 } catch (InvalidMidiDataException e) {}
470                 // Sends sequence to GUI.
471                 if (gui != null) gui.setSequence(seq);
472         }
473
474         /** 
475          * Loads a MIDI sequence from the given file.
476          * @param filename      the filename to use
477          */
478         public static boolean load(String file) {
479                 // Loads sequence from file
480                 filename = file;
481                 try {
482                         seq = MidiSystem.getSequence(new File(filename));
483                 } catch (InvalidMidiDataException e) {
484                         return false;
485                 } catch (IOException e) {
486                         return false;
487                 }
488                 edited = false;
489
490                 Track[] tracks = seq.getTracks();
491                 emptyTracks = new ArrayList();
492                 trackMute = new HashMap();
493                 trackSolo = new HashMap();
494
495                 // Searches track 0 for changes in tempo and time signature.
496                 MidiEvent event;
497                 MetaMessage metaMsg;
498                 ArrayList ts = new ArrayList(), tc = new ArrayList();
499                 for (int i = 0; i < tracks[0].size(); i++) {
500                         event = tracks[0].get(i);
501                         if (event.getMessage().getStatus() == MetaMessage.META) {
502                                 metaMsg = (MetaMessage)event.getMessage();
503                                 switch(metaMsg.getType()) {
504                                         case 81: tc.add(event); break;
505                                         case 88: ts.add(event);
506                                 }
507                         }
508                 }
509                 // timeSignatures = ts.toArray(timeSignatures);
510                 // tempoChanges = tc.toArray(tempoChanges);
511
512                 // Wraps each NoteOn event with its NoteOff event in a MooNote
513                 ArrayList noteOns, noteOffs;
514                 for (int i = 0; i < tracks.length; i++) {
515                         // Searches the sequence for NoteOn and NoteOff events
516                         noteOns = new ArrayList(tracks[i].size() / 2);
517                         noteOffs = new ArrayList(tracks[i].size() / 2);
518                         for (int j = 0; j < tracks[i].size(); j++) {
519                                 event = tracks[i].get(j);
520                                 if (event.getMessage().getStatus() >= 144 &&
521                                     event.getMessage().getStatus() < 160) noteOns.add(event);
522                                 if (event.getMessage().getStatus() >= 128 &&
523                                     event.getMessage().getStatus() < 144) noteOffs.add(event);
524                         }
525                         noteOns.trimToSize();
526                         noteOffs.trimToSize();
527                         if (noteOns.size() == 0) emptyTracks.add(tracks[i]);
528                         
529                         // Sorts the note lists by tick position.
530                         Comparator c = new NoteComparator();
531                         Collections.sort(noteOns, c);
532                         Collections.sort(noteOffs, c);
533
534                         // Replaces each NoteOn event it with a MooNote containing a reference to the NoteOff event.
535                         Iterator iOn = noteOns.iterator(), iOff;
536                         MidiEvent on, off = null, nextOff;
537                         ShortMessage onMsg, nextOffMsg;
538                         while(iOn.hasNext()) {
539                                 on = (MidiEvent)iOn.next();
540                                 onMsg = (ShortMessage)on.getMessage();
541                                 iOff = noteOffs.iterator();
542                                 while(iOff.hasNext()) {
543                                         nextOff = (MidiEvent)iOff.next();
544                                         nextOffMsg = (ShortMessage)nextOff.getMessage();
545                                         if(onMsg.getChannel() == nextOffMsg.getChannel() &&
546                                            onMsg.getData1() == nextOffMsg.getData1() &&
547                                            c.compare(nextOff, on) > 0) {
548                                                 off = nextOff;
549                                                 iOff.remove();
550                                                 break;
551                                         }
552                                                 
553                                 }
554                                 tracks[i].remove(on);
555                                 if (off != null) {
556                                         tracks[i].add(new MooNote(on, off));
557                                 } else {
558                                         tracks[i].add(new MooNote(on, new MidiEvent((ShortMessage)on.getMessage().clone(), on.getTick() + 48)));
559                                 }
560                                 iOn.remove();
561                         }
562                 }
563                 // Sends sequence to GUI and sequencer, then returns
564                 if (gui != null) gui.setSequence(seq);
565                 try {
566                         sequencer.setSequence(seq);
567                 } catch (InvalidMidiDataException e) {}
568                 return true;
569         }
570
571         /** 
572          * Prompts the user .
573          */
574         public static boolean promptOnUnsavedChanges() {
575                 if (!edited) return false;
576                 int exitOption = JOptionPane.showConfirmDialog(gui,
577                         "The current sequence has been edited, but not saved.\nDo you wish to continue anyway?",
578                         "File not saved - continue?", 
579                         JOptionPane.OK_CANCEL_OPTION, 
580                         JOptionPane.WARNING_MESSAGE);
581                 if (exitOption == JOptionPane.CANCEL_OPTION || exitOption == JOptionPane.CLOSED_OPTION) return true;
582                 return false;
583         }
584
585         /** 
586          * Saves the current sequence to the previously given filename.
587          */
588         public static boolean save() {
589                 if (filename == null) return false;
590                 else {
591                         saveAs(filename);
592                         return true;
593                 }
594         }
595
596         /** 
597          * Saves the current sequence to the given filename
598          * @param file  the filename to use
599          */
600         public static boolean saveAs(String file) {
601                 try {
602                         MidiSystem.write(seq, 1, new File(file));
603                         filename = file;
604                         edited = false;
605                         gui.setStatus("Saved " + file);
606                         return true;
607                 } catch (IOException e) {
608                         gui.setStatus("Failed in saving " + file);
609                         return false;
610                 }
611         }
612
613         /** 
614          * Releases all reserved devices and exits the program.
615          */
616         public static void quit() {
617                 if (gui != null) {
618                         if (promptOnUnsavedChanges()) return;
619                 }
620                 if (sequencer.isOpen()) sequencer.close();
621                 if (synthesizer.isOpen()) synthesizer.close();
622                 System.exit(0);
623         }
624         
625         /** 
626          * A Comparator for sorting lists of MidiEvents.
627          */
628         public static class NoteComparator implements Comparator {
629                 public int compare(Object o1, Object o2) {
630                         return (int)(((MidiEvent)o1).getTick() - ((MidiEvent)o2).getTick());
631                 }
632         }
633 }