| MidiPlayer.java |
1 package fi.jyu.mit.Music;
2
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.File;
6 import java.io.FileNotFoundException;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
9 import java.net.HttpURLConnection;
10 import java.net.MalformedURLException;
11 import java.net.URL;
12 import java.util.ArrayList;
13 import java.util.Enumeration;
14 import java.util.List;
15 import java.util.Scanner;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
18 import java.util.zip.ZipEntry;
19 import java.util.zip.ZipException;
20 import java.util.zip.ZipFile;
21
22 import javax.sound.midi.*;
23
24 /**
25 * MidiPlayer-luokka sisältää varsinaisen äänijärjestelmän yksinkertaisen MIDI-datan toistoa varten.
26 * äänijärjestelmä kykenee soittamaan yhdellä instrumentilla nuoteista koostuvaa soittoa. Kaikki GM-1
27 * standardin tukemat instrumentit ovat käytässä. Standardin määritykset läytyvät <a href="http://www.midi.org/techspecs/gm1sound.php">spesifikaatiosta</a>.
28 *
29 * Oktaavi, ks: esim: http://fi.wikipedia.org/wiki/Oktaavi
30 *
31 * @see BufferedMidiPlayer
32 */
33 public class MidiPlayer implements BasicMidiPlayer {
34 /**
35 * Nuotin kestot tahdin desimaaliosissa.
36 */
37 public enum NoteDuration {
38 DOUBLE(2.0),
39 EIGHTH(0.125),
40 HALF(0.5),
41 QUARTER(0.25),
42 SIXTEENTH(0.0625),
43 SIXTYFOURTH(0.015625),
44 THIRTYSECOND(0.03125),
45 WHOLE(1.0);
46
47 private final double length;
48
49 NoteDuration(double length) {
50 this.length = length;
51 }
52
53 public double length() { return this.length; }
54 };
55
56 public static final int DEFVELOCITY = 64;
57 public static final int DEFOCTAVE = 4;
58 public static final String DELAYST = "-";
59
60
61 /**
62 * Lista nuoteista.
63 */
64 public static final String[] ALLNOTES = {
65 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "Bb", "B"
66 };
67 public static final String basicNotes = "C D EF G A B";
68 public static final String basicNotes2 = "C D EF G A H";
69
70 /**
71 * Tempo. Määräytyy BPM:n (beats per minute) mukaan, oletus on 120 eli
72 * 120 iskua minuutissa, tällöin yhden iskun pituus on 500 ms.
73 */
74 private int beatLength = 500; // 120 BPM
75
76 /**
77 * Nopeus jolla "kosketinta" lyödään, 64 on oletus
78 */
79 private int velocity = DEFVELOCITY;
80
81 /**
82 * Tulostetaanko debuggausviestejä. Oletuksena on, että ei, mutta toggleVerbosella sen voi muuttaa.
83 */
84 private boolean verbose = false;
85
86 /**
87 * Äänipankkitiedosto latausta varten.
88 */
89 private final String remoteSoundbankUrl = "http://java.sun.com/products/java-media/sound/soundbank-min.gm.zip";
90
91 /**
92 * Taulukko midikanavia varten.
93 */
94 protected MidiChannel[] channels;
95
96 /**
97 * Nykyinen käytässä oleva kanava. Oletuksena 0.
98 */
99 protected int currentChannel = 0;
100
101 /**
102 * Taulukko instrumentteja varten.
103 */
104 protected Instrument[] instruments;
105
106
107 /**
108 * Nykyinen instrumentti;
109 */
110 private int instrument = 1;
111
112
113 /**
114 * Iskuja minuutissa
115 */
116 private int bpm = 120;
117
118
119 /**
120 * Oletusoktaavi, josta nuotit soitetaan
121 */
122 private int defOctave = DEFOCTAVE;
123
124 public int getDefOctave() {
125 return defOctave;
126 }
127
128 public void setDefOctave(int defocatve) {
129 this.defOctave = defocatve;
130 }
131
132 /**
133 * Syntetisaattori.
134 */
135 protected Synthesizer synth;
136
137 /**
138 * Muodostaa äänijärjestelmän. Alustaa syntetisaattorin ym.
139 */
140 public MidiPlayer() {
141 this.init();
142 }
143
144 /**
145 * Muodostaa äänijärjestelmän, kuitenkaan syntetisaattoria alustamatta. Tämä tarpeen jos ei
146 * halua viedä syntesaattoria järjestelmällä joissa niitä on vain yksi.
147 * @param init Alustustehto. Jos true, niin syntetisaattori käynnistetään, jos false, niin ei.
148 */
149 public MidiPlayer(final boolean init) {
150 if (init)
151 this.init();
152 }
153
154 /**
155 * Jäsentää merkkijonon nuotit & tauot yksittäisiksi nuotteiksi.
156 * Jonossa saa olla ylmääräisiä välilyäntejä tai | merkkejä selkeyttämässä jakoa
157 * ilman että ne sotkevat mitään. Esimerkkjä käytöstä ks alla olevat testit.
158 *
159 * @param sequence Merkkijono nuoteista, esim. C#5
160 * @param defaultOctave Oletusoktaavi, jossa nuotit soitetaan, jos niille ei annettu oktaavia
161 * @param length Yksittäisen nuotin pituus jos nuotille ei erikeen ole annettu pituutta
162 * @param velocity voimakkuus, 64 on perus
163 * @return
164 * @example
165 * <pre name="test">
166 * #import java.util.*;
167 * MidiPlayer mp = new MidiPlayer(); List<Note> notes;
168 *
169 * notes = mp.getNotes("-b -/4 -#*",0.25,4,64); // - = tauko, ylennys tai alennus ei haittaa
170 * notes.get(0).isDelay() === true;
171 * notes.get(1).isDelay() === true;
172 * notes.get(2).isDelay() === true;
173 * notes.get(0).getLength() === 500;
174 * notes.get(1).getLength() === 125;
175 * notes.get(2).getLength() === 1000;
176 *
177 * notes = mp.getNotes("C/E/4D*F*4A.F*1.2",0.25,4,64); // /nro jakaa pituuden, *nro kertoo
178 * notes.get(0).isDelay() === false;
179 * notes.get(0).getLength() === 250; // / yksin on sama kuin /2
180 * notes.get(1).getLength() === 125; // /4 jakaa pituuden 4:llä
181 * notes.get(2).getLength() === 1000; // * yksin on sama kuin *2
182 * notes.get(3).getLength() === 2000; // *4 kertoo pituuden 4:llä
183 * notes.get(4).getLength() ~~~ 500*1.5; // kerrotaan pituus 1.5:lla
184 * notes.get(5).getLength() ~~~ 500*1.2; // ja 1.2lla
185 *
186 * notes = mp.getNotes("C C# D D# E F F# G G# A Bb B",0.25,4,64); // oktaavin kaikki nuotit
187 * for (int i=0; i<12; i++) {
188 * notes.get(i).getNote() === 60 + i;
189 * }
190 *
191 * notes = mp.getNotes("c c# d d# e f f# g g# a Bb B",0.25,4,64); // samat pienillä (paitsi B)
192 * for (int i=0; i<12; i++) {
193 * notes.get(i).getNote() === 60 + i;
194 * }
195 *
196 * notes = mp.getNotes("C Db D Eb E F Gb | G Ab A A# B",0.25,4,64); // ylennetään "eritavalla"
197 * for (int i=0; i<12; i++) {
198 * notes.get(i).getNote() === 60 + i;
199 * }
200 *
201 * notes = mp.getNotes("HBhbBb",0.25,4,64); // H ja B ovat synonyymejä
202 * notes.get(0).getNote() === notes.get(1).getNote()
203 * notes.get(2).getNote() === notes.get(3).getNote()
204 *
205 * notes = mp.getNotes("C1C2C3C4C5C6C7C8",0.25,4,64); // kokeillaan C:tä eri oktaaveista
206 * notes.get(0).getNote() === 60 - 3*12
207 * notes.get(1).getNote() === 60 - 2*12
208 * notes.get(2).getNote() === 60 - 1*12
209 * notes.get(3).getNote() === 60 - 0*12
210 * notes.get(4).getNote() === 60 + 1*12
211 * notes.get(5).getNote() === 60 + 2*12
212 * notes.get(6).getNote() === 60 + 3*12
213 * notes.get(7).getNote() === 60 + 4*12
214 *
215 * notes = mp.getNotes("E# F H# C5",0.25,4,64);
216 * notes.get(0).getNote() === notes.get(1).getNote()
217 * notes.get(2).getNote() === notes.get(3).getNote()
218 *
219 * notes = mp.getNotes("A#3* Hb3*",0.25,4,64); // Ylennetty A on sama kuin alennettu H molemmat 2*
220 * notes.get(0).getNote() === notes.get(1).getNote()
221 * notes.get(0).getLength() === notes.get(1).getLength()
222 * </pre>
223 */
224 public final List<Note> getNotes(final String sequence, final double length, final int defaultOctave, final int velocity) {
225 List<Note> notes = new ArrayList<Note>();
226 final Pattern p = Pattern.compile("(([-acdefghACDEFGBH])([#b]?)([0-8]?)([/*.][0-8.]*)?)");
227 final Matcher m = p.matcher(sequence);
228
229 Note n;
230 while (m.find()) {
231 // Nuotti on toinen täsmäysjoukko (ensimmäinen on tauko)
232 String note = m.group(2);
233 // Ylennys/madallusmerkki
234 String pitchMove = (m.group(3) != null) ? m.group(3) : "";
235 // Oktaavi
236 int octave = (!m.group(4).isEmpty()) ? Integer.parseInt(m.group(4)) : defaultOctave;
237 double factor = 1.0;
238 String sf = m.group(5);
239 char cf = 0;
240 if (sf != null && sf.length() > 0) cf = sf.charAt(0);
241 if (cf == '/') {
242 factor = 0.5;
243 String smult = sf.substring(1);
244 if (smult.length() > 0) factor = 1.0 / Double.parseDouble(smult);
245 }
246 if (cf == '*') {
247 factor = 2.0;
248 String smult = sf.substring(1);
249 if (smult.length() > 0) factor = Double.parseDouble(smult);
250 }
251 if (cf == '.')
252 factor = 1.5;
253
254 n = makeNote(note + pitchMove, length * factor, octave, velocity);
255 notes.add(n);
256 }
257 return notes;
258 }
259
260 /**
261 * Alustaa MIDI-syntetisaattorin.
262 * @throws MidiUnavailableException
263 * @throws InvalidMidiDataException
264 */
265 public void init() {
266 Soundbank s = null;
267 try {
268 synth = MidiSystem.getSynthesizer();
269 synth.open();
270 if ( verbose ) System.out.println("Ladattiin syntetisaattori " + synth);
271 s = synth.getDefaultSoundbank();
272 // Syntetisaattori ei saanut äänipankkia, joten ladataan sen etäpalvelimelta.
273 if (s == null) {
274 synth.close();
275 downloadSoundbank(remoteSoundbankUrl);
276 synth.open();
277 s = synth.getDefaultSoundbank();
278 // Jos ei vieläkään
279 if (s == null) {
280 error("Lataaminen epäonnistui tai äänipankkitiedosto on epäkelpo. Ks. https://trac.cc.jyu.fi/projects/ohj1/wiki/music");
281 }
282 }
283 } catch (MidiUnavailableException e) {
284 e.printStackTrace();
285 }
286 instruments = s.getInstruments();
287 if (instruments == null) {
288 error("Instrumenttien lataamisessa tapahtui virhe.");
289 }
290
291 synth.loadInstrument(instruments[1]); // piano
292 channels = synth.getChannels();
293 selectChannel(1);
294 }
295
296 /**
297 * Lataa äänipankin Sunin sivuilta.
298 * @param remote URL, josta ladataan
299 */
300 private void downloadSoundbank(String remote) {
301 HttpURLConnection source = null;
302 BufferedOutputStream bos = null;
303 BufferedInputStream bis = null;
304 final String zipFileName = "soundbank-min.gm.zip";
305 try {
306 File z = new File(zipFileName);
307 // Jos zip-pakettia ei löydy jo valmiiksi, ladataan se
308 if (!z.exists()) {
309 URL url = new URL(remote);
310 source = (HttpURLConnection) url.openConnection();
311 source.connect();
312 System.out.print("Haetaan äänipankki osoitteesta " + remote + " ... ");
313 // Tarkista palvelimen vastaus
314 String response = "HTTP " + source.getResponseCode() + " " + source.getResponseMessage();
315 // Jos 404 (ei löydy)
316 if (source.getResponseCode() != HttpURLConnection.HTTP_OK) {
317 System.err.println(response);
318 error("Äänipankkitiedoston latauksessa tapahtui virhe. Ks. https://trac.cc.jyu.fi/projects/ohj1/wiki/music");
319 }
320 System.out.println(response);
321 bis = new BufferedInputStream(source.getInputStream());
322 bos = new BufferedOutputStream(new FileOutputStream(zipFileName));
323 int count = 0;
324 byte[] buf = new byte[8192]; // HTTP-puskurin suositeltu koko
325 // Lataa tiedosto aina puskurin koko kerrallaan
326 int fileSize = source.getContentLength(); // tiedoston koko palvelimella
327 System.out.print("Ladataan " + zipFileName + " (" + fileSize/1000 + " kB) ... ");
328 for (int bytesRead = bis.read(buf); bytesRead > 0; bytesRead = bis.read(buf)) {
329 count += bytesRead;
330 bos.write(buf, 0, bytesRead);
331 }
332 System.out.println(count/1000 + " kB ladattu");
333 bis.close();
334 bos.close();
335 source.disconnect();
336 }
337 // Puretaan tiedosto
338 try {
339 ZipFile pkg = new ZipFile(new File(zipFileName));
340 // Hae paketin sisältö
341 Enumeration<? extends ZipEntry> entries = pkg.entries();
342 while (entries.hasMoreElements()) {
343 ZipEntry ze = (ZipEntry)entries.nextElement();
344 // Pura vain .gm-päätteiset tiedostot
345 if (!ze.isDirectory() && ze.getName().endsWith(".gm")) {
346 System.out.println("Puretaan äänipankki " + ze.getName());
347 // Kopioi tiedoston sisältö
348 BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(ze.getName()));
349 BufferedInputStream in = new BufferedInputStream(pkg.getInputStream(ze));
350 int data;
351 while ((data = in.read()) != -1) {
352 out.write((byte) data);
353 }
354 out.close();
355 in.close();
356 }
357 }
358 } catch (ZipException ze) {
359 System.err.println("Virhe avattaessa zip-tiedostoa: " + ze.getMessage());
360 }
361 }
362 catch (MalformedURLException mue) {
363 error("Virheellinen URL");
364 } catch (IOException e) {
365 // TODO Auto-generated catch block
366 e.printStackTrace();
367 }
368 }
369
370 /**
371 * Muuntaa nuotin, oktaavin ja voimakkuuden Note-luokan alkioksi. <b>Huom.! Nuotin perään ei tule oktaavia.</b>
372 * Tauko on - -merkki.
373 * @param note Nuotti tekstimuodossa, esim. C# tai Bb
374 * @param length Nuotin pituus tahdin kokonaisosina
375 * @param octave Oktaavi (0-8)
376 * @param velocity voimakkuus, 64 on normaali
377 * @return Muunnettu nuotti.
378 * @see Note
379 * @example
380 * <pre name="test">
381 * MidiPlayer mp = new MidiPlayer();
382 * Note note;
383 *
384 * note = mp.makeNote("C",0.5,4,64);
385 * note.getLength() === 1000;
386 * note.getNote() === 60;
387 *
388 * note = mp.makeNote("C#",0.5,4,64); // korotus
389 * note.getNote() === 61;
390 *
391 * note = mp.makeNote("H#",0.5,4,64); // korotettu H on seuraava C
392 * note.getNote() === 72;
393 *
394 * note = mp.makeNote("Bb",0.5,4,64); // Alennettu B (H)
395 * note.getNote() === 70;
396 *
397 * note = mp.makeNote("-",0.5,4,64); // Tauko
398 * note.isDelay() === true;
399 *
400 * note = mp.makeNote("-#",0.5,4,64); // Tauko, ylennys ei haittaa
401 * note.isDelay() === true;
402 *
403 * note = mp.makeNote("-b",0.5,4,64); // Tauko, alennus ei haittaa
404 * note.isDelay() === true;
405 * </pre>
406 */
407 public final Note makeNote(final String note, final double length, final int octave, final int velocity) {
408 final int noteLength = (int)((this.beatLength * 4) * length);
409
410 if ( octave < 0 || note.length() < 1 || DELAYST.indexOf(note.charAt(0)) >= 0 )
411 return new Note.Delay(noteLength);
412
413 char noteChar = Character.toUpperCase(note.charAt(0));
414 char pitchMove = 0;
415 if ( note.length() > 1 ) pitchMove = note.charAt(1);
416
417 // Tarkistetaan että nuotit & oktaavit ovat oikein
418
419 int noteIndex = basicNotes.indexOf(noteChar);
420 if ( noteIndex < 0 ) noteIndex = basicNotes2.indexOf(noteChar);
421
422 if ( noteIndex < 0 ) {
423 error("Virheellinen nuotti: " + note);
424 return null;
425 }
426 if (octave < 0 || octave > 8) {
427 error("Virheellinen oktaavi: " + octave);
428 return null;
429 }
430 if (length < 0.0625) {
431 error("Virheellinen nuotin kesto, " + length + " minimi: 0.0625 - 1");
432 return null;
433 }
434 int base = 24;
435 int resultNote = 0;
436
437 if ( pitchMove == '#' ) noteIndex++;
438 if ( pitchMove == 'b' ) noteIndex--;
439
440 // Valitaan oktaavi. Ensimmäisesä 0-oktaavissa on vain kolme nuottia (A, Bb, B), joten
441 // indeksin kaksi yli menevät (C-G#) tuottavat virheen.
442 if (octave == 0) {
443 if (noteIndex < 9) {
444 error("Virhe: nollaoktaavissa soivat vain A, Bb ja B, koitit: " + note);
445 } else {
446 resultNote = 21 + (noteIndex - 9);
447 }
448 } else {
449 resultNote = base + ((octave - 1) * 12) + noteIndex;
450 }
451 return new Note(resultNote, noteLength, velocity);
452 }
453
454
455 /**
456 * Muuntaa nuotin, oktaavin ja voimakkuuden Note-luokan alkioksi. <b>Huom.! Nuotin perään ei tule oktaavia.</b>
457 * @param note Nuotti tekstimuodossa, esim. "C#"
458 * @param length Nuotin pituus tahdin kokonaisosina
459 * @param octave Oktaavi (0-8)
460 * @return muutettu nuotti
461 */
462 public final Note makeNote(final String note, final double length, final int octave) {
463 return makeNote(note,length,octave,this.velocity);
464 }
465
466 /**
467 * Soittaa kanavalta MIDI-nuotin (21-108).
468 * @param note Nuotti (21-108)
469 * @param length Kesto millisekunteina
470 * @param velocity Nuotin nk. painovoimakkuus. Oletus on 64.
471 */
472 public void midiSoundNote(final int note, final int length, final int velocity)
473 {
474 checkSynth("midiSoundNote");
475 try
476 {
477 channelOn(note, velocity);
478 message("Soitin nuotin " + Note.message(note, length, velocity));
479 Thread.sleep(length);
480 channelOff(note);
481 }
482 catch (InterruptedException e)
483 {
484 channelOff(note);
485 }
486 }
487
488 /**
489 * Aloittaa kanavalta MIDI-nuotin (21-108).
490 * @param note Nuotti (21-108)
491 * @param length Kesto millisekunteina
492 * @param velocity Nuotin nk. painovoimakkuus. Oletus on 64.
493 */
494 public void midiStartNote(final int note, final int length, final int velocity)
495 {
496 checkSynth("midiStartNote");
497 channelOn(note, velocity);
498 message("Aloitin nuotin " + Note.message(note, length, velocity));
499 }
500
501 /**
502 * Aloittaa kanavalta MIDI-nuotin (21-108).
503 * @param note Nuotti (21-108)
504 * @param length Kesto millisekunteina
505 * @param velocity Nuotin nk. painovoimakkuus. Oletus on 64.
506 */
507 public void midiEndNote(final int note, final int length, final int velocity)
508 {
509 checkSynth("midiEndNote");
510 message("Lopetin nuotin " + Note.message(note, length, velocity));
511 channelOff(note);
512 }
513
514 /**
515 * Toistaa nuotteja. Nuottien kesto oletetaan neljäsosanuoteiksi.
516 * @param sequence Nuotit
517 */
518 @Override
519 public void play(final String sequence){
520 play(sequence, 0.25, getDefOctave());
521 }
522
523 /**
524 * Toistaa nuotteja tietyssä oktaavissa neljäsosanuotteina.
525 * @param sequence Nuotit
526 * @param octave Oktaavi
527 */
528 @Override
529 public void play(final String sequence, final double length) {
530 play(sequence, length, getDefOctave());
531 }
532
533 /**
534 * Toistaa merkkijonon neljäsosanuotteja perusoktaavissa (4), esim. "C5CC5CBbAA#". Huomaa, että "Bb" on aina annettava
535 * sellaisenaan, sillä parseri ei osaa erottaa onko kyseessä "bb" vai "BB", parseri olettaa että tuolloin on kaksi
536 * B-nuottia.
537 *
538 *
539 * Syntaksi yksittäiselle nuotille on seuraava: ABCD, jossa
540 * <ul>
541 * <li><i>A</i> on itse nuotti <b>(pakollinen)</b></li>
542 * <li><i>B</i> on nuotille ylennysmerkki # tai alennusmerkki b</li>
543 * <li><i>C</i> on nuotille oktaavi</li>
544 * <li><i>D</i> on nuotin kesto suhteessa peruspituuteen joko /jakaja tai *kerroin. Jos jakaja tai kerroin puuttu, se on 2</li>
545 * </ul>
546 * Mikä tahansa osista B, C tai D voi puuttua.
547 * <pre>
548 * Esim:
549 * C#A on ylennetty C ja A, oktaavista 4, kesto perusnuotin verran (yleensä siis 1/4 nuotti)
550 * C5 C 5:stä oktaavista
551 * C*2 on perusnuottia 2 pidempi nuotti (yleensä siis puolinuotti)
552 * C/4 C jonka kesto on 1/4 perusnuotista (siis 1/16 nuotti yleensä)
553 * A/ A jonka kesto on puoleta perusnuotista (yleensä siis 1/8 nuotti)
554 * C#5*4 4 kertaa normaali pidempi ylennetty C 5. oktaavista
555 * D. 1.5 kertaa normaalia pidempi D, eli sama kuin D*1.5
556 * </pre>
557 * @param sequence Nuotit merkkijonona, jonossa olevat välilyönnit ja | eivät haittaa
558 * kunhan niitä ei laiteta nuotin sisälle
559 * @param octave Oletusoktaavi. Jos nuotille ei ole omaa oktaavia, käytettään tätä
560 * @param length Yksittäisen nuotin kesto, tähän suhteutetaan muut jonon nuotit
561 * @param velocity voimakkuus, 64 on normaali
562 */
563 @Override
564 public void play(final String sequence, final double length, final int octave, final int velocity) {
565 play(getNotes(sequence, length, octave, velocity));
566 }
567
568
569 @Override
570 public void play(final String sequence, final double length, final int octave) {
571 play(sequence, length, octave, this.velocity);
572 }
573
574
575 /**
576 * Soittaa nuotit nuottien listasta
577 * @param notes soitettavat nuotit
578 */
579 public void play(final List<Note> notes) {
580 for (Note n : notes) {
581 // Tauko
582 if (n.getNote() < 0 ) {
583 rest(n.getLength());
584 continue;
585 }
586 midiSoundNote(n.getNote(), n.getLength(), n.getVelocity());
587 }
588 }
589
590
591 /**
592 * Toistaa ASCII-tiedoston.
593 * @param filename
594 */
595 public void playAsciiFile(final String filename) {
596 try {
597 final Scanner sc = new Scanner(new File(filename));
598 // Toistetaan sisältä
599 String notes = sc.next();
600 play(notes);
601 } catch (FileNotFoundException e) {
602 error("Tiedostoa " + filename + " ei läytynyt!");
603 }
604 }
605
606 /**
607 * Toistaa MIDI-tiedoston niin että ohjelma vi jatkaa muita tehtäviä.
608 * Tälläin palautettu sequencer pitä ottaa vastaan ja sanoa sille
609 * joskus close.
610 * @param filename Tiedoston nimi, oltava midi-tiedosto
611 * @return seuencer jolla soitetaan, niin voi pysäyttää milloin haluaa
612 */
613 public Sequencer playMidiFileAsync(final String filename) {
614 try {
615 // Lataa MIDI-tiedosto
616 final Sequence song = MidiSystem.getSequence(new File(filename));
617 // Lataa sekvensseri
618 Sequencer sequencer = MidiSystem.getSequencer();
619 sequencer.open();
620 sequencer.setSequence(song);
621
622 if ( isVerbose() ) printSequence(song);
623
624 // Aloita soitto
625 sequencer.start();
626 message("Soitetaan MIDI-tiedosto: " + filename);
627 return sequencer;
628 } catch (InvalidMidiDataException e) {
629 error("Virhe luettaessa MIDI-dataa! Annoithan MIDI-tiedoston? (.mid)");
630 } catch (IOException e) {
631 error("Virhe avattaessa MIDI-tiedostoa: " + e.getMessage());
632 } catch (MidiUnavailableException e) {
633 error("MIDI-soitinta ei voida alustaa.");
634 }
635 return null;
636 }
637
638 /**
639 * Soittaa valitun midi-tiedoston ja odottaa soiton loppumista.
640 * @param filename
641 */
642 public void playMidiFile(final String filename) {
643 Sequencer sequencer = playMidiFileAsync(filename);
644 if ( sequencer == null ) return;
645 while ( sequencer.isRunning() ) {
646 try {
647 Thread.sleep(200);
648 } catch (InterruptedException e) {
649 }
650 }
651 sequencer.close();
652 }
653
654
655 private String messageInfotoString(MidiMessage message) {
656 if (! ( message instanceof ShortMessage ) ) return message.toString();
657 ShortMessage m = (ShortMessage)message;
658 return String.format("Channel %d command: %d data %d %d",m.getChannel(),m.getCommand(),m.getData1(), m.getData2());
659 }
660
661
662
663 public void printSequence(Sequence song) {
664 // Print sequence information
665 System.out.println( " length: " + song.getTickLength() + " ticks" );
666 System.out.println( " duration: " + song.getMicrosecondLength() + " micro seconds" );
667 //System.out.println( " division type: " + divisionTypeToString( song.getDivisionType() );
668 System.out.println( " division type: " + song.getDivisionType() );
669 System.out.println( " resolution type: " + song.getResolution() );
670
671 // Print track information
672 Track[] tracks = song.getTracks();
673 if (tracks == null) return;
674 for (int i = 0; i < tracks.length; i++) {
675 System.out.println("Track " + i + ":");
676 Track track = tracks[i];
677 for (int j = 0; j < track.size(); j++) {
678 MidiEvent event = track.get(j);
679 System.out.printf(" tick %8d, %s%n",event.getTick(),messageInfotoString(event.getMessage()));
680 } // for
681 } // for
682 }
683
684 /**
685 * Toistaa neljäsosanuotin neljännessä oktaavissa.
686 * @param note Nuotti (C-B)
687 * @throws InvalidMidiDataException
688 */
689 @Override
690 public void playSingleNote(final String note) {
691 playPreciseNote(note, 0.25, getDefOctave());
692 }
693
694 /**
695 * Toistaa nuotin neljännessä oktaavissa.
696 * @param note Nuotti
697 * @param octave Oktaavi
698 * @throws InvalidMidiDataException
699 */
700 @Override
701 public void playSingleNote(final String note, final double length) {
702 playPreciseNote(note, length, getDefOctave());
703 }
704
705 /**
706 * Toistaa minkä tahansa kestoisen nuotin tietyssä oktaavissa.
707 * @param note Nuotti
708 * @param length Kesto
709 * @param octave Oktaavi
710 * @throws InvalidMidiDataException
711 */
712 @Override
713 public void playSingleNote(String note, double length, int octave){
714 playPreciseNote(note, length, octave);
715 }
716
717 /**
718 * Toistaa minkä tahansa kestoisen nuotin tietyssä oktaavissa.
719 * @param note Nuotti
720 * @param length Kesto
721 * @param octave Oktaavi
722 * @param velocity voimakkuus
723 * @throws InvalidMidiDataException
724 */
725 @Override
726 public void playSingleNote(String note, double length, int octave, int velocity){
727 playPreciseNote(note,length, octave, this.velocity);
728 }
729
730
731 /**
732 * Toistaa mielivaltaisen tauon.
733 * @param barLength tauon pituus ms
734 */
735 protected void rest(final int barLength) {
736 // Ei negatiivisia taukoja
737 if (barLength < 0)
738 return;
739 try {
740 int restLength = barLength;
741 message("Soitin nuotin " + Note.message(Note.DELAY, barLength, Note.DELAY));
742 Thread.sleep(restLength);
743 } catch (InterruptedException e) {
744
745 }
746 }
747
748
749 /**
750 * Valitsee kanavan. Kanavia on MIDI-spesifikaatiossa 16.
751 * @param channel Kanava väliltä 1-16.
752 */
753 public void selectChannel(final int channel) {
754 if (channel < channels.length && channel > 0) {
755 currentChannel = channel;
756 message("Valittiin kanava: " + channels[currentChannel]);
757 } else {
758 System.err.println("Virheellinen kanava, täytyy olla väliltä 1-16! Annoit: " + channel);
759 }
760 }
761
762 /**
763 * Vaihtaa instrumentin. Ks. <a href="http://www.midi.org/techspecs/gm1sound.php#instrument">spesifikaatio</a>.
764 * @param instrument Instrumentti väliltä 1-128.
765 */
766 public void setInstrument(final int instrument) {
767 checkSynth("setInstrument");
768 if ( instrument < 1 || instruments.length < instrument ) {
769 error(String.format("Instrumentti oltava väliltä 1-%d, annoit: %d ",instruments.length, instrument));
770 return;
771 }
772 message("Ladattiin soitin " + instruments[instrument - 1]);
773 synth.loadInstrument(instruments[instrument - 1]);
774 this.instrument = instrument;
775 channels[currentChannel].programChange(instrument - 1);
776 }
777
778 /**
779 * @return nykyinen instrumentti
780 */
781 public int getInstrument() {
782 return instrument;
783 }
784
785 /**
786 * Vaihtaa instrumentin. Ks. <a href="http://www.midi.org/techspecs/gm1sound.php#instrument">spesifikaatio</a>.
787 * @param instrument Instrumentti väliltä 1-128.
788 * @param def Oletusarvo mitä käytetään jos annettu indeksi on väärin
789 */
790 public void setInstrument(final int instrument, final int def) {
791 int i = instrument;
792 if ( i < 1 || instruments.length < i ) i = def;
793 setInstrument(i);
794 }
795
796 /**
797 * Asettaa tempon. BPM on iskuja minuutissa, oletus on 120. Tämä määrää syntetisaattorin tempon.
798 * @param bpm
799 */
800 @Override
801 public void setTempo(final int bpm) {
802 if (bpm > 0) {
803 beatLength = 60000 / bpm;
804 this.bpm = bpm;
805 }
806 }
807
808
809 /**
810 * @return nykyinen tempo bmp
811 */
812 public int getTempo() {
813 return bpm;
814 }
815
816
817 /**
818 * Asetetaan nopeus jolla "kosketinta lyödään"
819 * @param velocity lyöntinopeus, 64 on normaali
820 */
821 public void setVelocity(int velocity) {
822 this.velocity = velocity;
823 }
824
825
826 /**
827 * @return nopeus jolla kosketinta lyödään
828 */
829 public int getVelocity() {
830 return velocity;
831 }
832
833
834 /**
835 * Debug-viestit päälle/pois
836 */
837 public void setVerbose(final boolean set) {
838 this.verbose = set;
839 }
840
841 /**
842 * Toistaa yksittäisen nuotin instrumentilla. Standardissa MIDI 1.0-spesifikaatiossa on 86 eri nuottia,
843 * jotka menevät A'' - c'''' (A0 - C8). Toisin sanoen vajaa 8 oktaavia. Nuotit ovat
844 * A, Bb, B, C, C#, D, D#, E, F, F#, G, G#.
845 *
846 *
847 * Nuotin kestot ovat tahdin kestoja 1/16, 1/8, 1/4, 2/4, 3/4, 4/4 ... n desimaalimuodossa eli
848 * 0.0625, 0.125, 0.25, 0.5, 0.75, 1, ..., n. Tahdin pituus määräytyy tempon perusteella.
849 *
850 * @param note Nuotti (ks. yllä)
851 * @param octave Oktaavi (0-8).
852 * @param duration Nuotin kesto iskuina.
853 * @param velocity voimakkuus
854 */
855 private void playPreciseNote(final String note, final double length, final int octave, final int velocity) {
856 Note n = makeNote(note, length, octave, velocity);
857 // Kaikki ok. Soitetaan!
858 midiSoundNote(n.getNote(), n.getLength(), n.getVelocity());
859 }
860
861
862 /**
863 * Soittaa nuotin
864 * @param note Nuotti (ks. yllä)
865 * @param length Nuotin kesto iskuina.
866 * @param octave Oktaavi (0-8).
867 */
868 private void playPreciseNote(final String note, final double length, final int octave) {
869 playPreciseNote(note, length, octave, this.velocity);
870 }
871
872
873 /**
874 * Sulkee kanavalta nuotin.
875 * @param note nuotti
876 * @param velocity voimakkuus
877 */
878 protected void channelOff(final int note)
879 {
880 this.channels[currentChannel].noteOff(note);
881 }
882
883 /**
884 * Avaa kanavalta nuotin.
885 * @param note nuotti
886 * @param velocity voimakkuus
887 */
888 protected void channelOn(final int note, final int velocity)
889 {
890 this.channels[currentChannel].noteOn(note, velocity);
891 }
892
893 /**
894 * Tulostaa virheviestin. Tähän tarkoitukseen olisi sopinut kutsuvan metodin nimen onkiminen pinosta,
895 * mutta se on hidasta.
896 * @param origin Kutsuva metodi
897 */
898 protected void checkSynth(final String origin) {
899 if (synth == null) {
900 error(origin + ": Syntetisaattori käynnistämättä!");
901 }
902 }
903
904 /**
905 * Tulostaa virheviestin ja lopettaa ohjelman.
906 * @param msg Viesti
907 */
908 protected void error(final String msg) {
909 // Selvitä kutsuneen metodin nimi
910 final String callerName = Thread.currentThread().getStackTrace()[2].getMethodName();
911 System.err.println("Virhe: " + callerName + ": " + msg);
912 System.exit(0);
913 }
914
915 protected final int getBeatLength() {
916 return beatLength;
917 }
918
919 /**
920 * Tulostaa viestin mikäli viestintulostus on päällä. Oletus on, että järjestelmä on hiljaa.
921 * @param msg Viesti
922 */
923 protected void message(final String msg) {
924 if (verbose)
925 System.out.println(msg);
926 }
927
928 /**
929 * Tulostaa kaikkien instrumenttien nimet
930 */
931 public void printInstruments() {
932 int n=1;
933 for (Instrument i:instruments)
934 System.out.printf("%3d %s%n",n++,i.getName());
935 }
936
937
938 /**
939 * Etsii ensimmäisen instrumentin jonka nimi täsmää hakuehtoon
940 * @param regexp
941 * @return sen isntrumentin indkesi, joka täsmää hakuehtoon, -1 = ei löydy
942 */
943 public int getInstrumentIndex(String regexp) {
944 for (int i=0; i<instruments.length; i++) {
945 Instrument inst = instruments[i];
946 if ( inst.getName().matches(regexp)) return i+1;
947 }
948 return -1;
949
950 }
951
952 /**
953 * @return onko aputulosteet päällä vai ei
954 */
955 public boolean isVerbose() {
956 return verbose;
957 }
958
959 }
960