package sound.paulscode; import java.io.IOException; import java.net.URL; import java.util.LinkedList; import java.util.ListIterator; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MetaEventListener; import javax.sound.midi.MetaMessage; import javax.sound.midi.MidiDevice; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Receiver; import javax.sound.midi.Sequence; import javax.sound.midi.Sequencer; import javax.sound.midi.ShortMessage; import javax.sound.midi.Synthesizer; /** * The MidiChannel class provides an interface for playing MIDI files, using * the JavaSound API. For more information about the JavaSound API, visit * http://java.sun.com/products/java-media/sound/ *<br><br> *<b><i> SoundSystem License:</b></i><br><b><br> * You are free to use this library for any purpose, commercial or otherwise. * You may modify this library or source code, and distribute it any way you * like, provided the following conditions are met: *<br> * 1) You may not falsely claim to be the author of this library or any * unmodified portion of it. *<br> * 2) You may not copyright this library or a modified version of it and then * sue me for copyright infringement. *<br> * 3) If you modify the source code, you must clearly document the changes * made before redistributing the modified source code, so other users know * it is not the original code. *<br> * 4) You are not required to give me credit for this library in any derived * work, but if you do, you must also mention my website: * http://www.paulscode.com *<br> * 5) I the author will not be responsible for any damages (physical, * financial, or otherwise) caused by the use if this library or any part * of it. *<br> * 6) I the author do not guarantee, warrant, or make any representations, * either expressed or implied, regarding the use of this library or any * part of it. * <br><br> * Author: Paul Lamb * <br> * http://www.paulscode.com * </b> */ public class MidiChannel implements MetaEventListener { /** * Processes status messages, warnings, and error messages. */ private SoundSystemLogger logger; /** * Filename/URL to the file: */ private FilenameURL filenameURL; /** * Unique source identifier for this MIDI source. */ private String sourcename; /** * Global identifier for the MIDI "change volume" event. */ private static final int CHANGE_VOLUME = 7; /** * Global identifier for the MIDI "end of track" event. */ private static final int END_OF_TRACK = 47; /** * Used to return a current value from one of the synchronized * boolean-interface methods. */ private static final boolean GET = false; /** * Used to set the value in one of the synchronized boolean-interface methods. */ private static final boolean SET = true; /** * Used when a parameter for one of the synchronized boolean-interface methods * is not aplicable. */ private static final boolean XXX = false; /** * Runs the assigned sequence, passing information on to the synthesizer for * playback. */ private Sequencer sequencer = null; /** * Converts MIDI events into audio. */ private Synthesizer synthesizer = null; /** * Converts MIDI events into audio if there is no default Synthesizer. */ private MidiDevice synthDevice = null; /** * Sequence of MIDI events defining sound. */ private Sequence sequence = null; /** * Should playback loop or play only once. */ private boolean toLoop = true; /** * Playback volume, float value (0.0f - 1.0f). */ private float gain = 1.0f; /** * True while sequencer is busy being set up. */ private boolean loading = true; /** * The list of MIDI files to play when the current sequence finishes. */ private LinkedList<FilenameURL> sequenceQueue = null; /** * Ensures that only one thread accesses the sequenceQueue at a time. */ private final Object sequenceQueueLock = new Object(); /** * Specifies the gain factor used for the fade-out effect, or -1 when * playback is not currently fading out. */ protected float fadeOutGain = -1.0f; /** * Specifies the gain factor used for the fade-in effect, or 1 when * playback is not currently fading in. */ protected float fadeInGain = 1.0f; /** * Specifies the number of miliseconds it should take to fade out. */ protected long fadeOutMilis = 0; /** * Specifies the number of miliseconds it should take to fade in. */ protected long fadeInMilis = 0; /** * System time in miliseconds when the last fade in/out volume check occurred. */ protected long lastFadeCheck = 0; /** * Used for fading in and out effects. */ private FadeThread fadeThread = null; /** * Constructor: Defines the basic source information. * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param filename Name of the MIDI file to play. */ public MidiChannel( boolean toLoop, String sourcename, String filename ) { // let others know we are busy loading: loading( SET, true ); // grab a handle to the message logger: logger = SoundSystemConfig.getLogger(); // save information about the source: filenameURL( SET, new FilenameURL( filename ) ); sourcename( SET, sourcename ); setLooping( toLoop ); // initialize the MIDI channel: init(); // finished loading: loading( SET, false ); } /** * Constructor: Defines the basic source information. The fourth parameter, * 'identifier' should look like a filename, and it must have the correct * extension (.mid or .midi). * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param midiFile URL to the MIDI file to play. * @param identifier Filename/identifier for the MIDI file. */ public MidiChannel( boolean toLoop, String sourcename, URL midiFile, String identifier ) { // let others know we are busy loading loading( SET, true ); // grab a handle to the message logger: logger = SoundSystemConfig.getLogger(); // save information about the source: filenameURL( SET, new FilenameURL( midiFile, identifier ) ); sourcename( SET, sourcename ); setLooping( toLoop ); // initialize the MIDI channel: init(); // finished loading: loading( SET, false ); } /** * Constructor: Defines the basic source information. * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param midiFilenameURL Filename/URL to the MIDI file to play. */ public MidiChannel( boolean toLoop, String sourcename, FilenameURL midiFilenameURL ) { // let others know we are busy loading loading( SET, true ); // grab a handle to the message logger: logger = SoundSystemConfig.getLogger(); // save information about the source: filenameURL( SET, midiFilenameURL ); sourcename( SET, sourcename ); setLooping( toLoop ); // initialize the MIDI channel: init(); // finished loading: loading( SET, false ); } /** * Initializes the sequencer, loads the sequence, and sets up the synthesizer. */ private void init() { // Load a sequencer: getSequencer(); // Load the sequence to play: setSequence( filenameURL( GET, null).getURL() ); // Load a synthesizer to play the sequence on: getSynthesizer(); // Ensure the initial volume is correct: // (TODO: doesn't always work??) resetGain(); } /** * Shuts the channel down and removes references to all instantiated objects. */ public void cleanup() { loading( SET, true ); setLooping( true ); if( sequencer != null ) { try { sequencer.stop(); sequencer.close(); sequencer.removeMetaEventListener( this ); } catch( Exception e ) {} } logger = null; sequencer = null; synthesizer = null; sequence = null; synchronized( sequenceQueueLock ) { if( sequenceQueue != null ) sequenceQueue.clear(); sequenceQueue = null; } // End the fade effects thread if it exists: if( fadeThread != null ) { boolean killException = false; try { fadeThread.kill(); // end the fade effects thread. fadeThread.interrupt(); // wake the thread up so it can end. } catch( Exception e ) { killException = true; } if( !killException ) { // wait up to 5 seconds for fade effects thread to end: for( int i = 0; i < 50; i++ ) { if( !fadeThread.alive() ) break; try{Thread.sleep( 100 );}catch(InterruptedException e){} } } // Let user know if there was a problem ending the fade thread if( killException || fadeThread.alive() ) { errorMessage( "MIDI fade effects thread did not die!" ); message( "Ignoring errors... continuing clean-up." ); } } fadeThread = null; loading( SET, false ); } /** * Queues up the next MIDI sequence to play when the previous sequence ends. * @param filenameURL MIDI sequence to play next. */ public void queueSound( FilenameURL filenameURL ) { if( filenameURL == null ) { errorMessage( "Filename/URL not specified in method 'queueSound'" ); return; } synchronized( sequenceQueueLock ) { if( sequenceQueue == null ) sequenceQueue = new LinkedList<FilenameURL>(); sequenceQueue.add( filenameURL ); } } /** * Removes the first occurrence of the specified filename/identifier from the * list of MIDI sequences to play when the previous sequence ends. * @param filename Filename or identifier of a MIDI sequence to remove from the * queue. */ public void dequeueSound( String filename ) { if( filename == null || filename.equals( "" ) ) { errorMessage( "Filename not specified in method 'dequeueSound'" ); return; } synchronized( sequenceQueueLock ) { if( sequenceQueue != null ) { ListIterator<FilenameURL> i = sequenceQueue.listIterator(); while( i.hasNext() ) { if( i.next().getFilename().equals( filename ) ) { i.remove(); break; } } } } } /** * Fades out the volume of whatever sequence is currently playing, then * begins playing the specified MIDI file at the previously assigned * volume level. If the filenameURL parameter is null or empty, playback will * simply fade out and stop. The miliseconds parameter must be non-negative or * zero. This method will remove anything that is currently in the list of * queued MIDI sequences that would have played next when current playback * finished. * @param filenameURL MIDI file to play next, or null for none. * @param milis Number of miliseconds the fadeout should take. */ public void fadeOut( FilenameURL filenameURL, long milis ) { if( milis < 0 ) { errorMessage( "Miliseconds may not be negative in method " + "'fadeOut'." ); return; } fadeOutMilis = milis; fadeInMilis = 0; fadeOutGain = 1.0f; lastFadeCheck = System.currentTimeMillis(); synchronized( sequenceQueueLock ) { if( sequenceQueue != null ) sequenceQueue.clear(); if( filenameURL != null ) { if( sequenceQueue == null ) sequenceQueue = new LinkedList<FilenameURL>(); sequenceQueue.add( filenameURL ); } } if( fadeThread == null ) { fadeThread = new FadeThread(); fadeThread.start(); } fadeThread.interrupt(); } /** * Fades out the volume of whatever sequence is currently playing, then * fades the volume back in playing the specified MIDI file. Final volume * after fade-in completes will be equal to the previously assigned volume * level. The filenameURL parameter may not be null or empty. The miliseconds * parameters must be non-negative or zero. This method will remove anything * that is currently in the list of queued MIDI sequences that would have * played next when current playback finished. * @param filenameURL MIDI file to play next, or null for none. * @param milisOut Number of miliseconds the fadeout should take. * @param milisIn Number of miliseconds the fadein should take. */ public void fadeOutIn( FilenameURL filenameURL, long milisOut, long milisIn ) { if( filenameURL == null ) { errorMessage( "Filename/URL not specified in method 'fadeOutIn'." ); return; } if( milisOut < 0 || milisIn < 0 ) { errorMessage( "Miliseconds may not be negative in method " + "'fadeOutIn'." ); return; } fadeOutMilis = milisOut; fadeInMilis = milisIn; fadeOutGain = 1.0f; lastFadeCheck = System.currentTimeMillis(); synchronized( sequenceQueueLock ) { if( sequenceQueue == null ) sequenceQueue = new LinkedList<FilenameURL>(); sequenceQueue.clear(); sequenceQueue.add( filenameURL ); } if( fadeThread == null ) { fadeThread = new FadeThread(); fadeThread.start(); } fadeThread.interrupt(); } /** * Resets this source's volume if it is fading out or in. Returns true if this * source is currently in the process of fading out. When fade-out completes, * this method transitions the source to the next sound in the sound sequence * queue if there is one. This method has no effect on non-streaming sources. * @return True if this source is in the process of fading out. */ private synchronized boolean checkFadeOut() { if( fadeOutGain == -1.0f && fadeInGain == 1.0f ) return false; long currentTime = System.currentTimeMillis(); long milisPast = currentTime - lastFadeCheck; lastFadeCheck = currentTime; if( fadeOutGain >= 0.0f ) { if( fadeOutMilis == 0 ) { fadeOutGain = 0.0f; fadeInGain = 0.0f; if( !incrementSequence() ) stop(); rewind(); resetGain(); return false; } else { float fadeOutReduction = ((float)milisPast) / ((float)fadeOutMilis); fadeOutGain -= fadeOutReduction; if( fadeOutGain <= 0.0f ) { fadeOutGain = -1.0f; fadeInGain = 0.0f; if( !incrementSequence() ) stop(); rewind(); resetGain(); return false; } } resetGain(); return true; } if( fadeInGain < 1.0f ) { fadeOutGain = -1.0f; if( fadeInMilis == 0 ) { fadeOutGain = -1.0f; fadeInGain = 1.0f; } else { float fadeInIncrease = ((float)milisPast) / ((float)fadeInMilis); fadeInGain += fadeInIncrease; if( fadeInGain >= 1.0f ) { fadeOutGain = -1.0f; fadeInGain = 1.0f; } } resetGain(); } return false; } /** * Removes the next sequence from the queue and assigns it to the sequencer. * @return True if there was something in the queue. */ private boolean incrementSequence() { synchronized( sequenceQueueLock ) { // Is there a queue, and if so, is there anything in it: if( sequenceQueue != null && sequenceQueue.size() > 0 ) { // grab the next filename/URL from the queue: filenameURL( SET, sequenceQueue.remove( 0 ) ); // Let everyone know we are busy loading: loading( SET, true ); // Check if we have a sequencer: if( sequencer == null ) { // nope, try and get one now: getSequencer(); } else { // We have a sequencer. Stop it now: sequencer.stop(); // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); // Stop listening for a moment: sequencer.removeMetaEventListener( this ); // wait a bit for the sequencer to shut down and rewind: try{ Thread.sleep( 100 ); }catch( InterruptedException e ){} } // We need to have a sequencer at this point: if( sequencer == null ) { errorMessage( "Unable to set the sequence in method " + "'incrementSequence', because there wasn't " + "a sequencer to use." ); // Finished loading: loading( SET, false ); // failure: return false; } // set the new sequence to be played: setSequence( filenameURL( GET, null ).getURL() ); // start playing again: sequencer.start(); // make sure we play at the correct volume: // (TODO: This doesn't always work??) resetGain(); // start listening for end of track event again: sequencer.addMetaEventListener( this ); // Finished loading: loading( SET, false ); // We successfully moved to the next sequence: return true; } } // Nothing left to load return false; } /** * Plays the MIDI file from the beginning, or from where it left off if it was * paused. */ public void play() { if( !loading() ) { // Make sure there is a sequencer: if( sequencer == null ) return; try { // start playing: sequencer.start(); // event will be sent when end of track is reached: sequencer.addMetaEventListener( this ); } catch( Exception e ) { errorMessage( "Exception in method 'play'" ); printStackTrace( e ); SoundSystemException sse = new SoundSystemException( e.getMessage() ); SoundSystem.setException( sse ); } } } /** * Stops playback and rewinds to the beginning. */ public void stop() { if( !loading() ) { // Make sure there is a sequencer: if( sequencer == null ) return; try { // stop playback: sequencer.stop(); // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); // No need to listen any more: sequencer.removeMetaEventListener( this ); } catch( Exception e ) { errorMessage( "Exception in method 'stop'" ); printStackTrace( e ); SoundSystemException sse = new SoundSystemException( e.getMessage() ); SoundSystem.setException( sse ); } } } /** * Temporarily stops playback without rewinding. */ public void pause() { if( !loading() ) { // Make sure there is a sequencer: if( sequencer == null ) return; try { //stop playback. Will resume from this location next play. sequencer.stop(); } catch( Exception e ) { errorMessage( "Exception in method 'pause'" ); printStackTrace( e ); SoundSystemException sse = new SoundSystemException( e.getMessage() ); SoundSystem.setException( sse ); } } } /** * Returns playback to the beginning. */ public void rewind() { if( !loading() ) { // Make sure there is a sequencer: if( sequencer == null ) return; try { // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); } catch( Exception e ) { errorMessage( "Exception in method 'rewind'" ); printStackTrace( e ); SoundSystemException sse = new SoundSystemException( e.getMessage() ); SoundSystem.setException( sse ); } } } /** * Changes the volume of MIDI playback. * @param value Float value (0.0f - 1.0f). */ public void setVolume( float value ) { gain = value; resetGain(); } /** * Returns the current volume for the MIDI source. * @return Float value (0.0f - 1.0f). */ public float getVolume() { return gain; } /** * Changes the basic information about the MIDI source. This method removes * any queued filenames/URLs from the list of MIDI sequences that would have * played after the current sequence ended. * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param filename Name of the MIDI file to play. */ public void switchSource( boolean toLoop, String sourcename, String filename ) { // Let everyone know we are busy loading: loading( SET, true ); // save information about the source: filenameURL( SET, new FilenameURL( filename ) ); sourcename( SET, sourcename ); setLooping( toLoop ); reset(); // Finished loading: loading( SET, false ); } /** * Changes the basic information about the MIDI source. This method removes * any queued filenames/URLs from the list of MIDI sequences that would have * played after the current sequence ended. The fourth parameter, * 'identifier' should look like a filename, and it must have the correct * extension (.mid or .midi). * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param midiFile URL to the MIDI file to play. * @param identifier Filename/identifier for the MIDI file. */ public void switchSource( boolean toLoop, String sourcename, URL midiFile, String identifier ) { // Let everyone know we are busy loading: loading( SET, true ); // save information about the source: filenameURL( SET, new FilenameURL( midiFile, identifier ) ); sourcename( SET, sourcename ); setLooping( toLoop ); reset(); // Finished loading: loading( SET, false ); } /** * Changes the basic information about the MIDI source. This method removes * any queued filenames/URLs from the list of MIDI sequences that would have * played after the current sequence ended. * @param toLoop Should playback loop or play only once? * @param sourcename Unique identifier for this source. * @param filenameURL Filename/URL of the MIDI file to play. */ public void switchSource( boolean toLoop, String sourcename, FilenameURL filenameURL ) { // Let everyone know we are busy loading: loading( SET, true ); // save information about the source: filenameURL( SET, filenameURL ); sourcename( SET, sourcename ); setLooping( toLoop ); reset(); // Finished loading: loading( SET, false ); } /** * Stops and rewinds the sequencer, and resets the sequence. */ private void reset() { synchronized( sequenceQueueLock ) { if( sequenceQueue != null ) sequenceQueue.clear(); } // Check if we have a sequencer: if( sequencer == null ) { // nope, try and get one now: getSequencer(); } else { // We have a sequencer. Stop it now: sequencer.stop(); // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); // Stop listening for a moment: sequencer.removeMetaEventListener( this ); // wait a bit for the sequencer to shut down and rewind: try{ Thread.sleep( 100 ); }catch( InterruptedException e ){} } // We need to have a sequencer at this point: if( sequencer == null ) { errorMessage( "Unable to set the sequence in method " + "'reset', because there wasn't " + "a sequencer to use." ); return; } // set the new sequence to be played: setSequence( filenameURL( GET, null ).getURL() ); // start playing again: sequencer.start(); // make sure we play at the correct volume: // (TODO: This doesn't always work??) resetGain(); // start listening for end of track event again: sequencer.addMetaEventListener( this ); } /** * Sets the value of boolean 'toLoop'. * @param value True or False. */ public void setLooping( boolean value ) { toLoop( SET, value ); } /** * Returns the value of boolean 'toLoop'. * @return True while looping. */ public boolean getLooping() { return toLoop( GET, XXX ); } /** * Sets or returns the value of boolean 'toLoop'. * @param action GET or SET. * @param value New value if action == SET, or XXX if action == GET. * @return True while looping. */ private synchronized boolean toLoop( boolean action, boolean value ) { if( action == SET ) toLoop = value; return toLoop; } /** * Check if a MIDI file is in the process of loading. */ public boolean loading() { return( loading( GET, XXX ) ); } /** * Sets or returns the value of boolean 'loading'. * @param action GET or SET. * @param value New value if action == SET, or XXX if action == GET. * @return True while a MIDI file is in the process of loading. */ private synchronized boolean loading( boolean action, boolean value ) { if( action == SET ) loading = value; return loading; } /** * Defines the unique identifier for this source * @param value New source name. */ public void setSourcename( String value ) { sourcename( SET, value ); } /** * Returns the unique identifier for this source. * @return The source's name. */ public String getSourcename() { return sourcename( GET, null ); } /** * Sets or returns the value of String 'sourcename'. * @param action GET or SET. * @param value New value if action == SET, or null if action == GET. * @return The source's name. */ private synchronized String sourcename( boolean action, String value ) { if( action == SET ) sourcename = value; return sourcename; } /** * Defines which MIDI file to play. * @param value Path to the MIDI file. */ public void setFilenameURL( FilenameURL value ) { filenameURL( SET, value ); } /** * Returns the filename/identifier of the MIDI file being played. * @return Filename of identifier of the MIDI file. */ public String getFilename() { return filenameURL( GET, null ).getFilename(); } /** * Returns the MIDI file being played. * @return Filename/URL of the MIDI file. */ public FilenameURL getFilenameURL() { return filenameURL( GET, null ); } /** * Sets or returns the value of filenameURL. * @param action GET or SET. * @param value New value if action == SET, or null if action == GET. * @return Path to the MIDI file. */ private synchronized FilenameURL filenameURL( boolean action, FilenameURL value ) { if( action == SET ) filenameURL = value; return filenameURL; } /** * Called when MIDI events occur. * @param message Meta mssage describing the MIDI event. */ public void meta( MetaMessage message ) { if( message.getType() == END_OF_TRACK ) { // Generate an EOS event: SoundSystemConfig.notifyEOS( sourcename, sequenceQueue.size() ); // check if we should loop or not: if( toLoop ) { // looping // Check if playback is in the process of fading out. if( !checkFadeOut() ) { // Not fading out, progress to the next MIDI sequence if // any are queued. if( !incrementSequence() ) { try { // Rewind to the beginning. sequencer.setMicrosecondPosition( 0 ); sequencer.start(); // Make sure playback volume is correct. resetGain(); } catch( Exception e ){} } } else if( sequencer != null ) { try { // Rewind to the beginning. sequencer.setMicrosecondPosition( 0 ); sequencer.start(); // Make sure playback volume is correct. resetGain(); } catch( Exception e ){} } } else { //non-looping if( !checkFadeOut() ) { if( !incrementSequence() ) { try { // stop playback: sequencer.stop(); // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); // stop looping: sequencer.removeMetaEventListener( this ); } catch( Exception e ){} } } else { try { // stop playback: sequencer.stop(); // rewind to the beginning: sequencer.setMicrosecondPosition( 0 ); // stop looping: sequencer.removeMetaEventListener( this ); } catch( Exception e ){} } } } } /** * Resets playback volume to the correct level. */ public void resetGain() { // make sure the value for gain is valid (between 0 and 1) if( gain < 0.0f ) gain = 0.0f; if( gain > 1.0f ) gain = 1.0f; int midiVolume = (int) ( gain * SoundSystemConfig.getMasterGain() * (float) Math.abs( fadeOutGain ) * fadeInGain * 127.0f ); if( synthesizer != null ) { javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels(); for( int c = 0; channels != null && c < channels.length; c++ ) { channels[c].controlChange( CHANGE_VOLUME, midiVolume ); } } else if( synthDevice != null ) { try { ShortMessage volumeMessage = new ShortMessage(); for( int i = 0; i < 16; i++ ) { volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, i, CHANGE_VOLUME, midiVolume ); synthDevice.getReceiver().send( volumeMessage, -1 ); } } catch( Exception e ) { errorMessage( "Error resetting gain on MIDI device" ); printStackTrace( e ); } } else if( sequencer != null && sequencer instanceof Synthesizer ) { synthesizer = (Synthesizer) sequencer; javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels(); for( int c = 0; channels != null && c < channels.length; c++ ) { channels[c].controlChange( CHANGE_VOLUME, midiVolume ); } } else { try { Receiver receiver = MidiSystem.getReceiver(); ShortMessage volumeMessage= new ShortMessage(); for( int c = 0; c < 16; c++ ) { volumeMessage.setMessage( ShortMessage.CONTROL_CHANGE, c, CHANGE_VOLUME, midiVolume ); receiver.send( volumeMessage, -1 ); } } catch( Exception e ) { errorMessage( "Error resetting gain on default receiver" ); printStackTrace( e ); } } } /** * Attempts to load the default sequencer. If it fails, then other common * sequencers are tried. If none can be loaded, then variable 'sequencer' * remains null. */ private void getSequencer() { try { sequencer = MidiSystem.getSequencer(); if( sequencer != null ) { try { sequencer.getTransmitter(); } catch( MidiUnavailableException mue ) { message( "Unable to get a transmitter from the " + "default MIDI sequencer" ); } sequencer.open(); } } catch( MidiUnavailableException mue ) { message( "Unable to open the default MIDI sequencer" ); sequencer = null; } catch( Exception e ) { if( e instanceof InterruptedException ) { message( "Caught InterruptedException while attempting to " + "open the default MIDI sequencer. Trying again." ); sequencer = null; } try { sequencer = MidiSystem.getSequencer(); if( sequencer != null ) { try { sequencer.getTransmitter(); } catch( MidiUnavailableException mue ) { message( "Unable to get a transmitter from the " + "default MIDI sequencer" ); } sequencer.open(); } } catch( MidiUnavailableException mue ) { message( "Unable to open the default MIDI sequencer" ); sequencer = null; } catch( Exception e2 ) { message( "Unknown error opening the default MIDI sequencer" ); sequencer = null; } } if( sequencer == null ) sequencer = openSequencer( "Real Time Sequencer" ); if( sequencer == null ) sequencer = openSequencer( "Java Sound Sequencer"); if( sequencer == null ) { errorMessage( "Failed to find an available MIDI sequencer" ); return; } } /** * Loads the MIDI sequence form the specified URL, and sets the sequence. If * variable 'sequencer' is null or an error occurs, then variable 'sequence' * remains null. * @param midiSource URL to a MIDI file. */ private void setSequence( URL midiSource ) { if( sequencer == null ) { errorMessage( "Unable to update the sequence in method " + "'setSequence', because variable 'sequencer' " + "is null" ); return; } if( midiSource == null ) { errorMessage( "Unable to load Midi file in method 'setSequence'." ); return; } try { sequence = MidiSystem.getSequence( midiSource ); } catch( IOException ioe ) { errorMessage( "Input failed while reading from MIDI file in " + "method 'setSequence'." ); printStackTrace( ioe ); return; } catch( InvalidMidiDataException imde ) { errorMessage( "Invalid MIDI data encountered, or not a MIDI " + "file in method 'setSequence' (1)." ); printStackTrace( imde ); return; } if( sequence == null ) { errorMessage( "MidiSystem 'getSequence' method returned null " + "in method 'setSequence'." ); } else { try { sequencer.setSequence( sequence ); } catch( InvalidMidiDataException imde ) { errorMessage( "Invalid MIDI data encountered, or not a MIDI " + "file in method 'setSequence' (2)." ); printStackTrace( imde ); return; } catch( Exception e ) { errorMessage( "Problem setting sequence from MIDI file in " + "method 'setSequence'." ); printStackTrace( e ); return; } } } /** * First attempts to load the specified "override MIDI synthesizer" if one was * defined. If none was defined or unable to use it, then attempts to load the * default synthesizer. If that fails, then other common synthesizers are * attempted. If none can be loaded, then MIDI is not possible on this system. */ private void getSynthesizer() { if( sequencer == null ) { errorMessage( "Unable to load a Synthesizer in method " + "'getSynthesizer', because variable 'sequencer' " + "is null" ); return; } // Check if an alternate MIDI synthesizer was specified to use String overrideMIDISynthesizer = SoundSystemConfig.getOverrideMIDISynthesizer(); if( overrideMIDISynthesizer != null && !overrideMIDISynthesizer.equals( "" ) ) { // Try and open the specified device: synthDevice = openMidiDevice( overrideMIDISynthesizer ); // See if we got it: if( synthDevice != null ) { // Got it, try and link it to the sequencer: try { sequencer.getTransmitter().setReceiver( synthDevice.getReceiver() ); // Success! return; } catch( MidiUnavailableException mue ) { // Problem linking the two, let the user know errorMessage( "Unable to link sequencer transmitter " + "with receiver for MIDI device '" + overrideMIDISynthesizer + "'" ); } } } // No alternate MIDI synthesizer was specified, or unable to use it. // If the squencer were also a synthesizer, that would make things easy: if( sequencer instanceof Synthesizer ) { synthesizer = (Synthesizer) sequencer; } else { // Try getting the default synthesizer first: try { synthesizer = MidiSystem.getSynthesizer(); synthesizer.open(); } catch( MidiUnavailableException mue ) { message( "Unable to open the default synthesizer" ); synthesizer = null; } // See if we were sucessful: if( synthesizer == null ) { // Try for the common MIDI synthesizers: synthDevice = openMidiDevice( "Java Sound Synthesizer" ); if( synthDevice == null ) synthDevice = openMidiDevice( "Microsoft GS Wavetable" ); if( synthDevice == null ) synthDevice = openMidiDevice( "Gervill" ); if( synthDevice == null ) { // Still nothing, MIDI is not going to work errorMessage( "Failed to find an available MIDI " + "synthesizer" ); return; } } // Are we using the default synthesizer or something else? if( synthesizer == null ) { // Link the sequencer and synthesizer: try { sequencer.getTransmitter().setReceiver( synthDevice.getReceiver() ); } catch( MidiUnavailableException mue ) { errorMessage( "Unable to link sequencer transmitter " + "with MIDI device receiver" ); } } else { // Bug-fix for multiple-receivers playing simultaneously if( synthesizer.getDefaultSoundbank() == null ) { // Link the sequencer to the default receiver: try { sequencer.getTransmitter().setReceiver( MidiSystem.getReceiver() ); } catch( MidiUnavailableException mue ) { errorMessage( "Unable to link sequencer transmitter " + "with default receiver" ); } } else { // Link the sequencer to the default synthesizer: try { sequencer.getTransmitter().setReceiver( synthesizer.getReceiver() ); } catch( MidiUnavailableException mue ) { errorMessage( "Unable to link sequencer transmitter " + "with synthesizer receiver" ); } } // End bug-fix } } } /** * Attempts to open the Sequencer with a name containing the specified string. * @param containsString Part or all of a Sequencer's name. * @return Handle to the Sequencer, or null if not found or error. */ private Sequencer openSequencer( String containsString ) { Sequencer s = null; s = (Sequencer) openMidiDevice( containsString ); if( s == null ) return null; try { s.getTransmitter(); } catch( MidiUnavailableException mue ) { message( " Unable to get a transmitter from this sequencer" ); s = null; return null; } return s; } /** * Attempts to open the MIDI device with a name containing the specified * string. * @param containsString Part or all of a MIDI device's name. * @return Handle to the MIDI device, or null if not found or error. */ private MidiDevice openMidiDevice( String containsString ) { message( "Searching for MIDI device with name containing '" + containsString + "'" ); MidiDevice device = null; MidiDevice.Info[] midiDevices = MidiSystem.getMidiDeviceInfo(); for( int i = 0; i < midiDevices.length; i++ ) { device = null; try { device = MidiSystem.getMidiDevice( midiDevices[i] ); } catch( MidiUnavailableException e ) { message( " Problem in method 'getMidiDevice': " + "MIDIUnavailableException was thrown" ); device = null; } if( device != null && midiDevices[i].getName().contains( containsString ) ) { message( " Found MIDI device named '" + midiDevices[i].getName() + "'" ); if( device instanceof Synthesizer ) message( " *this is a Synthesizer instance" ); if( device instanceof Sequencer ) message( " *this is a Sequencer instance" ); try { device.open(); } catch( MidiUnavailableException mue ) { message( " Unable to open this MIDI device" ); device = null; } return device; } } message( " MIDI device not found" ); return null; } /** * Prints a message. * @param message Message to print. */ protected void message( String message ) { logger.message( message, 0 ); } /** * Prints an important message. * @param message Message to print. */ protected void importantMessage( String message ) { logger.importantMessage( message, 0 ); } /** * Prints the specified message if error is true. * @param error True or False. * @param message Message to print if error is true. * @return True if error is true. */ protected boolean errorCheck( boolean error, String message ) { return logger.errorCheck( error, "MidiChannel", message, 0 ); } /** * Prints an error message. * @param message Message to print. */ protected void errorMessage( String message ) { logger.errorMessage( "MidiChannel", message, 0 ); } /** * Prints an exception's error message followed by the stack trace. * @param e Exception containing the information to print. */ protected void printStackTrace( Exception e ) { logger.printStackTrace( e, 1 ); } /** * The FadeThread class handles sequence changing, timing, and volume change * messages in the background. */ private class FadeThread extends SimpleThread { @Override /** * Runs in the background, timing fade in and fade out, changing the sequence, * and issuing the appropriate volume change messages. */ public void run() { while( !dying() ) { // if not currently fading in or out, put the thread to sleep if( fadeOutGain == -1.0f && fadeInGain == 1.0f ) snooze( 3600000 ); checkFadeOut(); // only update every 50 miliseconds (no need to peg the cpu) snooze( 50 ); } // Important! cleanup(); } } }