Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lDEVinux
GitHub Repository: lDEVinux/eaglercraft
Path: blob/main/src/lwjgl/java/paulscode/sound/MidiChannel.java
8644 views
1
package paulscode.sound;
2
3
import java.io.IOException;
4
import java.net.URL;
5
import java.util.LinkedList;
6
import java.util.ListIterator;
7
8
import javax.sound.midi.InvalidMidiDataException;
9
import javax.sound.midi.MetaEventListener;
10
import javax.sound.midi.MetaMessage;
11
import javax.sound.midi.MidiDevice;
12
import javax.sound.midi.MidiSystem;
13
import javax.sound.midi.MidiUnavailableException;
14
import javax.sound.midi.Receiver;
15
import javax.sound.midi.Sequence;
16
import javax.sound.midi.Sequencer;
17
import javax.sound.midi.ShortMessage;
18
import javax.sound.midi.Synthesizer;
19
20
/**
21
* The MidiChannel class provides an interface for playing MIDI files, using
22
* the JavaSound API. For more information about the JavaSound API, visit
23
* http://java.sun.com/products/java-media/sound/
24
*<br><br>
25
*<b><i> SoundSystem License:</b></i><br><b><br>
26
* You are free to use this library for any purpose, commercial or otherwise.
27
* You may modify this library or source code, and distribute it any way you
28
* like, provided the following conditions are met:
29
*<br>
30
* 1) You may not falsely claim to be the author of this library or any
31
* unmodified portion of it.
32
*<br>
33
* 2) You may not copyright this library or a modified version of it and then
34
* sue me for copyright infringement.
35
*<br>
36
* 3) If you modify the source code, you must clearly document the changes
37
* made before redistributing the modified source code, so other users know
38
* it is not the original code.
39
*<br>
40
* 4) You are not required to give me credit for this library in any derived
41
* work, but if you do, you must also mention my website:
42
* http://www.paulscode.com
43
*<br>
44
* 5) I the author will not be responsible for any damages (physical,
45
* financial, or otherwise) caused by the use if this library or any part
46
* of it.
47
*<br>
48
* 6) I the author do not guarantee, warrant, or make any representations,
49
* either expressed or implied, regarding the use of this library or any
50
* part of it.
51
* <br><br>
52
* Author: Paul Lamb
53
* <br>
54
* http://www.paulscode.com
55
* </b>
56
*/
57
public class MidiChannel implements MetaEventListener
58
{
59
/**
60
* Processes status messages, warnings, and error messages.
61
*/
62
private SoundSystemLogger logger;
63
64
/**
65
* Filename/URL to the file:
66
*/
67
private FilenameURL filenameURL;
68
69
/**
70
* Unique source identifier for this MIDI source.
71
*/
72
private String sourcename;
73
74
/**
75
* Global identifier for the MIDI "change volume" event.
76
*/
77
private static final int CHANGE_VOLUME = 7;
78
79
/**
80
* Global identifier for the MIDI "end of track" event.
81
*/
82
private static final int END_OF_TRACK = 47;
83
84
/**
85
* Used to return a current value from one of the synchronized
86
* boolean-interface methods.
87
*/
88
private static final boolean GET = false;
89
90
/**
91
* Used to set the value in one of the synchronized boolean-interface methods.
92
*/
93
private static final boolean SET = true;
94
95
/**
96
* Used when a parameter for one of the synchronized boolean-interface methods
97
* is not aplicable.
98
*/
99
private static final boolean XXX = false;
100
101
/**
102
* Runs the assigned sequence, passing information on to the synthesizer for
103
* playback.
104
*/
105
private Sequencer sequencer = null;
106
107
/**
108
* Converts MIDI events into audio.
109
*/
110
private Synthesizer synthesizer = null;
111
112
/**
113
* Converts MIDI events into audio if there is no default Synthesizer.
114
*/
115
private MidiDevice synthDevice = null;
116
117
/**
118
* Sequence of MIDI events defining sound.
119
*/
120
private Sequence sequence = null;
121
122
/**
123
* Should playback loop or play only once.
124
*/
125
private boolean toLoop = true;
126
127
/**
128
* Playback volume, float value (0.0f - 1.0f).
129
*/
130
private float gain = 1.0f;
131
132
/**
133
* True while sequencer is busy being set up.
134
*/
135
private boolean loading = true;
136
137
/**
138
* The list of MIDI files to play when the current sequence finishes.
139
*/
140
private LinkedList<FilenameURL> sequenceQueue = null;
141
142
/**
143
* Ensures that only one thread accesses the sequenceQueue at a time.
144
*/
145
private final Object sequenceQueueLock = new Object();
146
147
/**
148
* Specifies the gain factor used for the fade-out effect, or -1 when
149
* playback is not currently fading out.
150
*/
151
protected float fadeOutGain = -1.0f;
152
153
/**
154
* Specifies the gain factor used for the fade-in effect, or 1 when
155
* playback is not currently fading in.
156
*/
157
protected float fadeInGain = 1.0f;
158
159
/**
160
* Specifies the number of miliseconds it should take to fade out.
161
*/
162
protected long fadeOutMilis = 0;
163
164
/**
165
* Specifies the number of miliseconds it should take to fade in.
166
*/
167
protected long fadeInMilis = 0;
168
169
/**
170
* System time in miliseconds when the last fade in/out volume check occurred.
171
*/
172
protected long lastFadeCheck = 0;
173
174
/**
175
* Used for fading in and out effects.
176
*/
177
private FadeThread fadeThread = null;
178
179
/**
180
* Constructor: Defines the basic source information.
181
* @param toLoop Should playback loop or play only once?
182
* @param sourcename Unique identifier for this source.
183
* @param filename Name of the MIDI file to play.
184
*/
185
public MidiChannel( boolean toLoop, String sourcename, String filename )
186
{
187
// let others know we are busy loading:
188
loading( SET, true );
189
190
// grab a handle to the message logger:
191
logger = SoundSystemConfig.getLogger();
192
193
// save information about the source:
194
filenameURL( SET, new FilenameURL( filename ) );
195
sourcename( SET, sourcename );
196
setLooping( toLoop );
197
198
// initialize the MIDI channel:
199
init();
200
201
// finished loading:
202
loading( SET, false );
203
}
204
205
/**
206
* Constructor: Defines the basic source information. The fourth parameter,
207
* 'identifier' should look like a filename, and it must have the correct
208
* extension (.mid or .midi).
209
* @param toLoop Should playback loop or play only once?
210
* @param sourcename Unique identifier for this source.
211
* @param midiFile URL to the MIDI file to play.
212
* @param identifier Filename/identifier for the MIDI file.
213
*/
214
public MidiChannel( boolean toLoop, String sourcename, URL midiFile,
215
String identifier )
216
{
217
// let others know we are busy loading
218
loading( SET, true );
219
220
// grab a handle to the message logger:
221
logger = SoundSystemConfig.getLogger();
222
223
// save information about the source:
224
filenameURL( SET, new FilenameURL( midiFile, identifier ) );
225
sourcename( SET, sourcename );
226
setLooping( toLoop );
227
228
// initialize the MIDI channel:
229
init();
230
231
// finished loading:
232
loading( SET, false );
233
}
234
235
/**
236
* Constructor: Defines the basic source information.
237
* @param toLoop Should playback loop or play only once?
238
* @param sourcename Unique identifier for this source.
239
* @param midiFilenameURL Filename/URL to the MIDI file to play.
240
*/
241
public MidiChannel( boolean toLoop, String sourcename,
242
FilenameURL midiFilenameURL )
243
{
244
// let others know we are busy loading
245
loading( SET, true );
246
247
// grab a handle to the message logger:
248
logger = SoundSystemConfig.getLogger();
249
250
// save information about the source:
251
filenameURL( SET, midiFilenameURL );
252
sourcename( SET, sourcename );
253
setLooping( toLoop );
254
255
// initialize the MIDI channel:
256
init();
257
258
// finished loading:
259
loading( SET, false );
260
}
261
262
/**
263
* Initializes the sequencer, loads the sequence, and sets up the synthesizer.
264
*/
265
private void init()
266
{
267
// Load a sequencer:
268
getSequencer();
269
270
// Load the sequence to play:
271
setSequence( filenameURL( GET, null).getURL() );
272
273
// Load a synthesizer to play the sequence on:
274
getSynthesizer();
275
276
// Ensure the initial volume is correct:
277
// (TODO: doesn't always work??)
278
resetGain();
279
}
280
281
/**
282
* Shuts the channel down and removes references to all instantiated objects.
283
*/
284
public void cleanup()
285
{
286
loading( SET, true );
287
setLooping( true );
288
289
if( sequencer != null )
290
{
291
try
292
{
293
sequencer.stop();
294
sequencer.close();
295
sequencer.removeMetaEventListener( this );
296
}
297
catch( Exception e )
298
{}
299
}
300
301
logger = null;
302
sequencer = null;
303
synthesizer = null;
304
sequence = null;
305
306
synchronized( sequenceQueueLock )
307
{
308
if( sequenceQueue != null )
309
sequenceQueue.clear();
310
sequenceQueue = null;
311
}
312
313
// End the fade effects thread if it exists:
314
if( fadeThread != null )
315
{
316
boolean killException = false;
317
try
318
{
319
fadeThread.kill(); // end the fade effects thread.
320
fadeThread.interrupt(); // wake the thread up so it can end.
321
}
322
catch( Exception e )
323
{
324
killException = true;
325
}
326
327
if( !killException )
328
{
329
// wait up to 5 seconds for fade effects thread to end:
330
for( int i = 0; i < 50; i++ )
331
{
332
if( !fadeThread.alive() )
333
break;
334
try{Thread.sleep( 100 );}catch(InterruptedException e){}
335
}
336
}
337
338
// Let user know if there was a problem ending the fade thread
339
if( killException || fadeThread.alive() )
340
{
341
errorMessage( "MIDI fade effects thread did not die!" );
342
message( "Ignoring errors... continuing clean-up." );
343
}
344
}
345
346
fadeThread = null;
347
348
loading( SET, false );
349
}
350
351
/**
352
* Queues up the next MIDI sequence to play when the previous sequence ends.
353
* @param filenameURL MIDI sequence to play next.
354
*/
355
public void queueSound( FilenameURL filenameURL )
356
{
357
if( filenameURL == null )
358
{
359
errorMessage( "Filename/URL not specified in method 'queueSound'" );
360
return;
361
}
362
363
synchronized( sequenceQueueLock )
364
{
365
if( sequenceQueue == null )
366
sequenceQueue = new LinkedList<FilenameURL>();
367
sequenceQueue.add( filenameURL );
368
}
369
}
370
371
/**
372
* Removes the first occurrence of the specified filename/identifier from the
373
* list of MIDI sequences to play when the previous sequence ends.
374
* @param filename Filename or identifier of a MIDI sequence to remove from the
375
* queue.
376
*/
377
public void dequeueSound( String filename )
378
{
379
if( filename == null || filename.equals( "" ) )
380
{
381
errorMessage( "Filename not specified in method 'dequeueSound'" );
382
return;
383
}
384
385
synchronized( sequenceQueueLock )
386
{
387
if( sequenceQueue != null )
388
{
389
ListIterator<FilenameURL> i = sequenceQueue.listIterator();
390
while( i.hasNext() )
391
{
392
if( i.next().getFilename().equals( filename ) )
393
{
394
i.remove();
395
break;
396
}
397
}
398
}
399
}
400
}
401
402
/**
403
* Fades out the volume of whatever sequence is currently playing, then
404
* begins playing the specified MIDI file at the previously assigned
405
* volume level. If the filenameURL parameter is null or empty, playback will
406
* simply fade out and stop. The miliseconds parameter must be non-negative or
407
* zero. This method will remove anything that is currently in the list of
408
* queued MIDI sequences that would have played next when current playback
409
* finished.
410
* @param filenameURL MIDI file to play next, or null for none.
411
* @param milis Number of miliseconds the fadeout should take.
412
*/
413
public void fadeOut( FilenameURL filenameURL, long milis )
414
{
415
if( milis < 0 )
416
{
417
errorMessage( "Miliseconds may not be negative in method " +
418
"'fadeOut'." );
419
return;
420
}
421
422
fadeOutMilis = milis;
423
fadeInMilis = 0;
424
fadeOutGain = 1.0f;
425
lastFadeCheck = System.currentTimeMillis();
426
427
synchronized( sequenceQueueLock )
428
{
429
if( sequenceQueue != null )
430
sequenceQueue.clear();
431
432
if( filenameURL != null )
433
{
434
if( sequenceQueue == null )
435
sequenceQueue = new LinkedList<FilenameURL>();
436
sequenceQueue.add( filenameURL );
437
}
438
}
439
if( fadeThread == null )
440
{
441
fadeThread = new FadeThread();
442
fadeThread.start();
443
}
444
fadeThread.interrupt();
445
}
446
447
/**
448
* Fades out the volume of whatever sequence is currently playing, then
449
* fades the volume back in playing the specified MIDI file. Final volume
450
* after fade-in completes will be equal to the previously assigned volume
451
* level. The filenameURL parameter may not be null or empty. The miliseconds
452
* parameters must be non-negative or zero. This method will remove anything
453
* that is currently in the list of queued MIDI sequences that would have
454
* played next when current playback finished.
455
* @param filenameURL MIDI file to play next, or null for none.
456
* @param milisOut Number of miliseconds the fadeout should take.
457
* @param milisIn Number of miliseconds the fadein should take.
458
*/
459
public void fadeOutIn( FilenameURL filenameURL, long milisOut,
460
long milisIn )
461
{
462
if( filenameURL == null )
463
{
464
errorMessage( "Filename/URL not specified in method 'fadeOutIn'." );
465
return;
466
}
467
if( milisOut < 0 || milisIn < 0 )
468
{
469
errorMessage( "Miliseconds may not be negative in method " +
470
"'fadeOutIn'." );
471
return;
472
}
473
474
fadeOutMilis = milisOut;
475
fadeInMilis = milisIn;
476
fadeOutGain = 1.0f;
477
lastFadeCheck = System.currentTimeMillis();
478
479
synchronized( sequenceQueueLock )
480
{
481
if( sequenceQueue == null )
482
sequenceQueue = new LinkedList<FilenameURL>();
483
sequenceQueue.clear();
484
sequenceQueue.add( filenameURL );
485
}
486
if( fadeThread == null )
487
{
488
fadeThread = new FadeThread();
489
fadeThread.start();
490
}
491
fadeThread.interrupt();
492
}
493
494
/**
495
* Resets this source's volume if it is fading out or in. Returns true if this
496
* source is currently in the process of fading out. When fade-out completes,
497
* this method transitions the source to the next sound in the sound sequence
498
* queue if there is one. This method has no effect on non-streaming sources.
499
* @return True if this source is in the process of fading out.
500
*/
501
private synchronized boolean checkFadeOut()
502
{
503
if( fadeOutGain == -1.0f && fadeInGain == 1.0f )
504
return false;
505
506
long currentTime = System.currentTimeMillis();
507
long milisPast = currentTime - lastFadeCheck;
508
lastFadeCheck = currentTime;
509
510
if( fadeOutGain >= 0.0f )
511
{
512
if( fadeOutMilis == 0 )
513
{
514
fadeOutGain = 0.0f;
515
fadeInGain = 0.0f;
516
if( !incrementSequence() )
517
stop();
518
rewind();
519
resetGain();
520
return false;
521
}
522
else
523
{
524
float fadeOutReduction = ((float)milisPast) / ((float)fadeOutMilis);
525
526
fadeOutGain -= fadeOutReduction;
527
if( fadeOutGain <= 0.0f )
528
{
529
fadeOutGain = -1.0f;
530
fadeInGain = 0.0f;
531
if( !incrementSequence() )
532
stop();
533
rewind();
534
resetGain();
535
return false;
536
}
537
}
538
resetGain();
539
return true;
540
}
541
542
if( fadeInGain < 1.0f )
543
{
544
fadeOutGain = -1.0f;
545
if( fadeInMilis == 0 )
546
{
547
fadeOutGain = -1.0f;
548
fadeInGain = 1.0f;
549
}
550
else
551
{
552
float fadeInIncrease = ((float)milisPast) / ((float)fadeInMilis);
553
fadeInGain += fadeInIncrease;
554
if( fadeInGain >= 1.0f )
555
{
556
fadeOutGain = -1.0f;
557
fadeInGain = 1.0f;
558
}
559
}
560
resetGain();
561
}
562
563
return false;
564
}
565
566
/**
567
* Removes the next sequence from the queue and assigns it to the sequencer.
568
* @return True if there was something in the queue.
569
*/
570
private boolean incrementSequence()
571
{
572
synchronized( sequenceQueueLock )
573
{
574
// Is there a queue, and if so, is there anything in it:
575
if( sequenceQueue != null && sequenceQueue.size() > 0 )
576
{
577
// grab the next filename/URL from the queue:
578
filenameURL( SET, sequenceQueue.remove( 0 ) );
579
580
// Let everyone know we are busy loading:
581
loading( SET, true );
582
583
// Check if we have a sequencer:
584
if( sequencer == null )
585
{
586
// nope, try and get one now:
587
getSequencer();
588
}
589
else
590
{
591
// We have a sequencer. Stop it now:
592
sequencer.stop();
593
// rewind to the beginning:
594
sequencer.setMicrosecondPosition( 0 );
595
// Stop listening for a moment:
596
sequencer.removeMetaEventListener( this );
597
// wait a bit for the sequencer to shut down and rewind:
598
try{ Thread.sleep( 100 ); }catch( InterruptedException e ){}
599
}
600
// We need to have a sequencer at this point:
601
if( sequencer == null )
602
{
603
errorMessage( "Unable to set the sequence in method " +
604
"'incrementSequence', because there wasn't " +
605
"a sequencer to use." );
606
607
// Finished loading:
608
loading( SET, false );
609
610
// failure:
611
return false;
612
}
613
// set the new sequence to be played:
614
setSequence( filenameURL( GET, null ).getURL() );
615
// start playing again:
616
sequencer.start();
617
// make sure we play at the correct volume:
618
// (TODO: This doesn't always work??)
619
resetGain();
620
// start listening for end of track event again:
621
sequencer.addMetaEventListener( this );
622
623
// Finished loading:
624
loading( SET, false );
625
626
// We successfully moved to the next sequence:
627
return true;
628
}
629
}
630
631
// Nothing left to load
632
return false;
633
}
634
635
/**
636
* Plays the MIDI file from the beginning, or from where it left off if it was
637
* paused.
638
*/
639
public void play()
640
{
641
if( !loading() )
642
{
643
// Make sure there is a sequencer:
644
if( sequencer == null )
645
return;
646
647
try
648
{
649
// start playing:
650
sequencer.start();
651
// event will be sent when end of track is reached:
652
sequencer.addMetaEventListener( this );
653
}
654
catch( Exception e )
655
{
656
errorMessage( "Exception in method 'play'" );
657
printStackTrace( e );
658
SoundSystemException sse = new SoundSystemException(
659
e.getMessage() );
660
SoundSystem.setException( sse );
661
}
662
}
663
}
664
665
/**
666
* Stops playback and rewinds to the beginning.
667
*/
668
public void stop()
669
{
670
if( !loading() )
671
{
672
// Make sure there is a sequencer:
673
if( sequencer == null )
674
return;
675
676
try
677
{
678
// stop playback:
679
sequencer.stop();
680
// rewind to the beginning:
681
sequencer.setMicrosecondPosition( 0 );
682
// No need to listen any more:
683
sequencer.removeMetaEventListener( this );
684
}
685
catch( Exception e )
686
{
687
errorMessage( "Exception in method 'stop'" );
688
printStackTrace( e );
689
SoundSystemException sse = new SoundSystemException(
690
e.getMessage() );
691
SoundSystem.setException( sse );
692
}
693
}
694
}
695
696
/**
697
* Temporarily stops playback without rewinding.
698
*/
699
public void pause()
700
{
701
if( !loading() )
702
{
703
// Make sure there is a sequencer:
704
if( sequencer == null )
705
return;
706
707
try
708
{
709
//stop playback. Will resume from this location next play.
710
sequencer.stop();
711
}
712
catch( Exception e )
713
{
714
errorMessage( "Exception in method 'pause'" );
715
printStackTrace( e );
716
SoundSystemException sse = new SoundSystemException(
717
e.getMessage() );
718
SoundSystem.setException( sse );
719
}
720
}
721
}
722
723
/**
724
* Returns playback to the beginning.
725
*/
726
public void rewind()
727
{
728
if( !loading() )
729
{
730
// Make sure there is a sequencer:
731
if( sequencer == null )
732
return;
733
734
try
735
{
736
// rewind to the beginning:
737
sequencer.setMicrosecondPosition( 0 );
738
}
739
catch( Exception e )
740
{
741
errorMessage( "Exception in method 'rewind'" );
742
printStackTrace( e );
743
SoundSystemException sse = new SoundSystemException(
744
e.getMessage() );
745
SoundSystem.setException( sse );
746
}
747
}
748
}
749
750
/**
751
* Changes the volume of MIDI playback.
752
* @param value Float value (0.0f - 1.0f).
753
*/
754
public void setVolume( float value )
755
{
756
gain = value;
757
resetGain();
758
}
759
760
/**
761
* Returns the current volume for the MIDI source.
762
* @return Float value (0.0f - 1.0f).
763
*/
764
public float getVolume()
765
{
766
return gain;
767
}
768
769
/**
770
* Changes the basic information about the MIDI source. This method removes
771
* any queued filenames/URLs from the list of MIDI sequences that would have
772
* played after the current sequence ended.
773
* @param toLoop Should playback loop or play only once?
774
* @param sourcename Unique identifier for this source.
775
* @param filename Name of the MIDI file to play.
776
*/
777
public void switchSource( boolean toLoop, String sourcename,
778
String filename )
779
{
780
// Let everyone know we are busy loading:
781
loading( SET, true );
782
783
// save information about the source:
784
filenameURL( SET, new FilenameURL( filename ) );
785
sourcename( SET, sourcename );
786
setLooping( toLoop );
787
788
reset();
789
790
// Finished loading:
791
loading( SET, false );
792
}
793
794
/**
795
* Changes the basic information about the MIDI source. This method removes
796
* any queued filenames/URLs from the list of MIDI sequences that would have
797
* played after the current sequence ended. The fourth parameter,
798
* 'identifier' should look like a filename, and it must have the correct
799
* extension (.mid or .midi).
800
* @param toLoop Should playback loop or play only once?
801
* @param sourcename Unique identifier for this source.
802
* @param midiFile URL to the MIDI file to play.
803
* @param identifier Filename/identifier for the MIDI file.
804
*/
805
public void switchSource( boolean toLoop, String sourcename, URL midiFile,
806
String identifier )
807
{
808
// Let everyone know we are busy loading:
809
loading( SET, true );
810
811
// save information about the source:
812
filenameURL( SET, new FilenameURL( midiFile, identifier ) );
813
sourcename( SET, sourcename );
814
setLooping( toLoop );
815
816
reset();
817
818
// Finished loading:
819
loading( SET, false );
820
}
821
822
/**
823
* Changes the basic information about the MIDI source. This method removes
824
* any queued filenames/URLs from the list of MIDI sequences that would have
825
* played after the current sequence ended.
826
* @param toLoop Should playback loop or play only once?
827
* @param sourcename Unique identifier for this source.
828
* @param filenameURL Filename/URL of the MIDI file to play.
829
*/
830
public void switchSource( boolean toLoop, String sourcename,
831
FilenameURL filenameURL )
832
{
833
// Let everyone know we are busy loading:
834
loading( SET, true );
835
836
// save information about the source:
837
filenameURL( SET, filenameURL );
838
sourcename( SET, sourcename );
839
setLooping( toLoop );
840
841
reset();
842
843
// Finished loading:
844
loading( SET, false );
845
}
846
847
/**
848
* Stops and rewinds the sequencer, and resets the sequence.
849
*/
850
private void reset()
851
{
852
synchronized( sequenceQueueLock )
853
{
854
if( sequenceQueue != null )
855
sequenceQueue.clear();
856
}
857
858
// Check if we have a sequencer:
859
if( sequencer == null )
860
{
861
// nope, try and get one now:
862
getSequencer();
863
}
864
else
865
{
866
// We have a sequencer. Stop it now:
867
sequencer.stop();
868
// rewind to the beginning:
869
sequencer.setMicrosecondPosition( 0 );
870
// Stop listening for a moment:
871
sequencer.removeMetaEventListener( this );
872
// wait a bit for the sequencer to shut down and rewind:
873
try{ Thread.sleep( 100 ); }catch( InterruptedException e ){}
874
}
875
// We need to have a sequencer at this point:
876
if( sequencer == null )
877
{
878
errorMessage( "Unable to set the sequence in method " +
879
"'reset', because there wasn't " +
880
"a sequencer to use." );
881
return;
882
}
883
884
// set the new sequence to be played:
885
setSequence( filenameURL( GET, null ).getURL() );
886
// start playing again:
887
sequencer.start();
888
// make sure we play at the correct volume:
889
// (TODO: This doesn't always work??)
890
resetGain();
891
// start listening for end of track event again:
892
sequencer.addMetaEventListener( this );
893
}
894
895
/**
896
* Sets the value of boolean 'toLoop'.
897
* @param value True or False.
898
*/
899
public void setLooping( boolean value )
900
{
901
toLoop( SET, value );
902
}
903
904
/**
905
* Returns the value of boolean 'toLoop'.
906
* @return True while looping.
907
*/
908
public boolean getLooping()
909
{
910
return toLoop( GET, XXX );
911
}
912
913
/**
914
* Sets or returns the value of boolean 'toLoop'.
915
* @param action GET or SET.
916
* @param value New value if action == SET, or XXX if action == GET.
917
* @return True while looping.
918
*/
919
private synchronized boolean toLoop( boolean action, boolean value )
920
{
921
if( action == SET )
922
toLoop = value;
923
return toLoop;
924
}
925
926
/**
927
* Check if a MIDI file is in the process of loading.
928
*/
929
public boolean loading()
930
{
931
return( loading( GET, XXX ) );
932
}
933
934
/**
935
* Sets or returns the value of boolean 'loading'.
936
* @param action GET or SET.
937
* @param value New value if action == SET, or XXX if action == GET.
938
* @return True while a MIDI file is in the process of loading.
939
*/
940
private synchronized boolean loading( boolean action, boolean value )
941
{
942
if( action == SET )
943
loading = value;
944
return loading;
945
}
946
947
/**
948
* Defines the unique identifier for this source
949
* @param value New source name.
950
*/
951
public void setSourcename( String value )
952
{
953
sourcename( SET, value );
954
}
955
956
/**
957
* Returns the unique identifier for this source.
958
* @return The source's name.
959
*/
960
public String getSourcename()
961
{
962
return sourcename( GET, null );
963
}
964
965
/**
966
* Sets or returns the value of String 'sourcename'.
967
* @param action GET or SET.
968
* @param value New value if action == SET, or null if action == GET.
969
* @return The source's name.
970
*/
971
private synchronized String sourcename( boolean action, String value )
972
{
973
if( action == SET )
974
sourcename = value;
975
return sourcename;
976
}
977
978
/**
979
* Defines which MIDI file to play.
980
* @param value Path to the MIDI file.
981
*/
982
public void setFilenameURL( FilenameURL value )
983
{
984
filenameURL( SET, value );
985
}
986
987
/**
988
* Returns the filename/identifier of the MIDI file being played.
989
* @return Filename of identifier of the MIDI file.
990
*/
991
public String getFilename()
992
{
993
return filenameURL( GET, null ).getFilename();
994
}
995
996
/**
997
* Returns the MIDI file being played.
998
* @return Filename/URL of the MIDI file.
999
*/
1000
public FilenameURL getFilenameURL()
1001
{
1002
return filenameURL( GET, null );
1003
}
1004
1005
/**
1006
* Sets or returns the value of filenameURL.
1007
* @param action GET or SET.
1008
* @param value New value if action == SET, or null if action == GET.
1009
* @return Path to the MIDI file.
1010
*/
1011
private synchronized FilenameURL filenameURL( boolean action,
1012
FilenameURL value )
1013
{
1014
if( action == SET )
1015
filenameURL = value;
1016
return filenameURL;
1017
}
1018
1019
/**
1020
* Called when MIDI events occur.
1021
* @param message Meta mssage describing the MIDI event.
1022
*/
1023
public void meta( MetaMessage message )
1024
{
1025
if( message.getType() == END_OF_TRACK )
1026
{
1027
// Generate an EOS event:
1028
SoundSystemConfig.notifyEOS( sourcename, sequenceQueue.size() );
1029
1030
// check if we should loop or not:
1031
if( toLoop )
1032
{
1033
// looping
1034
// Check if playback is in the process of fading out.
1035
if( !checkFadeOut() )
1036
{
1037
// Not fading out, progress to the next MIDI sequence if
1038
// any are queued.
1039
if( !incrementSequence() )
1040
{
1041
try
1042
{
1043
// Rewind to the beginning.
1044
sequencer.setMicrosecondPosition( 0 );
1045
sequencer.start();
1046
// Make sure playback volume is correct.
1047
resetGain();
1048
}
1049
catch( Exception e ){}
1050
}
1051
}
1052
else if( sequencer != null )
1053
{
1054
try
1055
{
1056
// Rewind to the beginning.
1057
sequencer.setMicrosecondPosition( 0 );
1058
sequencer.start();
1059
// Make sure playback volume is correct.
1060
resetGain();
1061
}
1062
catch( Exception e ){}
1063
}
1064
}
1065
else
1066
{
1067
//non-looping
1068
if( !checkFadeOut() )
1069
{
1070
if( !incrementSequence() )
1071
{
1072
try
1073
{
1074
// stop playback:
1075
sequencer.stop();
1076
// rewind to the beginning:
1077
sequencer.setMicrosecondPosition( 0 );
1078
// stop looping:
1079
sequencer.removeMetaEventListener( this );
1080
}
1081
catch( Exception e ){}
1082
}
1083
}
1084
else
1085
{
1086
try
1087
{
1088
// stop playback:
1089
sequencer.stop();
1090
// rewind to the beginning:
1091
sequencer.setMicrosecondPosition( 0 );
1092
// stop looping:
1093
sequencer.removeMetaEventListener( this );
1094
}
1095
catch( Exception e ){}
1096
}
1097
}
1098
}
1099
}
1100
1101
/**
1102
* Resets playback volume to the correct level.
1103
*/
1104
public void resetGain()
1105
{
1106
// make sure the value for gain is valid (between 0 and 1)
1107
if( gain < 0.0f )
1108
gain = 0.0f;
1109
if( gain > 1.0f )
1110
gain = 1.0f;
1111
1112
int midiVolume = (int) ( gain * SoundSystemConfig.getMasterGain()
1113
* (float) Math.abs( fadeOutGain ) * fadeInGain
1114
* 127.0f );
1115
if( synthesizer != null )
1116
{
1117
javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();
1118
for( int c = 0; channels != null && c < channels.length; c++ )
1119
{
1120
channels[c].controlChange( CHANGE_VOLUME, midiVolume );
1121
}
1122
}
1123
else if( synthDevice != null )
1124
{
1125
try
1126
{
1127
ShortMessage volumeMessage = new ShortMessage();
1128
for( int i = 0; i < 16; i++ )
1129
{
1130
volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, i,
1131
CHANGE_VOLUME, midiVolume );
1132
synthDevice.getReceiver().send( volumeMessage, -1 );
1133
}
1134
}
1135
catch( Exception e )
1136
{
1137
errorMessage( "Error resetting gain on MIDI device" );
1138
printStackTrace( e );
1139
}
1140
}
1141
else if( sequencer != null && sequencer instanceof Synthesizer )
1142
{
1143
synthesizer = (Synthesizer) sequencer;
1144
javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();
1145
for( int c = 0; channels != null && c < channels.length; c++ )
1146
{
1147
channels[c].controlChange( CHANGE_VOLUME, midiVolume );
1148
}
1149
}
1150
else
1151
{
1152
try
1153
{
1154
Receiver receiver = MidiSystem.getReceiver();
1155
ShortMessage volumeMessage= new ShortMessage();
1156
for( int c = 0; c < 16; c++ )
1157
{
1158
volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, c,
1159
CHANGE_VOLUME, midiVolume );
1160
receiver.send( volumeMessage, -1 );
1161
}
1162
}
1163
catch( Exception e )
1164
{
1165
errorMessage( "Error resetting gain on default receiver" );
1166
printStackTrace( e );
1167
}
1168
}
1169
}
1170
1171
/**
1172
* Attempts to load the default sequencer. If it fails, then other common
1173
* sequencers are tried. If none can be loaded, then variable 'sequencer'
1174
* remains null.
1175
*/
1176
private void getSequencer()
1177
{
1178
try
1179
{
1180
sequencer = MidiSystem.getSequencer();
1181
if( sequencer != null )
1182
{
1183
try
1184
{
1185
sequencer.getTransmitter();
1186
}
1187
catch( MidiUnavailableException mue )
1188
{
1189
message( "Unable to get a transmitter from the " +
1190
"default MIDI sequencer" );
1191
}
1192
sequencer.open();
1193
}
1194
}
1195
catch( MidiUnavailableException mue )
1196
{
1197
message( "Unable to open the default MIDI sequencer" );
1198
sequencer = null;
1199
}
1200
catch( Exception e )
1201
{
1202
if( e instanceof InterruptedException )
1203
{
1204
message( "Caught InterruptedException while attempting to " +
1205
"open the default MIDI sequencer. Trying again." );
1206
sequencer = null;
1207
}
1208
try
1209
{
1210
sequencer = MidiSystem.getSequencer();
1211
if( sequencer != null )
1212
{
1213
try
1214
{
1215
sequencer.getTransmitter();
1216
}
1217
catch( MidiUnavailableException mue )
1218
{
1219
message( "Unable to get a transmitter from the " +
1220
"default MIDI sequencer" );
1221
}
1222
sequencer.open();
1223
}
1224
}
1225
catch( MidiUnavailableException mue )
1226
{
1227
message( "Unable to open the default MIDI sequencer" );
1228
sequencer = null;
1229
}
1230
catch( Exception e2 )
1231
{
1232
message( "Unknown error opening the default MIDI sequencer" );
1233
sequencer = null;
1234
}
1235
}
1236
1237
if( sequencer == null )
1238
sequencer = openSequencer( "Real Time Sequencer" );
1239
if( sequencer == null )
1240
sequencer = openSequencer( "Java Sound Sequencer");
1241
if( sequencer == null )
1242
{
1243
errorMessage( "Failed to find an available MIDI sequencer" );
1244
return;
1245
}
1246
}
1247
1248
/**
1249
* Loads the MIDI sequence form the specified URL, and sets the sequence. If
1250
* variable 'sequencer' is null or an error occurs, then variable 'sequence'
1251
* remains null.
1252
* @param midiSource URL to a MIDI file.
1253
*/
1254
private void setSequence( URL midiSource )
1255
{
1256
if( sequencer == null )
1257
{
1258
errorMessage( "Unable to update the sequence in method " +
1259
"'setSequence', because variable 'sequencer' " +
1260
"is null" );
1261
return;
1262
}
1263
1264
if( midiSource == null )
1265
{
1266
errorMessage( "Unable to load Midi file in method 'setSequence'." );
1267
return;
1268
}
1269
1270
try
1271
{
1272
sequence = MidiSystem.getSequence( midiSource );
1273
}
1274
catch( IOException ioe )
1275
{
1276
errorMessage( "Input failed while reading from MIDI file in " +
1277
"method 'setSequence'." );
1278
printStackTrace( ioe );
1279
return;
1280
}
1281
catch( InvalidMidiDataException imde )
1282
{
1283
errorMessage( "Invalid MIDI data encountered, or not a MIDI " +
1284
"file in method 'setSequence' (1)." );
1285
printStackTrace( imde );
1286
return;
1287
}
1288
if( sequence == null )
1289
{
1290
errorMessage( "MidiSystem 'getSequence' method returned null " +
1291
"in method 'setSequence'." );
1292
}
1293
else
1294
{
1295
try
1296
{
1297
sequencer.setSequence( sequence );
1298
}
1299
catch( InvalidMidiDataException imde )
1300
{
1301
errorMessage( "Invalid MIDI data encountered, or not a MIDI " +
1302
"file in method 'setSequence' (2)." );
1303
printStackTrace( imde );
1304
return;
1305
}
1306
catch( Exception e )
1307
{
1308
errorMessage( "Problem setting sequence from MIDI file in " +
1309
"method 'setSequence'." );
1310
printStackTrace( e );
1311
return;
1312
}
1313
}
1314
}
1315
1316
/**
1317
* First attempts to load the specified "override MIDI synthesizer" if one was
1318
* defined. If none was defined or unable to use it, then attempts to load the
1319
* default synthesizer. If that fails, then other common synthesizers are
1320
* attempted. If none can be loaded, then MIDI is not possible on this system.
1321
*/
1322
private void getSynthesizer()
1323
{
1324
if( sequencer == null )
1325
{
1326
errorMessage( "Unable to load a Synthesizer in method " +
1327
"'getSynthesizer', because variable 'sequencer' " +
1328
"is null" );
1329
return;
1330
}
1331
1332
// Check if an alternate MIDI synthesizer was specified to use
1333
String overrideMIDISynthesizer =
1334
SoundSystemConfig.getOverrideMIDISynthesizer();
1335
if( overrideMIDISynthesizer != null
1336
&& !overrideMIDISynthesizer.equals( "" ) )
1337
{
1338
// Try and open the specified device:
1339
synthDevice = openMidiDevice( overrideMIDISynthesizer );
1340
// See if we got it:
1341
if( synthDevice != null )
1342
{
1343
// Got it, try and link it to the sequencer:
1344
try
1345
{
1346
sequencer.getTransmitter().setReceiver(
1347
synthDevice.getReceiver() );
1348
// Success!
1349
return;
1350
}
1351
catch( MidiUnavailableException mue )
1352
{
1353
// Problem linking the two, let the user know
1354
errorMessage( "Unable to link sequencer transmitter " +
1355
"with receiver for MIDI device '" +
1356
overrideMIDISynthesizer + "'" );
1357
}
1358
}
1359
}
1360
1361
// No alternate MIDI synthesizer was specified, or unable to use it.
1362
1363
// If the squencer were also a synthesizer, that would make things easy:
1364
if( sequencer instanceof Synthesizer )
1365
{
1366
synthesizer = (Synthesizer) sequencer;
1367
}
1368
else
1369
{
1370
// Try getting the default synthesizer first:
1371
try
1372
{
1373
synthesizer = MidiSystem.getSynthesizer();
1374
synthesizer.open();
1375
}
1376
catch( MidiUnavailableException mue )
1377
{
1378
message( "Unable to open the default synthesizer" );
1379
synthesizer = null;
1380
}
1381
1382
// See if we were sucessful:
1383
if( synthesizer == null )
1384
{
1385
// Try for the common MIDI synthesizers:
1386
synthDevice = openMidiDevice( "Java Sound Synthesizer" );
1387
if( synthDevice == null )
1388
synthDevice = openMidiDevice( "Microsoft GS Wavetable" );
1389
if( synthDevice == null )
1390
synthDevice = openMidiDevice( "Gervill" );
1391
if( synthDevice == null )
1392
{
1393
// Still nothing, MIDI is not going to work
1394
errorMessage( "Failed to find an available MIDI " +
1395
"synthesizer" );
1396
return;
1397
}
1398
}
1399
1400
// Are we using the default synthesizer or something else?
1401
if( synthesizer == null )
1402
{
1403
// Link the sequencer and synthesizer:
1404
try
1405
{
1406
sequencer.getTransmitter().setReceiver(
1407
synthDevice.getReceiver() );
1408
}
1409
catch( MidiUnavailableException mue )
1410
{
1411
errorMessage( "Unable to link sequencer transmitter " +
1412
"with MIDI device receiver" );
1413
}
1414
}
1415
else
1416
{
1417
// Bug-fix for multiple-receivers playing simultaneously
1418
if( synthesizer.getDefaultSoundbank() == null )
1419
{
1420
// Link the sequencer to the default receiver:
1421
try
1422
{
1423
sequencer.getTransmitter().setReceiver(
1424
MidiSystem.getReceiver() );
1425
}
1426
catch( MidiUnavailableException mue )
1427
{
1428
errorMessage( "Unable to link sequencer transmitter " +
1429
"with default receiver" );
1430
}
1431
}
1432
else
1433
{
1434
// Link the sequencer to the default synthesizer:
1435
try
1436
{
1437
sequencer.getTransmitter().setReceiver(
1438
synthesizer.getReceiver() );
1439
}
1440
catch( MidiUnavailableException mue )
1441
{
1442
errorMessage( "Unable to link sequencer transmitter " +
1443
"with synthesizer receiver" );
1444
}
1445
}
1446
// End bug-fix
1447
}
1448
}
1449
}
1450
1451
/**
1452
* Attempts to open the Sequencer with a name containing the specified string.
1453
* @param containsString Part or all of a Sequencer's name.
1454
* @return Handle to the Sequencer, or null if not found or error.
1455
*/
1456
private Sequencer openSequencer( String containsString )
1457
{
1458
Sequencer s = null;
1459
s = (Sequencer) openMidiDevice( containsString );
1460
if( s == null )
1461
return null;
1462
try
1463
{
1464
s.getTransmitter();
1465
}
1466
catch( MidiUnavailableException mue )
1467
{
1468
message( " Unable to get a transmitter from this sequencer" );
1469
s = null;
1470
return null;
1471
}
1472
1473
return s;
1474
}
1475
1476
/**
1477
* Attempts to open the MIDI device with a name containing the specified
1478
* string.
1479
* @param containsString Part or all of a MIDI device's name.
1480
* @return Handle to the MIDI device, or null if not found or error.
1481
*/
1482
private MidiDevice openMidiDevice( String containsString )
1483
{
1484
message( "Searching for MIDI device with name containing '" +
1485
containsString + "'" );
1486
MidiDevice device = null;
1487
MidiDevice.Info[] midiDevices = MidiSystem.getMidiDeviceInfo();
1488
for( int i = 0; i < midiDevices.length; i++ )
1489
{
1490
device = null;
1491
try
1492
{
1493
device = MidiSystem.getMidiDevice( midiDevices[i] );
1494
}
1495
catch( MidiUnavailableException e )
1496
{
1497
message( " Problem in method 'getMidiDevice': " +
1498
"MIDIUnavailableException was thrown" );
1499
device = null;
1500
}
1501
if( device != null && midiDevices[i].getName().contains(
1502
containsString ) )
1503
{
1504
message( " Found MIDI device named '" +
1505
midiDevices[i].getName() + "'" );
1506
if( device instanceof Synthesizer )
1507
message( " *this is a Synthesizer instance" );
1508
if( device instanceof Sequencer )
1509
message( " *this is a Sequencer instance" );
1510
try
1511
{
1512
device.open();
1513
}
1514
catch( MidiUnavailableException mue )
1515
{
1516
message( " Unable to open this MIDI device" );
1517
device = null;
1518
}
1519
return device;
1520
}
1521
}
1522
message( " MIDI device not found" );
1523
return null;
1524
}
1525
1526
/**
1527
* Prints a message.
1528
* @param message Message to print.
1529
*/
1530
protected void message( String message )
1531
{
1532
logger.message( message, 0 );
1533
}
1534
1535
/**
1536
* Prints an important message.
1537
* @param message Message to print.
1538
*/
1539
protected void importantMessage( String message )
1540
{
1541
logger.importantMessage( message, 0 );
1542
}
1543
1544
/**
1545
* Prints the specified message if error is true.
1546
* @param error True or False.
1547
* @param message Message to print if error is true.
1548
* @return True if error is true.
1549
*/
1550
protected boolean errorCheck( boolean error, String message )
1551
{
1552
return logger.errorCheck( error, "MidiChannel", message, 0 );
1553
}
1554
1555
/**
1556
* Prints an error message.
1557
* @param message Message to print.
1558
*/
1559
protected void errorMessage( String message )
1560
{
1561
logger.errorMessage( "MidiChannel", message, 0 );
1562
}
1563
1564
/**
1565
* Prints an exception's error message followed by the stack trace.
1566
* @param e Exception containing the information to print.
1567
*/
1568
protected void printStackTrace( Exception e )
1569
{
1570
logger.printStackTrace( e, 1 );
1571
}
1572
1573
/**
1574
* The FadeThread class handles sequence changing, timing, and volume change
1575
* messages in the background.
1576
*/
1577
private class FadeThread extends SimpleThread
1578
{
1579
@Override
1580
/**
1581
* Runs in the background, timing fade in and fade out, changing the sequence,
1582
* and issuing the appropriate volume change messages.
1583
*/
1584
public void run()
1585
{
1586
while( !dying() )
1587
{
1588
// if not currently fading in or out, put the thread to sleep
1589
if( fadeOutGain == -1.0f && fadeInGain == 1.0f )
1590
snooze( 3600000 );
1591
checkFadeOut();
1592
// only update every 50 miliseconds (no need to peg the cpu)
1593
snooze( 50 );
1594
}
1595
// Important!
1596
cleanup();
1597
}
1598
}
1599
1600
}
1601
1602
1603