| BufferedMidiPlayer2.java |
1 package fi.jyu.mit.Music;
2
3 import java.util.ArrayList;
4 import java.util.List;
5
6 import javax.sound.midi.InvalidMidiDataException;
7 import javax.sound.midi.MidiEvent;
8 import javax.sound.midi.MidiSystem;
9 import javax.sound.midi.MidiUnavailableException;
10 import javax.sound.midi.Sequence;
11 import javax.sound.midi.Sequencer;
12 import javax.sound.midi.ShortMessage;
13 import javax.sound.midi.Track;
14
15 /**
16 * Puskuroitu äänijärjestelmä, jonka tarkoitus on tukea mahdollisuutta
17 * syöttää äänijärjestelmälle nuotteja, jotka voidaan toistaa
18 * joko pääohjelman säikeessä tai uudessa säikeessä.
19 *
20 * Tälläin käskyt play ja playNote eivät toista saamiansa nuotteja heti,
21 * vaan ne puskuroidaan, eli varastoidaan. Soittaminen aloitetaan metodeilla
22 * run tai runAsync.
23 *
24 * Metodilla run äänijärjestelmä aloittaa soittamisen nykyisessä säikeessä,
25 * kun taas runAsync tekee uuden säikeen joka sitten pyörii niin kauan kunnes
26 * soitto on loppunut.
27 *
28 * Soittimessa on useita tarckejä (tai kanaviksikin voi ajatella),
29 * joihin voi "soittaa" eri soittimia. Trackin voi tahdistaa
30 * keskenään jos johonkin ei ole pitkän aikaan annettu nuotteja.
31 *
32 * Asynkronisen soittamisen voi aina lopettaa käskyllä stop.
33 *
34 * @author vesal
35 * @version 12.9.2010
36 *
37 * TODO: rinnakkain soivat nuotit esim syntaksilla A(FG)A
38 * voidaan tehdä esim. niin, että nuotti voi koostua nuoteista
39 * ja koostetun nuotin getLength palauttaa pisimmän sen sisällä olevan nuotin.
40 * Entä miten voidaan sanoa että ensin alkuun yksi nuotti ja ennen kuin
41 * se loppuu alkaa toinen ja ne soivat vähän aikaa yhtäaikaa?
42 * Pitäisikö sittenkin sanoa tyyliin: A(-F*+G*-)A eli ensin
43 * soitetaan A, sitten alkaa yhtäaikaisuus jossa toiseen taukoa,
44 * toisessa jo alkaa G, sitten toisessa F ja soittoa jää päällekkein
45 * yhden nuotin verran ja F soi yksin loppuun (G:n perässä olevaa taukoa
46 * ei tarvittaisi oikeastaan) ja sitten kun F loppuu, jatkuu pelkäällä A:lla.
47 * Tällöin pienen kirjoitettu yhtäaikaisuus olisi A(F+G)A
48 * TODO: miettivät onko tempo (bmp) otettu huomioon oikeassa paikassa, pitäisikö
49 * huomioida vasta soittohetkellä?
50 * TODO: saako nuotit poistaa soittamisessa?
51 * TODO: pitäisikö soittamisen ajaksi tehdä kopio trackeistä?
52 * TODO: ASCII-tiedoston soitti niin, että sieltä luetaan bpm, kanavien soittimet
53 * ja kullekin kanavalle tulevat nuotit, esim, syntaksilla 0: CCCA
54 * TODO: pitäisikö syntaksia laajentaa sen verran että voisi sanoa myös
55 * A*3/4 joka olisi nätimpi kuin A*0.75
56 */
57 public class BufferedMidiPlayer2 extends MidiPlayer implements BasicMidiPlayer {
58
59
60 /**
61 * Nuottipuskuri. Soitetaan tulojärjestyksessä.
62 */
63 private List<NoteTrack> tracks;
64
65 private Sequencer sequencer = null;
66
67 public BufferedMidiPlayer2() {
68 tracks = new ArrayList<NoteTrack>();
69 tracks.add(new NoteTrack());
70 }
71
72
73 public BufferedMidiPlayer2(int channel) {
74 this();
75 selectChannel(channel);
76 }
77
78
79 /**
80 * Luo kopion puskuroiduista nuoteista
81 * @param index kopioitavan trackin numero
82 * @return kopio nuoteista
83 */
84 public List<Note>getCopyOfNotes(int index) {
85 return getTrack(index).getCopyOfNotes();
86 }
87
88 /**
89 * Palauttaa nuotit viitteenä jolloin niiden muuttaminen muuttaa soitettavia nuotteja
90 * @param index kopioitavan trackin numero
91 * @return nuotit
92 */
93 public List<Note>getNotes(int index) {
94 return getTrack(index).getNotes();
95 }
96
97 /**
98 * Tallentaa nuotin puskuriin.
99 */
100 public void midiSoundNote(int note, int length, int velocity) {
101 tracks.get(0).add(new Note(note, length, velocity));
102 }
103
104 @Override
105 protected void rest(final int barLength) {
106 tracks.get(0).add(new Note.Delay(barLength));
107 }
108
109
110 /**
111 * Aloittaa soiton rinnakkaisesti ja jatkaa kunnes se loppuu.
112 * Soiton loputtua on kutsuttava stop(), muuten ohjelma jää käyntiin.
113 */
114 public void runAsync() {
115 long mul = 1;
116 try {
117 Sequence song = new Sequence(0, 4 * 120 ); //* getTempo());
118 Track track = song.createTrack();
119 for (int channel = 0; channel < tracks.size(); channel++) {
120 NoteTrack nt = tracks.get(channel);
121 if (nt == null) continue;
122 if ( nt.isMuted() ) continue;
123 long tick = nt.getStartTime()*mul;
124 ShortMessage msgInst = new ShortMessage();
125 msgInst.setMessage(ShortMessage.PROGRAM_CHANGE, channel, nt.getInstrument() - 1, 0);
126 track.add(new MidiEvent(msgInst, tick));
127 while (!nt.isEmpty()) {
128 Note n = nt.remove(0);
129 if (n.isDelay()) {
130 tick += mul * n.getLength();
131 message("Tauko " + n.message());
132 } else {
133 try {
134 ShortMessage msgOn = new ShortMessage();
135 msgOn.setMessage(ShortMessage.NOTE_ON, channel, n.getNote(), n.getVelocity());
136 track.add(new MidiEvent(msgOn, tick));
137 message("Soitin nuotin " + n.message());
138 tick += mul * n.getLength();
139 ShortMessage msgOff = new ShortMessage();
140 msgOff.setMessage(ShortMessage.NOTE_OFF, channel, n.getNote(), n.getVelocity());
141 track.add(new MidiEvent(msgOff, tick));
142 } catch (InvalidMidiDataException e) {
143 }
144 }
145
146 }
147 }
148 sequencer = MidiSystem.getSequencer();
149 sequencer.open();
150 sequencer.setSequence(song);
151
152 if ( isVerbose() ) printSequence(song);
153
154 // Aloita soitto
155 sequencer.start();
156 } catch (InvalidMidiDataException e) {
157 error("Virhe luettaessa MIDI-dataa! Annoithan MIDI-tiedoston? (.mid)");
158 } catch (MidiUnavailableException e) {
159 error("MIDI-soitinta ei voida alustaa.");
160 }
161
162 }
163
164
165 /**
166 * Pysäyttää asynkronisen soiton.
167 */
168 public void stop() {
169 if ( sequencer != null && sequencer.isRunning() ) {
170 sequencer.stop();
171 }
172 sequencer.close();
173 sequencer = null;
174 }
175
176
177 /**
178 * Ajaa kaikki trackit niin, että päääohjelmaan palataan vasta kun kaikki soitettu
179 */
180 public void run() {
181 runAsync();
182 while ( sequencer.isRunning() ) {
183 try {
184 Thread.sleep(200);
185 } catch (InterruptedException e) {
186 }
187 }
188 stop();
189 }
190
191
192 /**
193 * Vaihtaa instrumentin. Ks. <a href="http://www.midi.org/techspecs/gm1sound.php#instrument">spesifikaatio</a>.
194 * @param instrument Instrumentti väliltä 1-128.
195 */
196 public void setInstrument(final int instrument) {
197 setInstrument(0, instrument);
198 }
199
200 /**
201 * Vaihtaa instrumentin. Ks. <a href="http://www.midi.org/techspecs/gm1sound.php#instrument">spesifikaatio</a>.
202 * @param index track johon instrumentti vaihdetaan
203 * @param instrument Instrumentti väliltä 1-128.
204 */
205 public void setInstrument(final int index, final int instrument) {
206 super.setInstrument(instrument);
207 getTrack(index).setInstrument(instrument);
208 }
209
210
211 /**
212 * Palauttaa pyydetyn trackin. Jos ei ole, luodaan
213 * @param index mikä track halutaan
214 * @return uusi track tai indeksistä löytyvä track.
215 */
216 public NoteTrack getTrack(int index) {
217 NoteTrack nt;
218 if ( index < 0 ) return tracks.get(0);
219 if ( index < tracks.size() ) {
220 nt = tracks.get(index);
221 if ( nt != null ) return nt;
222 nt = new NoteTrack();
223 tracks.set(index,nt);
224 return nt;
225 }
226 while ( index >= tracks.size() ) tracks.add(null);
227 nt = new NoteTrack();
228 tracks.set(index,nt);
229 return nt;
230 }
231
232
233 /**
234 * Soittaa nuotit
235 * @param index track jolla soitetaan
236 * @param notes soitettavat nuotit
237 */
238
239 public void play(int index, final List<Note> notes) {
240 for (Note n : notes) {
241 getTrack(index).add(n);
242 }
243 }
244
245 /**
246 * Soittaa nuotit 0-trackille
247 * @param notes soitettavat nuotit
248 */
249 @Override
250 public void play(final List<Note> notes) {
251 play(0,notes);
252 }
253
254
255
256 /**
257 * @param index track jolla soitetaan
258 * @param sequence Nuotit
259 * @param octave Oktaavi
260 * @param length Yksittäisen nuotin kesto
261 * @param velocity voimakkuus, 64 on normaali
262 */
263 public void play(int index,final String sequence, final double length, final int octave, final int velocity) {
264 play(index, getNotes(sequence, length, octave, velocity));
265 }
266
267 public void play(int index,final String sequence, final double length, final int octave) {
268 play(index,sequence, length, octave, getVelocity());
269 }
270
271 public void play(int index,final String sequence, final double length) {
272 play(index,sequence, length, 4, getVelocity());
273 }
274
275 public void play(int index,final String sequence) {
276 play(index,sequence, 0.25, 4, getVelocity());
277 }
278
279
280 /**
281 * Vaimennetaan tietty track pois
282 * @param index
283 * @param muted
284 */
285 public void mute(int index,boolean muted) {
286 getTrack(index).mute(muted);
287 }
288
289 /**
290 * @param index tutkittava track
291 * @return true jos vaimennettu, muuten false
292 */
293 public boolean isMuted(int index) {
294 return getTrack(index).isMuted();
295 }
296
297 /**
298 * Palauttaa kavan nykyajanhetken kanavan alusta
299 * @param index mikä kanava
300 * @return nykyajanhetki
301 */
302 public int getCurrentTime(int index) {
303 return getTrack(index).getCurrentTime();
304 }
305
306
307 /**
308 * Asettaa alkuajan. Voi käyttää esim jos toinen kavan on jo
309 * paljon soittanut ja halutaan aloittaa toinen nykykohdasta.
310 * esim: mp.setStartTime(1, mp.getCurrentTime(0));
311 * @param index minkä kanvan aloitusaika asetetaan
312 * @param startTime uusi aloitusaika
313 */
314 public void setStartTime(int index, int startTime) {
315 getTrack(index).setStartTime(startTime);
316 }
317
318
319 /**
320 * Toinen tapa saada kanavat samaan tahtiin. Lisää tauon, jolla
321 * päästään samaan kohtaan. Lisätään taukjo siihen kanavaan, jossa
322 * on vähemmän aikaa.
323 * @param dest mikä kanava synkronoidaan
324 * @param source mihin kanavaan
325 */
326 public void sync(int dest, int source) {
327 int sourceTime = getCurrentTime(source);
328 int destTime = getCurrentTime(dest);
329
330 int dt = sourceTime - destTime;
331
332 if ( dt > 0 ) getTrack(dest).add(new Note.Delay(dt));
333 if ( dt < 0 ) getTrack(source).add(new Note.Delay(dt));
334 }
335
336 }
337