Path: blob/main/src/lwjgl/java/paulscode/sound/MidiChannel.java
8644 views
package paulscode.sound;12import java.io.IOException;3import java.net.URL;4import java.util.LinkedList;5import java.util.ListIterator;67import javax.sound.midi.InvalidMidiDataException;8import javax.sound.midi.MetaEventListener;9import javax.sound.midi.MetaMessage;10import javax.sound.midi.MidiDevice;11import javax.sound.midi.MidiSystem;12import javax.sound.midi.MidiUnavailableException;13import javax.sound.midi.Receiver;14import javax.sound.midi.Sequence;15import javax.sound.midi.Sequencer;16import javax.sound.midi.ShortMessage;17import javax.sound.midi.Synthesizer;1819/**20* The MidiChannel class provides an interface for playing MIDI files, using21* the JavaSound API. For more information about the JavaSound API, visit22* http://java.sun.com/products/java-media/sound/23*<br><br>24*<b><i> SoundSystem License:</b></i><br><b><br>25* You are free to use this library for any purpose, commercial or otherwise.26* You may modify this library or source code, and distribute it any way you27* like, provided the following conditions are met:28*<br>29* 1) You may not falsely claim to be the author of this library or any30* unmodified portion of it.31*<br>32* 2) You may not copyright this library or a modified version of it and then33* sue me for copyright infringement.34*<br>35* 3) If you modify the source code, you must clearly document the changes36* made before redistributing the modified source code, so other users know37* it is not the original code.38*<br>39* 4) You are not required to give me credit for this library in any derived40* work, but if you do, you must also mention my website:41* http://www.paulscode.com42*<br>43* 5) I the author will not be responsible for any damages (physical,44* financial, or otherwise) caused by the use if this library or any part45* of it.46*<br>47* 6) I the author do not guarantee, warrant, or make any representations,48* either expressed or implied, regarding the use of this library or any49* part of it.50* <br><br>51* Author: Paul Lamb52* <br>53* http://www.paulscode.com54* </b>55*/56public class MidiChannel implements MetaEventListener57{58/**59* Processes status messages, warnings, and error messages.60*/61private SoundSystemLogger logger;6263/**64* Filename/URL to the file:65*/66private FilenameURL filenameURL;6768/**69* Unique source identifier for this MIDI source.70*/71private String sourcename;7273/**74* Global identifier for the MIDI "change volume" event.75*/76private static final int CHANGE_VOLUME = 7;7778/**79* Global identifier for the MIDI "end of track" event.80*/81private static final int END_OF_TRACK = 47;8283/**84* Used to return a current value from one of the synchronized85* boolean-interface methods.86*/87private static final boolean GET = false;8889/**90* Used to set the value in one of the synchronized boolean-interface methods.91*/92private static final boolean SET = true;9394/**95* Used when a parameter for one of the synchronized boolean-interface methods96* is not aplicable.97*/98private static final boolean XXX = false;99100/**101* Runs the assigned sequence, passing information on to the synthesizer for102* playback.103*/104private Sequencer sequencer = null;105106/**107* Converts MIDI events into audio.108*/109private Synthesizer synthesizer = null;110111/**112* Converts MIDI events into audio if there is no default Synthesizer.113*/114private MidiDevice synthDevice = null;115116/**117* Sequence of MIDI events defining sound.118*/119private Sequence sequence = null;120121/**122* Should playback loop or play only once.123*/124private boolean toLoop = true;125126/**127* Playback volume, float value (0.0f - 1.0f).128*/129private float gain = 1.0f;130131/**132* True while sequencer is busy being set up.133*/134private boolean loading = true;135136/**137* The list of MIDI files to play when the current sequence finishes.138*/139private LinkedList<FilenameURL> sequenceQueue = null;140141/**142* Ensures that only one thread accesses the sequenceQueue at a time.143*/144private final Object sequenceQueueLock = new Object();145146/**147* Specifies the gain factor used for the fade-out effect, or -1 when148* playback is not currently fading out.149*/150protected float fadeOutGain = -1.0f;151152/**153* Specifies the gain factor used for the fade-in effect, or 1 when154* playback is not currently fading in.155*/156protected float fadeInGain = 1.0f;157158/**159* Specifies the number of miliseconds it should take to fade out.160*/161protected long fadeOutMilis = 0;162163/**164* Specifies the number of miliseconds it should take to fade in.165*/166protected long fadeInMilis = 0;167168/**169* System time in miliseconds when the last fade in/out volume check occurred.170*/171protected long lastFadeCheck = 0;172173/**174* Used for fading in and out effects.175*/176private FadeThread fadeThread = null;177178/**179* Constructor: Defines the basic source information.180* @param toLoop Should playback loop or play only once?181* @param sourcename Unique identifier for this source.182* @param filename Name of the MIDI file to play.183*/184public MidiChannel( boolean toLoop, String sourcename, String filename )185{186// let others know we are busy loading:187loading( SET, true );188189// grab a handle to the message logger:190logger = SoundSystemConfig.getLogger();191192// save information about the source:193filenameURL( SET, new FilenameURL( filename ) );194sourcename( SET, sourcename );195setLooping( toLoop );196197// initialize the MIDI channel:198init();199200// finished loading:201loading( SET, false );202}203204/**205* Constructor: Defines the basic source information. The fourth parameter,206* 'identifier' should look like a filename, and it must have the correct207* extension (.mid or .midi).208* @param toLoop Should playback loop or play only once?209* @param sourcename Unique identifier for this source.210* @param midiFile URL to the MIDI file to play.211* @param identifier Filename/identifier for the MIDI file.212*/213public MidiChannel( boolean toLoop, String sourcename, URL midiFile,214String identifier )215{216// let others know we are busy loading217loading( SET, true );218219// grab a handle to the message logger:220logger = SoundSystemConfig.getLogger();221222// save information about the source:223filenameURL( SET, new FilenameURL( midiFile, identifier ) );224sourcename( SET, sourcename );225setLooping( toLoop );226227// initialize the MIDI channel:228init();229230// finished loading:231loading( SET, false );232}233234/**235* Constructor: Defines the basic source information.236* @param toLoop Should playback loop or play only once?237* @param sourcename Unique identifier for this source.238* @param midiFilenameURL Filename/URL to the MIDI file to play.239*/240public MidiChannel( boolean toLoop, String sourcename,241FilenameURL midiFilenameURL )242{243// let others know we are busy loading244loading( SET, true );245246// grab a handle to the message logger:247logger = SoundSystemConfig.getLogger();248249// save information about the source:250filenameURL( SET, midiFilenameURL );251sourcename( SET, sourcename );252setLooping( toLoop );253254// initialize the MIDI channel:255init();256257// finished loading:258loading( SET, false );259}260261/**262* Initializes the sequencer, loads the sequence, and sets up the synthesizer.263*/264private void init()265{266// Load a sequencer:267getSequencer();268269// Load the sequence to play:270setSequence( filenameURL( GET, null).getURL() );271272// Load a synthesizer to play the sequence on:273getSynthesizer();274275// Ensure the initial volume is correct:276// (TODO: doesn't always work??)277resetGain();278}279280/**281* Shuts the channel down and removes references to all instantiated objects.282*/283public void cleanup()284{285loading( SET, true );286setLooping( true );287288if( sequencer != null )289{290try291{292sequencer.stop();293sequencer.close();294sequencer.removeMetaEventListener( this );295}296catch( Exception e )297{}298}299300logger = null;301sequencer = null;302synthesizer = null;303sequence = null;304305synchronized( sequenceQueueLock )306{307if( sequenceQueue != null )308sequenceQueue.clear();309sequenceQueue = null;310}311312// End the fade effects thread if it exists:313if( fadeThread != null )314{315boolean killException = false;316try317{318fadeThread.kill(); // end the fade effects thread.319fadeThread.interrupt(); // wake the thread up so it can end.320}321catch( Exception e )322{323killException = true;324}325326if( !killException )327{328// wait up to 5 seconds for fade effects thread to end:329for( int i = 0; i < 50; i++ )330{331if( !fadeThread.alive() )332break;333try{Thread.sleep( 100 );}catch(InterruptedException e){}334}335}336337// Let user know if there was a problem ending the fade thread338if( killException || fadeThread.alive() )339{340errorMessage( "MIDI fade effects thread did not die!" );341message( "Ignoring errors... continuing clean-up." );342}343}344345fadeThread = null;346347loading( SET, false );348}349350/**351* Queues up the next MIDI sequence to play when the previous sequence ends.352* @param filenameURL MIDI sequence to play next.353*/354public void queueSound( FilenameURL filenameURL )355{356if( filenameURL == null )357{358errorMessage( "Filename/URL not specified in method 'queueSound'" );359return;360}361362synchronized( sequenceQueueLock )363{364if( sequenceQueue == null )365sequenceQueue = new LinkedList<FilenameURL>();366sequenceQueue.add( filenameURL );367}368}369370/**371* Removes the first occurrence of the specified filename/identifier from the372* list of MIDI sequences to play when the previous sequence ends.373* @param filename Filename or identifier of a MIDI sequence to remove from the374* queue.375*/376public void dequeueSound( String filename )377{378if( filename == null || filename.equals( "" ) )379{380errorMessage( "Filename not specified in method 'dequeueSound'" );381return;382}383384synchronized( sequenceQueueLock )385{386if( sequenceQueue != null )387{388ListIterator<FilenameURL> i = sequenceQueue.listIterator();389while( i.hasNext() )390{391if( i.next().getFilename().equals( filename ) )392{393i.remove();394break;395}396}397}398}399}400401/**402* Fades out the volume of whatever sequence is currently playing, then403* begins playing the specified MIDI file at the previously assigned404* volume level. If the filenameURL parameter is null or empty, playback will405* simply fade out and stop. The miliseconds parameter must be non-negative or406* zero. This method will remove anything that is currently in the list of407* queued MIDI sequences that would have played next when current playback408* finished.409* @param filenameURL MIDI file to play next, or null for none.410* @param milis Number of miliseconds the fadeout should take.411*/412public void fadeOut( FilenameURL filenameURL, long milis )413{414if( milis < 0 )415{416errorMessage( "Miliseconds may not be negative in method " +417"'fadeOut'." );418return;419}420421fadeOutMilis = milis;422fadeInMilis = 0;423fadeOutGain = 1.0f;424lastFadeCheck = System.currentTimeMillis();425426synchronized( sequenceQueueLock )427{428if( sequenceQueue != null )429sequenceQueue.clear();430431if( filenameURL != null )432{433if( sequenceQueue == null )434sequenceQueue = new LinkedList<FilenameURL>();435sequenceQueue.add( filenameURL );436}437}438if( fadeThread == null )439{440fadeThread = new FadeThread();441fadeThread.start();442}443fadeThread.interrupt();444}445446/**447* Fades out the volume of whatever sequence is currently playing, then448* fades the volume back in playing the specified MIDI file. Final volume449* after fade-in completes will be equal to the previously assigned volume450* level. The filenameURL parameter may not be null or empty. The miliseconds451* parameters must be non-negative or zero. This method will remove anything452* that is currently in the list of queued MIDI sequences that would have453* played next when current playback finished.454* @param filenameURL MIDI file to play next, or null for none.455* @param milisOut Number of miliseconds the fadeout should take.456* @param milisIn Number of miliseconds the fadein should take.457*/458public void fadeOutIn( FilenameURL filenameURL, long milisOut,459long milisIn )460{461if( filenameURL == null )462{463errorMessage( "Filename/URL not specified in method 'fadeOutIn'." );464return;465}466if( milisOut < 0 || milisIn < 0 )467{468errorMessage( "Miliseconds may not be negative in method " +469"'fadeOutIn'." );470return;471}472473fadeOutMilis = milisOut;474fadeInMilis = milisIn;475fadeOutGain = 1.0f;476lastFadeCheck = System.currentTimeMillis();477478synchronized( sequenceQueueLock )479{480if( sequenceQueue == null )481sequenceQueue = new LinkedList<FilenameURL>();482sequenceQueue.clear();483sequenceQueue.add( filenameURL );484}485if( fadeThread == null )486{487fadeThread = new FadeThread();488fadeThread.start();489}490fadeThread.interrupt();491}492493/**494* Resets this source's volume if it is fading out or in. Returns true if this495* source is currently in the process of fading out. When fade-out completes,496* this method transitions the source to the next sound in the sound sequence497* queue if there is one. This method has no effect on non-streaming sources.498* @return True if this source is in the process of fading out.499*/500private synchronized boolean checkFadeOut()501{502if( fadeOutGain == -1.0f && fadeInGain == 1.0f )503return false;504505long currentTime = System.currentTimeMillis();506long milisPast = currentTime - lastFadeCheck;507lastFadeCheck = currentTime;508509if( fadeOutGain >= 0.0f )510{511if( fadeOutMilis == 0 )512{513fadeOutGain = 0.0f;514fadeInGain = 0.0f;515if( !incrementSequence() )516stop();517rewind();518resetGain();519return false;520}521else522{523float fadeOutReduction = ((float)milisPast) / ((float)fadeOutMilis);524525fadeOutGain -= fadeOutReduction;526if( fadeOutGain <= 0.0f )527{528fadeOutGain = -1.0f;529fadeInGain = 0.0f;530if( !incrementSequence() )531stop();532rewind();533resetGain();534return false;535}536}537resetGain();538return true;539}540541if( fadeInGain < 1.0f )542{543fadeOutGain = -1.0f;544if( fadeInMilis == 0 )545{546fadeOutGain = -1.0f;547fadeInGain = 1.0f;548}549else550{551float fadeInIncrease = ((float)milisPast) / ((float)fadeInMilis);552fadeInGain += fadeInIncrease;553if( fadeInGain >= 1.0f )554{555fadeOutGain = -1.0f;556fadeInGain = 1.0f;557}558}559resetGain();560}561562return false;563}564565/**566* Removes the next sequence from the queue and assigns it to the sequencer.567* @return True if there was something in the queue.568*/569private boolean incrementSequence()570{571synchronized( sequenceQueueLock )572{573// Is there a queue, and if so, is there anything in it:574if( sequenceQueue != null && sequenceQueue.size() > 0 )575{576// grab the next filename/URL from the queue:577filenameURL( SET, sequenceQueue.remove( 0 ) );578579// Let everyone know we are busy loading:580loading( SET, true );581582// Check if we have a sequencer:583if( sequencer == null )584{585// nope, try and get one now:586getSequencer();587}588else589{590// We have a sequencer. Stop it now:591sequencer.stop();592// rewind to the beginning:593sequencer.setMicrosecondPosition( 0 );594// Stop listening for a moment:595sequencer.removeMetaEventListener( this );596// wait a bit for the sequencer to shut down and rewind:597try{ Thread.sleep( 100 ); }catch( InterruptedException e ){}598}599// We need to have a sequencer at this point:600if( sequencer == null )601{602errorMessage( "Unable to set the sequence in method " +603"'incrementSequence', because there wasn't " +604"a sequencer to use." );605606// Finished loading:607loading( SET, false );608609// failure:610return false;611}612// set the new sequence to be played:613setSequence( filenameURL( GET, null ).getURL() );614// start playing again:615sequencer.start();616// make sure we play at the correct volume:617// (TODO: This doesn't always work??)618resetGain();619// start listening for end of track event again:620sequencer.addMetaEventListener( this );621622// Finished loading:623loading( SET, false );624625// We successfully moved to the next sequence:626return true;627}628}629630// Nothing left to load631return false;632}633634/**635* Plays the MIDI file from the beginning, or from where it left off if it was636* paused.637*/638public void play()639{640if( !loading() )641{642// Make sure there is a sequencer:643if( sequencer == null )644return;645646try647{648// start playing:649sequencer.start();650// event will be sent when end of track is reached:651sequencer.addMetaEventListener( this );652}653catch( Exception e )654{655errorMessage( "Exception in method 'play'" );656printStackTrace( e );657SoundSystemException sse = new SoundSystemException(658e.getMessage() );659SoundSystem.setException( sse );660}661}662}663664/**665* Stops playback and rewinds to the beginning.666*/667public void stop()668{669if( !loading() )670{671// Make sure there is a sequencer:672if( sequencer == null )673return;674675try676{677// stop playback:678sequencer.stop();679// rewind to the beginning:680sequencer.setMicrosecondPosition( 0 );681// No need to listen any more:682sequencer.removeMetaEventListener( this );683}684catch( Exception e )685{686errorMessage( "Exception in method 'stop'" );687printStackTrace( e );688SoundSystemException sse = new SoundSystemException(689e.getMessage() );690SoundSystem.setException( sse );691}692}693}694695/**696* Temporarily stops playback without rewinding.697*/698public void pause()699{700if( !loading() )701{702// Make sure there is a sequencer:703if( sequencer == null )704return;705706try707{708//stop playback. Will resume from this location next play.709sequencer.stop();710}711catch( Exception e )712{713errorMessage( "Exception in method 'pause'" );714printStackTrace( e );715SoundSystemException sse = new SoundSystemException(716e.getMessage() );717SoundSystem.setException( sse );718}719}720}721722/**723* Returns playback to the beginning.724*/725public void rewind()726{727if( !loading() )728{729// Make sure there is a sequencer:730if( sequencer == null )731return;732733try734{735// rewind to the beginning:736sequencer.setMicrosecondPosition( 0 );737}738catch( Exception e )739{740errorMessage( "Exception in method 'rewind'" );741printStackTrace( e );742SoundSystemException sse = new SoundSystemException(743e.getMessage() );744SoundSystem.setException( sse );745}746}747}748749/**750* Changes the volume of MIDI playback.751* @param value Float value (0.0f - 1.0f).752*/753public void setVolume( float value )754{755gain = value;756resetGain();757}758759/**760* Returns the current volume for the MIDI source.761* @return Float value (0.0f - 1.0f).762*/763public float getVolume()764{765return gain;766}767768/**769* Changes the basic information about the MIDI source. This method removes770* any queued filenames/URLs from the list of MIDI sequences that would have771* played after the current sequence ended.772* @param toLoop Should playback loop or play only once?773* @param sourcename Unique identifier for this source.774* @param filename Name of the MIDI file to play.775*/776public void switchSource( boolean toLoop, String sourcename,777String filename )778{779// Let everyone know we are busy loading:780loading( SET, true );781782// save information about the source:783filenameURL( SET, new FilenameURL( filename ) );784sourcename( SET, sourcename );785setLooping( toLoop );786787reset();788789// Finished loading:790loading( SET, false );791}792793/**794* Changes the basic information about the MIDI source. This method removes795* any queued filenames/URLs from the list of MIDI sequences that would have796* played after the current sequence ended. The fourth parameter,797* 'identifier' should look like a filename, and it must have the correct798* extension (.mid or .midi).799* @param toLoop Should playback loop or play only once?800* @param sourcename Unique identifier for this source.801* @param midiFile URL to the MIDI file to play.802* @param identifier Filename/identifier for the MIDI file.803*/804public void switchSource( boolean toLoop, String sourcename, URL midiFile,805String identifier )806{807// Let everyone know we are busy loading:808loading( SET, true );809810// save information about the source:811filenameURL( SET, new FilenameURL( midiFile, identifier ) );812sourcename( SET, sourcename );813setLooping( toLoop );814815reset();816817// Finished loading:818loading( SET, false );819}820821/**822* Changes the basic information about the MIDI source. This method removes823* any queued filenames/URLs from the list of MIDI sequences that would have824* played after the current sequence ended.825* @param toLoop Should playback loop or play only once?826* @param sourcename Unique identifier for this source.827* @param filenameURL Filename/URL of the MIDI file to play.828*/829public void switchSource( boolean toLoop, String sourcename,830FilenameURL filenameURL )831{832// Let everyone know we are busy loading:833loading( SET, true );834835// save information about the source:836filenameURL( SET, filenameURL );837sourcename( SET, sourcename );838setLooping( toLoop );839840reset();841842// Finished loading:843loading( SET, false );844}845846/**847* Stops and rewinds the sequencer, and resets the sequence.848*/849private void reset()850{851synchronized( sequenceQueueLock )852{853if( sequenceQueue != null )854sequenceQueue.clear();855}856857// Check if we have a sequencer:858if( sequencer == null )859{860// nope, try and get one now:861getSequencer();862}863else864{865// We have a sequencer. Stop it now:866sequencer.stop();867// rewind to the beginning:868sequencer.setMicrosecondPosition( 0 );869// Stop listening for a moment:870sequencer.removeMetaEventListener( this );871// wait a bit for the sequencer to shut down and rewind:872try{ Thread.sleep( 100 ); }catch( InterruptedException e ){}873}874// We need to have a sequencer at this point:875if( sequencer == null )876{877errorMessage( "Unable to set the sequence in method " +878"'reset', because there wasn't " +879"a sequencer to use." );880return;881}882883// set the new sequence to be played:884setSequence( filenameURL( GET, null ).getURL() );885// start playing again:886sequencer.start();887// make sure we play at the correct volume:888// (TODO: This doesn't always work??)889resetGain();890// start listening for end of track event again:891sequencer.addMetaEventListener( this );892}893894/**895* Sets the value of boolean 'toLoop'.896* @param value True or False.897*/898public void setLooping( boolean value )899{900toLoop( SET, value );901}902903/**904* Returns the value of boolean 'toLoop'.905* @return True while looping.906*/907public boolean getLooping()908{909return toLoop( GET, XXX );910}911912/**913* Sets or returns the value of boolean 'toLoop'.914* @param action GET or SET.915* @param value New value if action == SET, or XXX if action == GET.916* @return True while looping.917*/918private synchronized boolean toLoop( boolean action, boolean value )919{920if( action == SET )921toLoop = value;922return toLoop;923}924925/**926* Check if a MIDI file is in the process of loading.927*/928public boolean loading()929{930return( loading( GET, XXX ) );931}932933/**934* Sets or returns the value of boolean 'loading'.935* @param action GET or SET.936* @param value New value if action == SET, or XXX if action == GET.937* @return True while a MIDI file is in the process of loading.938*/939private synchronized boolean loading( boolean action, boolean value )940{941if( action == SET )942loading = value;943return loading;944}945946/**947* Defines the unique identifier for this source948* @param value New source name.949*/950public void setSourcename( String value )951{952sourcename( SET, value );953}954955/**956* Returns the unique identifier for this source.957* @return The source's name.958*/959public String getSourcename()960{961return sourcename( GET, null );962}963964/**965* Sets or returns the value of String 'sourcename'.966* @param action GET or SET.967* @param value New value if action == SET, or null if action == GET.968* @return The source's name.969*/970private synchronized String sourcename( boolean action, String value )971{972if( action == SET )973sourcename = value;974return sourcename;975}976977/**978* Defines which MIDI file to play.979* @param value Path to the MIDI file.980*/981public void setFilenameURL( FilenameURL value )982{983filenameURL( SET, value );984}985986/**987* Returns the filename/identifier of the MIDI file being played.988* @return Filename of identifier of the MIDI file.989*/990public String getFilename()991{992return filenameURL( GET, null ).getFilename();993}994995/**996* Returns the MIDI file being played.997* @return Filename/URL of the MIDI file.998*/999public FilenameURL getFilenameURL()1000{1001return filenameURL( GET, null );1002}10031004/**1005* Sets or returns the value of filenameURL.1006* @param action GET or SET.1007* @param value New value if action == SET, or null if action == GET.1008* @return Path to the MIDI file.1009*/1010private synchronized FilenameURL filenameURL( boolean action,1011FilenameURL value )1012{1013if( action == SET )1014filenameURL = value;1015return filenameURL;1016}10171018/**1019* Called when MIDI events occur.1020* @param message Meta mssage describing the MIDI event.1021*/1022public void meta( MetaMessage message )1023{1024if( message.getType() == END_OF_TRACK )1025{1026// Generate an EOS event:1027SoundSystemConfig.notifyEOS( sourcename, sequenceQueue.size() );10281029// check if we should loop or not:1030if( toLoop )1031{1032// looping1033// Check if playback is in the process of fading out.1034if( !checkFadeOut() )1035{1036// Not fading out, progress to the next MIDI sequence if1037// any are queued.1038if( !incrementSequence() )1039{1040try1041{1042// Rewind to the beginning.1043sequencer.setMicrosecondPosition( 0 );1044sequencer.start();1045// Make sure playback volume is correct.1046resetGain();1047}1048catch( Exception e ){}1049}1050}1051else if( sequencer != null )1052{1053try1054{1055// Rewind to the beginning.1056sequencer.setMicrosecondPosition( 0 );1057sequencer.start();1058// Make sure playback volume is correct.1059resetGain();1060}1061catch( Exception e ){}1062}1063}1064else1065{1066//non-looping1067if( !checkFadeOut() )1068{1069if( !incrementSequence() )1070{1071try1072{1073// stop playback:1074sequencer.stop();1075// rewind to the beginning:1076sequencer.setMicrosecondPosition( 0 );1077// stop looping:1078sequencer.removeMetaEventListener( this );1079}1080catch( Exception e ){}1081}1082}1083else1084{1085try1086{1087// stop playback:1088sequencer.stop();1089// rewind to the beginning:1090sequencer.setMicrosecondPosition( 0 );1091// stop looping:1092sequencer.removeMetaEventListener( this );1093}1094catch( Exception e ){}1095}1096}1097}1098}10991100/**1101* Resets playback volume to the correct level.1102*/1103public void resetGain()1104{1105// make sure the value for gain is valid (between 0 and 1)1106if( gain < 0.0f )1107gain = 0.0f;1108if( gain > 1.0f )1109gain = 1.0f;11101111int midiVolume = (int) ( gain * SoundSystemConfig.getMasterGain()1112* (float) Math.abs( fadeOutGain ) * fadeInGain1113* 127.0f );1114if( synthesizer != null )1115{1116javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();1117for( int c = 0; channels != null && c < channels.length; c++ )1118{1119channels[c].controlChange( CHANGE_VOLUME, midiVolume );1120}1121}1122else if( synthDevice != null )1123{1124try1125{1126ShortMessage volumeMessage = new ShortMessage();1127for( int i = 0; i < 16; i++ )1128{1129volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, i,1130CHANGE_VOLUME, midiVolume );1131synthDevice.getReceiver().send( volumeMessage, -1 );1132}1133}1134catch( Exception e )1135{1136errorMessage( "Error resetting gain on MIDI device" );1137printStackTrace( e );1138}1139}1140else if( sequencer != null && sequencer instanceof Synthesizer )1141{1142synthesizer = (Synthesizer) sequencer;1143javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();1144for( int c = 0; channels != null && c < channels.length; c++ )1145{1146channels[c].controlChange( CHANGE_VOLUME, midiVolume );1147}1148}1149else1150{1151try1152{1153Receiver receiver = MidiSystem.getReceiver();1154ShortMessage volumeMessage= new ShortMessage();1155for( int c = 0; c < 16; c++ )1156{1157volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, c,1158CHANGE_VOLUME, midiVolume );1159receiver.send( volumeMessage, -1 );1160}1161}1162catch( Exception e )1163{1164errorMessage( "Error resetting gain on default receiver" );1165printStackTrace( e );1166}1167}1168}11691170/**1171* Attempts to load the default sequencer. If it fails, then other common1172* sequencers are tried. If none can be loaded, then variable 'sequencer'1173* remains null.1174*/1175private void getSequencer()1176{1177try1178{1179sequencer = MidiSystem.getSequencer();1180if( sequencer != null )1181{1182try1183{1184sequencer.getTransmitter();1185}1186catch( MidiUnavailableException mue )1187{1188message( "Unable to get a transmitter from the " +1189"default MIDI sequencer" );1190}1191sequencer.open();1192}1193}1194catch( MidiUnavailableException mue )1195{1196message( "Unable to open the default MIDI sequencer" );1197sequencer = null;1198}1199catch( Exception e )1200{1201if( e instanceof InterruptedException )1202{1203message( "Caught InterruptedException while attempting to " +1204"open the default MIDI sequencer. Trying again." );1205sequencer = null;1206}1207try1208{1209sequencer = MidiSystem.getSequencer();1210if( sequencer != null )1211{1212try1213{1214sequencer.getTransmitter();1215}1216catch( MidiUnavailableException mue )1217{1218message( "Unable to get a transmitter from the " +1219"default MIDI sequencer" );1220}1221sequencer.open();1222}1223}1224catch( MidiUnavailableException mue )1225{1226message( "Unable to open the default MIDI sequencer" );1227sequencer = null;1228}1229catch( Exception e2 )1230{1231message( "Unknown error opening the default MIDI sequencer" );1232sequencer = null;1233}1234}12351236if( sequencer == null )1237sequencer = openSequencer( "Real Time Sequencer" );1238if( sequencer == null )1239sequencer = openSequencer( "Java Sound Sequencer");1240if( sequencer == null )1241{1242errorMessage( "Failed to find an available MIDI sequencer" );1243return;1244}1245}12461247/**1248* Loads the MIDI sequence form the specified URL, and sets the sequence. If1249* variable 'sequencer' is null or an error occurs, then variable 'sequence'1250* remains null.1251* @param midiSource URL to a MIDI file.1252*/1253private void setSequence( URL midiSource )1254{1255if( sequencer == null )1256{1257errorMessage( "Unable to update the sequence in method " +1258"'setSequence', because variable 'sequencer' " +1259"is null" );1260return;1261}12621263if( midiSource == null )1264{1265errorMessage( "Unable to load Midi file in method 'setSequence'." );1266return;1267}12681269try1270{1271sequence = MidiSystem.getSequence( midiSource );1272}1273catch( IOException ioe )1274{1275errorMessage( "Input failed while reading from MIDI file in " +1276"method 'setSequence'." );1277printStackTrace( ioe );1278return;1279}1280catch( InvalidMidiDataException imde )1281{1282errorMessage( "Invalid MIDI data encountered, or not a MIDI " +1283"file in method 'setSequence' (1)." );1284printStackTrace( imde );1285return;1286}1287if( sequence == null )1288{1289errorMessage( "MidiSystem 'getSequence' method returned null " +1290"in method 'setSequence'." );1291}1292else1293{1294try1295{1296sequencer.setSequence( sequence );1297}1298catch( InvalidMidiDataException imde )1299{1300errorMessage( "Invalid MIDI data encountered, or not a MIDI " +1301"file in method 'setSequence' (2)." );1302printStackTrace( imde );1303return;1304}1305catch( Exception e )1306{1307errorMessage( "Problem setting sequence from MIDI file in " +1308"method 'setSequence'." );1309printStackTrace( e );1310return;1311}1312}1313}13141315/**1316* First attempts to load the specified "override MIDI synthesizer" if one was1317* defined. If none was defined or unable to use it, then attempts to load the1318* default synthesizer. If that fails, then other common synthesizers are1319* attempted. If none can be loaded, then MIDI is not possible on this system.1320*/1321private void getSynthesizer()1322{1323if( sequencer == null )1324{1325errorMessage( "Unable to load a Synthesizer in method " +1326"'getSynthesizer', because variable 'sequencer' " +1327"is null" );1328return;1329}13301331// Check if an alternate MIDI synthesizer was specified to use1332String overrideMIDISynthesizer =1333SoundSystemConfig.getOverrideMIDISynthesizer();1334if( overrideMIDISynthesizer != null1335&& !overrideMIDISynthesizer.equals( "" ) )1336{1337// Try and open the specified device:1338synthDevice = openMidiDevice( overrideMIDISynthesizer );1339// See if we got it:1340if( synthDevice != null )1341{1342// Got it, try and link it to the sequencer:1343try1344{1345sequencer.getTransmitter().setReceiver(1346synthDevice.getReceiver() );1347// Success!1348return;1349}1350catch( MidiUnavailableException mue )1351{1352// Problem linking the two, let the user know1353errorMessage( "Unable to link sequencer transmitter " +1354"with receiver for MIDI device '" +1355overrideMIDISynthesizer + "'" );1356}1357}1358}13591360// No alternate MIDI synthesizer was specified, or unable to use it.13611362// If the squencer were also a synthesizer, that would make things easy:1363if( sequencer instanceof Synthesizer )1364{1365synthesizer = (Synthesizer) sequencer;1366}1367else1368{1369// Try getting the default synthesizer first:1370try1371{1372synthesizer = MidiSystem.getSynthesizer();1373synthesizer.open();1374}1375catch( MidiUnavailableException mue )1376{1377message( "Unable to open the default synthesizer" );1378synthesizer = null;1379}13801381// See if we were sucessful:1382if( synthesizer == null )1383{1384// Try for the common MIDI synthesizers:1385synthDevice = openMidiDevice( "Java Sound Synthesizer" );1386if( synthDevice == null )1387synthDevice = openMidiDevice( "Microsoft GS Wavetable" );1388if( synthDevice == null )1389synthDevice = openMidiDevice( "Gervill" );1390if( synthDevice == null )1391{1392// Still nothing, MIDI is not going to work1393errorMessage( "Failed to find an available MIDI " +1394"synthesizer" );1395return;1396}1397}13981399// Are we using the default synthesizer or something else?1400if( synthesizer == null )1401{1402// Link the sequencer and synthesizer:1403try1404{1405sequencer.getTransmitter().setReceiver(1406synthDevice.getReceiver() );1407}1408catch( MidiUnavailableException mue )1409{1410errorMessage( "Unable to link sequencer transmitter " +1411"with MIDI device receiver" );1412}1413}1414else1415{1416// Bug-fix for multiple-receivers playing simultaneously1417if( synthesizer.getDefaultSoundbank() == null )1418{1419// Link the sequencer to the default receiver:1420try1421{1422sequencer.getTransmitter().setReceiver(1423MidiSystem.getReceiver() );1424}1425catch( MidiUnavailableException mue )1426{1427errorMessage( "Unable to link sequencer transmitter " +1428"with default receiver" );1429}1430}1431else1432{1433// Link the sequencer to the default synthesizer:1434try1435{1436sequencer.getTransmitter().setReceiver(1437synthesizer.getReceiver() );1438}1439catch( MidiUnavailableException mue )1440{1441errorMessage( "Unable to link sequencer transmitter " +1442"with synthesizer receiver" );1443}1444}1445// End bug-fix1446}1447}1448}14491450/**1451* Attempts to open the Sequencer with a name containing the specified string.1452* @param containsString Part or all of a Sequencer's name.1453* @return Handle to the Sequencer, or null if not found or error.1454*/1455private Sequencer openSequencer( String containsString )1456{1457Sequencer s = null;1458s = (Sequencer) openMidiDevice( containsString );1459if( s == null )1460return null;1461try1462{1463s.getTransmitter();1464}1465catch( MidiUnavailableException mue )1466{1467message( " Unable to get a transmitter from this sequencer" );1468s = null;1469return null;1470}14711472return s;1473}14741475/**1476* Attempts to open the MIDI device with a name containing the specified1477* string.1478* @param containsString Part or all of a MIDI device's name.1479* @return Handle to the MIDI device, or null if not found or error.1480*/1481private MidiDevice openMidiDevice( String containsString )1482{1483message( "Searching for MIDI device with name containing '" +1484containsString + "'" );1485MidiDevice device = null;1486MidiDevice.Info[] midiDevices = MidiSystem.getMidiDeviceInfo();1487for( int i = 0; i < midiDevices.length; i++ )1488{1489device = null;1490try1491{1492device = MidiSystem.getMidiDevice( midiDevices[i] );1493}1494catch( MidiUnavailableException e )1495{1496message( " Problem in method 'getMidiDevice': " +1497"MIDIUnavailableException was thrown" );1498device = null;1499}1500if( device != null && midiDevices[i].getName().contains(1501containsString ) )1502{1503message( " Found MIDI device named '" +1504midiDevices[i].getName() + "'" );1505if( device instanceof Synthesizer )1506message( " *this is a Synthesizer instance" );1507if( device instanceof Sequencer )1508message( " *this is a Sequencer instance" );1509try1510{1511device.open();1512}1513catch( MidiUnavailableException mue )1514{1515message( " Unable to open this MIDI device" );1516device = null;1517}1518return device;1519}1520}1521message( " MIDI device not found" );1522return null;1523}15241525/**1526* Prints a message.1527* @param message Message to print.1528*/1529protected void message( String message )1530{1531logger.message( message, 0 );1532}15331534/**1535* Prints an important message.1536* @param message Message to print.1537*/1538protected void importantMessage( String message )1539{1540logger.importantMessage( message, 0 );1541}15421543/**1544* Prints the specified message if error is true.1545* @param error True or False.1546* @param message Message to print if error is true.1547* @return True if error is true.1548*/1549protected boolean errorCheck( boolean error, String message )1550{1551return logger.errorCheck( error, "MidiChannel", message, 0 );1552}15531554/**1555* Prints an error message.1556* @param message Message to print.1557*/1558protected void errorMessage( String message )1559{1560logger.errorMessage( "MidiChannel", message, 0 );1561}15621563/**1564* Prints an exception's error message followed by the stack trace.1565* @param e Exception containing the information to print.1566*/1567protected void printStackTrace( Exception e )1568{1569logger.printStackTrace( e, 1 );1570}15711572/**1573* The FadeThread class handles sequence changing, timing, and volume change1574* messages in the background.1575*/1576private class FadeThread extends SimpleThread1577{1578@Override1579/**1580* Runs in the background, timing fade in and fade out, changing the sequence,1581* and issuing the appropriate volume change messages.1582*/1583public void run()1584{1585while( !dying() )1586{1587// if not currently fading in or out, put the thread to sleep1588if( fadeOutGain == -1.0f && fadeInGain == 1.0f )1589snooze( 3600000 );1590checkFadeOut();1591// only update every 50 miliseconds (no need to peg the cpu)1592snooze( 50 );1593}1594// Important!1595cleanup();1596}1597}15981599}1600160116021603