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