package paulscode.sound;

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;

import net.lax1dude.eaglercraft.EaglerAdapter;

/**
 * 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 = EaglerAdapter.steadyTimeMillis();

        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 = EaglerAdapter.steadyTimeMillis();

        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 = EaglerAdapter.steadyTimeMillis();
        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();
        }
    }
    
}

