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
33 public class MidiPlayer implements BasicMidiPlayer {
34
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
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
74 private int beatLength = 500;
76
79 private int velocity = DEFVELOCITY;
80
81
84 private boolean verbose = false;
85
86
89 private final String remoteSoundbankUrl = "http://java.sun.com/products/java-media/sound/soundbank-min.gm.zip";
90
91
94 protected MidiChannel[] channels;
95
96
99 protected int currentChannel = 0;
100
101
104 protected Instrument[] instruments;
105
106
107
110 private int instrument = 1;
111
112
113
116 private int bpm = 120;
117
118
119
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
135 protected Synthesizer synth;
136
137
140 public MidiPlayer() {
141 this.init();
142 }
143
144
149 public MidiPlayer(final boolean init) {
150 if (init)
151 this.init();
152 }
153
154
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 String note = m.group(2);
233 String pitchMove = (m.group(3) != null) ? m.group(3) : "";
235 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
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 if (s == null) {
274 synth.close();
275 downloadSoundbank(remoteSoundbankUrl);
276 synth.open();
277 s = synth.getDefaultSoundbank();
278 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]); channels = synth.getChannels();
293 selectChannel(1);
294 }
295
296
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 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 String response = "HTTP " + source.getResponseCode() + " " + source.getResponseMessage();
315 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]; int fileSize = source.getContentLength(); 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 try {
339 ZipFile pkg = new ZipFile(new File(zipFileName));
340 Enumeration<? extends ZipEntry> entries = pkg.entries();
342 while (entries.hasMoreElements()) {
343 ZipEntry ze = (ZipEntry)entries.nextElement();
344 if (!ze.isDirectory() && ze.getName().endsWith(".gm")) {
346 System.out.println("Puretaan äänipankki " + ze.getName());
347 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 e.printStackTrace();
367 }
368 }
369
370
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
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 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
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
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
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
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
518 @Override
519 public void play(final String sequence){
520 play(sequence, 0.25, getDefOctave());
521 }
522
523
528 @Override
529 public void play(final String sequence, final double length) {
530 play(sequence, length, getDefOctave());
531 }
532
533
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
579 public void play(final List<Note> notes) {
580 for (Note n : notes) {
581 if (n.getNote() < 0 ) {
583 rest(n.getLength());
584 continue;
585 }
586 midiSoundNote(n.getNote(), n.getLength(), n.getVelocity());
587 }
588 }
589
590
591
595 public void playAsciiFile(final String filename) {
596 try {
597 final Scanner sc = new Scanner(new File(filename));
598 String notes = sc.next();
600 play(notes);
601 } catch (FileNotFoundException e) {
602 error("Tiedostoa " + filename + " ei läytynyt!");
603 }
604 }
605
606
613 public Sequencer playMidiFileAsync(final String filename) {
614 try {
615 final Sequence song = MidiSystem.getSequence(new File(filename));
617 Sequencer sequencer = MidiSystem.getSequencer();
619 sequencer.open();
620 sequencer.setSequence(song);
621
622 if ( isVerbose() ) printSequence(song);
623
624 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
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 System.out.println( " length: " + song.getTickLength() + " ticks" );
666 System.out.println( " duration: " + song.getMicrosecondLength() + " micro seconds" );
667 System.out.println( " division type: " + song.getDivisionType() );
669 System.out.println( " resolution type: " + song.getResolution() );
670
671 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 } } }
683
684
689 @Override
690 public void playSingleNote(final String note) {
691 playPreciseNote(note, 0.25, getDefOctave());
692 }
693
694
700 @Override
701 public void playSingleNote(final String note, final double length) {
702 playPreciseNote(note, length, getDefOctave());
703 }
704
705
712 @Override
713 public void playSingleNote(String note, double length, int octave){
714 playPreciseNote(note, length, octave);
715 }
716
717
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
735 protected void rest(final int barLength) {
736 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
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
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
781 public int getInstrument() {
782 return instrument;
783 }
784
785
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
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
812 public int getTempo() {
813 return bpm;
814 }
815
816
817
821 public void setVelocity(int velocity) {
822 this.velocity = velocity;
823 }
824
825
826
829 public int getVelocity() {
830 return velocity;
831 }
832
833
834
837 public void setVerbose(final boolean set) {
838 this.verbose = set;
839 }
840
841
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 midiSoundNote(n.getNote(), n.getLength(), n.getVelocity());
859 }
860
861
862
868 private void playPreciseNote(final String note, final double length, final int octave) {
869 playPreciseNote(note, length, octave, this.velocity);
870 }
871
872
873
878 protected void channelOff(final int note)
879 {
880 this.channels[currentChannel].noteOff(note);
881 }
882
883
888 protected void channelOn(final int note, final int velocity)
889 {
890 this.channels[currentChannel].noteOn(note, velocity);
891 }
892
893
898 protected void checkSynth(final String origin) {
899 if (synth == null) {
900 error(origin + ": Syntetisaattori käynnistämättä!");
901 }
902 }
903
904
908 protected void error(final String msg) {
909 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
923 protected void message(final String msg) {
924 if (verbose)
925 System.out.println(msg);
926 }
927
928
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
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
955 public boolean isVerbose() {
956 return verbose;
957 }
958
959 }
960