Source code: jmqt/QTUtil.java
1 /*
2
3 <This Java Class is part of the jMusic API version 1.4, February 2003.>
4
5 Copyright (C) 2000 Andrew Sorensen & Andrew Brown
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or any
10 later version.
11
12 This program is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20
21 */
22
23 package jmqt;
24
25 import java.awt.*;
26 import java.awt.event.*;
27
28 import jm.JMC;
29 import jm.music.data.*;
30
31 import quicktime.*;
32 import quicktime.app.*;
33 import quicktime.app.time.*;
34 import quicktime.io.*;
35 import quicktime.qd.*;
36 import quicktime.sound.*;
37 import quicktime.std.movies.*;
38 import quicktime.std.movies.media.*;
39 import quicktime.std.music.*;
40 import quicktime.std.*;
41 import quicktime.std.qtcomponents.*;
42 //new for OS X
43 //import com.apple.mrj.macos.carbon.CarbonLock;
44
45 import quicktime.util.*;
46
47 /**
48 * A class which plays a jMusic score via Apple's QuickTime.
49 * Requires the QTJava classes from Apple Computer inc.
50 * Based on example code and help from Bill Stewart (thanks Bill).
51 * @author Andrew Brown
52 */
53
54 public class QTUtil implements JMC, StdQTConstants, SoundConstants {
55 private TunePlayer tunePlayer1;
56 private MusicData tune1;
57 private boolean usingTuneOne = true;
58 private Score score;
59 // set up the number of channels required
60 private NoteChannel[] nc1 = new NoteChannel[16];
61 private double speed = 1.0;
62 private ToneDescription td;
63 private double tempoMultiplier = 600.0;
64
65 //-------------
66 //constructor
67 //-------------
68
69 public QTUtil() {
70 try {
71 //CarbonLock.acquire(); // new for OS X
72 QTSession.open();
73 tunePlayer1 = new TunePlayer();
74 // create note channels for all 16 MIDI channels
75 setupNoteChannels();
76 tunePlayer1.setNoteChannels(nc1);
77 // create a tone desc for single note playing
78 td = new ToneDescription(1);
79 } catch (QTException qte) {
80 qte.printStackTrace();
81 } finally {
82 //CarbonLock.release(); // new for OS X
83 }
84 }
85
86 public QTUtil(Score s) {
87 try {
88 QTSession.open();
89 tunePlayer1 = new TunePlayer();
90 // create note channels for all 16 MIDI channels
91 this.setupNoteChannels();
92 tunePlayer1.setNoteChannels(nc1);
93 this.setScore(s);
94 // create a tone desc for single note playing
95 td = new ToneDescription(1);
96 } catch (QTException qte) { qte.printStackTrace(); }
97 }
98
99 //-------------------
100 // Auxillary methods
101 //-------------------
102
103 public void setScore(Score score) {
104 this.score = score;
105 try {
106 usingTuneOne = !usingTuneOne;
107 setUp();
108 } catch (QTException qte) { qte.printStackTrace(); }
109
110 }
111
112 public Score getScore() {
113 return score;
114 }
115
116 private void setUp() throws QTException {
117 //erase any empty parts or phrases
118 if(this.score == null)
119 System.err.println("jMusic QTtil error: The score is null. Please initialise before using.");
120 this.score.clean();
121 // check there is data to convert
122 if (totalNotesInScore() < 1) {
123 System.err.println(new Exception("jMusic EXCEPTION: QTUtil Error: The Score is empty!"));
124 (new Exception()).printStackTrace();
125 }
126 // set speed from tempo
127 this.speed = score.getTempo() / 60.0;
128 // setup QT stuff
129 setGMInstruments();
130 // Set the number of events (notes and rests) in the score
131 // Add one for an end-of-sequence marker
132 int totalNotes = totalNotesInScore();
133 // get notes from score into a format ready for QT
134 double[][] noteOrder = new double[totalNotes][4];
135 noteOrder = sortNotes(score, totalNotes);
136 // calcuate the size of the sequence
137 int qt_sequence_length = ((totalNotes * 2 + 1) * 4); // * 4 for 8 bit values
138 // this is the tune itself
139 tune1 = convertFromScore(score, noteOrder, qt_sequence_length);
140 }
141
142
143 private void setupNoteChannels() throws QTException {
144 //System.out.println("QTU set up note channels");
145 int theKit = 16385; //409;//"electric"
146 //a channel with 4 voice polyphony
147 for(int i=0; i<16; i++) {
148 if (i != 9) {
149 nc1[i] = new NoteChannel(1, 6);
150 //System.out.println("set up cannel "+ i);
151 } else {
152 nc1[9] = new NoteChannel(theKit, 8);
153 //System.out.println("set up cannel "+ i);
154 }
155 }
156 }
157
158
159 private void setGMInstruments() throws StdQTException {
160 //System.out.println("Score size is " + score.size());
161 int scoreSize = score.size();
162 for(int i=0; i < scoreSize; i++) {
163 Part currentPart = score.getPart(i);
164 if ( currentPart.getInstrument() != 250) { // skip pg?
165 if (currentPart.getChannel() != 9) { // drum channel?
166 nc1[ currentPart.getChannel()].setInstrumentNumber(currentPart.getInstrument()+1);
167 } else {
168 nc1[9].setInstrumentNumber(currentPart.getInstrument() + 16385);
169 }
170 }
171 }
172 }
173
174
175 private int totalNotesInScore() {
176 int totalNotes = 0;
177 final int scoreSize = score.size();
178 for(int i=0; i< scoreSize; i++) {
179 Part currentPart = score.getPart(i);
180 final int partSize = currentPart.size();
181 for(int j=0; j<partSize ; j++) {
182 totalNotes += currentPart.getPhrase(j).size();
183 }
184 }
185 return totalNotes;
186 }
187
188 private double[][] sortNotes(Score score, int totalNotes) {
189 //System.out.println("QTU sort");
190 // put all notes into an array according to startTimes
191 double[][] noteOrder = new double[totalNotes][4];
192 int index = 0;
193 int scoreSize = score.size();
194 for(int i=0; i < scoreSize; i++) { // for each part
195 Part currentPart = score.getPart(i);
196 int partSize = currentPart.size();
197 for(int j=0; j < partSize; j++) { // for each phrase
198 Phrase currentPhrase = currentPart.getPhrase(j);
199 double st = currentPhrase.getStartTime();
200 for(int k = 0; k < currentPhrase.size(); k++) { // for each note
201 if (k == 0) { // deal with offset on the first note
202 st += currentPhrase.getNote(k).getOffset();
203 if (st < 0.0) { // deal with a negative offset on the first note
204 st = 0.0;
205 Note tempNote = (Note)currentPhrase.getNote(k);
206 tempNote.setRhythmValue( tempNote.getRhythmValue() + tempNote.getOffset());
207 }
208 }
209 // insert the values for each note
210 noteOrder[index][0] = st; // start time
211 //System.out.println("Start time is being set to "+noteOrder[index][0]);
212 noteOrder[index][1] = (double) i; // part in score
213 noteOrder[index][2] = (double) j; // phrase in part
214 noteOrder[index][3] = (double) k; // note in phrase
215 st += currentPhrase.getNote(k).getRhythmValue();
216 if (k > 0) {st = st - currentPhrase.getNote(k - 1).getOffset()
217 + currentPhrase.getNote(k).getOffset();
218 }
219 if (st < 0.0) st = 0.0;
220 index++;
221 }
222 }
223 }
224
225 quickSort(noteOrder, 0, noteOrder.length - 1);
226
227 //double time2 = System.currentTimeMillis();
228 //System.out.println("Sort time is "+(time2-time1)/1000.0+" seconds");
229 return noteOrder;
230 }
231
232 private void quickSort(double[][] noteOrder, int left, int right) {
233 int i, last;
234 if(left >= right) return; // already sorted
235 swap(noteOrder, left, (int)(Math.random() * (right - left)) + left);
236 // choose new pivot point
237 last = left;
238 for (i = left+1; i<=right; i++) {
239 if(noteOrder[i][0] <= noteOrder[left][0]) swap(noteOrder, ++last, i);
240 }
241 swap(noteOrder, left, last); // restore pivot
242 quickSort(noteOrder, left, last-1);
243 quickSort(noteOrder, last+1, right);
244 }
245
246 static void swap(double[][] noteOrder, final int i, final int j) {
247 double temp;
248 for(int a=0;a<4;a++){
249 temp = noteOrder[i][a];
250 noteOrder[i][a] = noteOrder[j][a];
251 noteOrder[j][a] = temp;
252 }
253 }
254
255
256 private MusicData convertFromScore(Score score, double[][] noteOrder, int qt_sequence_length) throws QTException {
257 //System.out.println("QTU convert from score");
258 MusicData tune = new MusicData (qt_sequence_length * 3);
259 double currentTime = 0.0;
260 // duration in 600ths of a second -- @ 300 the tempo will be 120 bpm
261 double deltaTime = 0.0;
262 // event index
263 int eventIndex = 0;
264 // check for not starting at time 0.0
265 if ( noteOrder[0][0] > 0.0) {
266 // add a rest to pad it out until the start time
267 tune.setMusicEvent (eventIndex, MusicData.stuffRestEvent(
268 (int)(noteOrder[0][0] * tempoMultiplier)));
269 eventIndex++;
270 currentTime = noteOrder[0][0];
271 }
272 // iterate through the notes converting them to QT music events
273 for(int index=0;index<noteOrder.length - 1 ;index++){
274
275 Note n = score.getPart((int)noteOrder[index][1]).getPhrase(
276 (int)noteOrder[index][2]).getNote((int)noteOrder[index][3]);
277 deltaTime = noteOrder[index+1][0] * tempoMultiplier - currentTime;
278 // store current pan position
279 double lastPan = 0.5;
280 // create events
281 if (n.getPitch() == REST) {
282
283 tune.setMusicEvent ( eventIndex, MusicData.stuffRestEvent((int)(deltaTime)));
284 eventIndex++;
285 } else {
286 // each note requires a NoteEvent for its duration and
287 // a RestEvent for the distance before the next note
288 // Add a pan event (controller 10) for each note
289 if (n.getPan() != lastPan) {
290 tune.setMusicEvent (eventIndex, MusicData.stuffControlEvent(
291 score.getPart((int)noteOrder[index][1]).getChannel() + 1,
292 10, (int)(n.getPan() * 256) + 256)); // pan
293 eventIndex++;
294 lastPan = n.getPan();
295 }
296
297 tune.setXMusicEvent (eventIndex, MusicData.stuffXNoteEvent(score.getPart(
298 (int)noteOrder[index][1]).getChannel()+1,
299 n.getPitch() * 256, n.getDynamic(), (int)(n.getDuration() * tempoMultiplier)));
300 eventIndex+=2;
301 // set timing between events
302 tune.setMusicEvent (eventIndex, MusicData.stuffRestEvent((int)(deltaTime)));
303 eventIndex++;
304 }
305 // the rhythm value time
306 currentTime += deltaTime;
307 }
308 // deal with the last note
309 Note nLast = score.getPart((int)noteOrder[(noteOrder.length -1)][1]).
310 getPhrase((int)noteOrder[(noteOrder.length -1)][2]).
311 getNote((int)noteOrder[(noteOrder.length -1)][3]);
312 if (nLast.getPitch() == REST) {
313 tune.setMusicEvent ( eventIndex, MusicData.stuffRestEvent(
314 (int)(nLast.getRhythmValue()* tempoMultiplier)));
315 eventIndex++;
316 } else {
317 // Add a pan event (controller 10) for each note
318 tune.setMusicEvent (eventIndex, MusicData.stuffControlEvent(
319 score.getPart((int)noteOrder[noteOrder.length - 1][1]).getChannel() + 1,
320 10, (int)(nLast.getPan() * 256) + 256)); // pan
321 eventIndex++;
322 // each note requires a NoteEvent for its duration and
323 // a RestEvent for the distance before the next note
324 tune.setXMusicEvent (eventIndex, MusicData.stuffXNoteEvent(score.getPart(
325 (int)noteOrder[(noteOrder.length -1)][1]).getChannel()+1,
326 nLast.getPitch() * 256, nLast.getDynamic(),
327 (int)(nLast.getDuration() * tempoMultiplier)));
328 eventIndex+=2;
329 }
330 return tune;
331 }
332
333 //--------------------------------------
334 public void playback(Score s){
335 //System.out.println("QTU playback");
336 setScore(s);
337 replay();
338 }
339
340 /**
341 * Play the current score again.
342 */
343 public void replay() {
344 //System.out.println("QTU replay");
345 try {
346 if(tunePlayer1 != null) {
347 tunePlayer1.queue (tune1, (float)speed, 0, 0x7FFFFFFF, 0);
348 } else {System.out.println("Woops! No score to play.");}
349 } catch (QTException qte) { qte.printStackTrace(); }
350 }
351
352 /**
353 * Halt the current score playabck.
354 */
355 public void stopPlayback() {
356 try {
357 if(tunePlayer1 != null) {
358 tunePlayer1.stop();
359 }
360 } catch (QTException qte) { qte.printStackTrace(); }
361 }
362
363 /**
364 * return the current speed, which changes with the score tempo
365 */
366 public double getSpeed() {
367 return this.speed;
368 }
369
370 /**
371 * return the current speed, which changes with the score tempo.
372 * @param speed The tempo to play back with (1.0 = normal).
373 */
374 public void setSpeed(double speed) {
375 this.speed = speed;
376 }
377
378 // ----------------------------------------
379 /**
380 * Play a single note via QuickTime
381 * @param note The jMusic note to be played
382 * @param channel The MIDI channel on which to play the note
383 */
384 public void playOneNote(Note note, int channel) {
385 int tempPitch = note.getPitch();
386 try {
387 NoteChannel tempnc = nc1[channel];
388 tempnc.playNote(tempPitch, note.getDynamic()); // note on
389 try {
390 Thread.sleep ((long)(note.getDuration() * 1000)); // * 1000 = 60 bpm
391 } catch (InterruptedException e) {
392 System.out.println("jMusic QTUtil ERROR: Problem sleeping single note playback thread");
393 tempnc.playNote (tempPitch, 0); // note off
394 }
395 tempnc.playNote ( tempPitch, 0); // note off
396 } catch (QTException qte) {
397 qte.printStackTrace();
398 }
399
400 }
401
402 /**
403 * Send a MIDI control change message immediately
404 * Note: this method is at the mercy of QuickTime and may not work when QT is set to
405 * the internal QT musical instruments sound synthesizer. It does work when sending to
406 * external MIDI devices. QuickTime may also not support external MIDI devices on some
407 * implementations, for example Mac OS X 10.0. In these cases a standard QT exception
408 * will be printed to he command line.
409 * @param channel MIDI channel number from 0 - 15
410 * @param controllerNumber MIDI controller number from 1 - 127 .e.g, 7 = volume
411 * @param value data for the specified controller, between 0 - 127
412 */
413 public void sendMIDIControlChange(int channel, int controllerNumber, int value) {
414 int[] data = new int[2];
415 data[0] = controllerNumber;
416 data[1] = value;
417 sendMIDIMessage(176, channel, data);
418 }
419
420 /**
421 * Send any MIDI channel message immediately
422 * @param staus The MIDI staus value. A number between 127 - 255.
423 * @param channel MIDI channel to send meesage on, 0 - 15.
424 * @param dataByteArray a list of the data values, usually 1 or 2
425 * values depending on the message status type.
426 */
427 public void sendMIDIMessage(int status, int channel, int[] dataByteArray) {
428 //System.out.println("getting note channel");
429 NoteChannel tempnc = nc1[channel];
430
431 byte[] midiMessage = new byte[dataByteArray.length + 1];
432 MusicMIDIPacket mmp = new MusicMIDIPacket(midiMessage);
433 mmp.setDataByte(0, status + channel); // 8 bits to a byte..
434 // first bit is status or data
435 // next three bits are the message type. next four bits are channel
436 for(int i = 0; i < dataByteArray.length; i++) {
437 mmp.setDataByte(i + 1, dataByteArray[i]);
438 }
439 try {
440 tempnc.sendMIDI(mmp);
441 } catch (QTException qte) {
442 qte.printStackTrace();
443 }
444 }
445
446
447 /**
448 * Choose a general MIDI sound for playback of single notes
449 */
450 public void setNoteInstrument() {
451 try {
452 // Have the user choose an instrument and print out the choice
453 td.pickInstrument (NoteAllocator.getDefault(), "Choose an Instrument...", 0);
454 //System.out.println (td);
455 } catch (QTException e) {
456 e.printStackTrace();
457 }
458 }
459
460 /*
461 * Help Java finish the quickTime session elegantly
462 */
463 public void finalize() {
464 QTSession.close();
465 }
466
467 }