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