package paulscode.sound;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.sound.sampled.AudioFormat;

/**
 * The Library class is the class from which all library types are extended.  
 * It provides generic methods for interfacing with the audio libraries 
 * supported by the SoundSystem.  Specific libraries should extend this class 
 * and override the necessary methods.  For consistant naming conventions, each 
 * sub-class should have the name prefix "Library".  
 * 
 * This class may also be used as the "No Sound Library" (i.e. silent mode) if
 * no other audio libraries are supported by the host machine, or to mute all
 * 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 Library
{
/**
 * Processes status messages, warnings, and error messages.
 */
    private SoundSystemLogger logger;
    
/**
 * Position and orientation of the listener.
 */
    protected ListenerData listener;

/**
 * Map containing sound file data for easy lookup by filename / identifier.
 */
    protected HashMap<String, SoundBuffer> bufferMap = null;

/**
 * Map containing all created sources for easy look-up by name.
 */
    protected HashMap<String, Source> sourceMap;  // (name, source data) pairs

/**
 * Interface through which MIDI files can be played.
 */
    private MidiChannel midiChannel;
    
/**
 * Array containing maximum number of non-streaming audio channels.
 */
    protected List<Channel> streamingChannels;
    
/**
 * Array containing maximum number of non-streaming audio channels.
 */
    protected List<Channel> normalChannels;
    
/**
 * Source name last played on each streaming channel.
 */
    private String[] streamingChannelSourceNames;
    
/**
 * Source name last played on each non-streaming channel.
 */
    private String[] normalChannelSourceNames;
    
/**
 * Increments through the steaming channel list as new sources are played.
 */
    private int nextStreamingChannel = 0;
    
/**
 * Increments through the non-steaming channel list as new sources are played.
 */
    private int nextNormalChannel = 0;

/**
 * Handles processing for all streaming sources.
 */
    protected StreamThread streamThread;
    
/**
 * Whether or not the library requires reversal of audio data byte order.
 */
    protected boolean reverseByteOrder = false;
    
/**
 * Constructor: Instantiates the source map and listener information.  NOTES: 
 * The 'super()' method should be at the top of constructors for all extended 
 * classes.  The varriable 'libraryType' should be given a new value in the 
 * constructors for all extended classes.
 */
    public Library() throws SoundSystemException
    {
        // grab a handle to the message logger:
        logger = SoundSystemConfig.getLogger();
        
        // instantiate the buffer map:
        bufferMap = new HashMap<String, SoundBuffer>();

        // instantiate the source map:
        sourceMap = new HashMap<String, Source>();
        
        listener = new ListenerData( 0.0f, 0.0f, 0.0f,  // position
                                     0.0f, 0.0f, -1.0f, // look-at direction
                                     0.0f, 1.0f, 0.0f,  // up direction
                                     0.0f );            // angle
        
        streamingChannels = new LinkedList<Channel>();
        normalChannels = new LinkedList<Channel>();
        streamingChannelSourceNames = new String[
                               SoundSystemConfig.getNumberStreamingChannels() ];
        normalChannelSourceNames = new String[
                                  SoundSystemConfig.getNumberNormalChannels() ];
        
        streamThread = new StreamThread();
        streamThread.start();
    }


/* ########################################################################## */
/*                        BEGIN OVERRIDE METHODS                              */
/*                                                                            */
/*         The following methods should be overrided as required              */
/* ########################################################################## */
    
/**
 * Stops all sources, shuts down sound library, and removes references to all 
 * instantiated objects.
 */
    public void cleanup()
    {
        streamThread.kill();
        streamThread.interrupt();
        
        // wait up to 5 seconds for stream thread to end:
        for( int i = 0; i < 50; i++ )
        {
            if( !streamThread.alive() )
                break;
            try
            {
                Thread.sleep(100);
            }
            catch(Exception e)
            {}
        }
        
        if( streamThread.alive() )
        {
            errorMessage( "Stream thread did not die!" );
            message( "Ignoring errors... continuing clean-up." );
        }
        
        if( midiChannel != null )
        {
            midiChannel.cleanup();
            midiChannel = null;
        }

        Channel channel = null;
        if( streamingChannels != null )
        {
            while( !streamingChannels.isEmpty() )
            {
                channel = streamingChannels.remove(0);
                channel.close();
                channel.cleanup();
                channel = null;
            }
            streamingChannels.clear();
            streamingChannels = null;
        }
        if( normalChannels != null )
        {
            while( !normalChannels.isEmpty() )
            {
                channel = normalChannels.remove(0);
                channel.close();
                channel.cleanup();
                channel = null;
            }
            normalChannels.clear();
            normalChannels = null;
        }
        
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source source;
        
        // loop through and cleanup all the sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
                source.cleanup();
        }
        sourceMap.clear();
        sourceMap = null;
        
        listener = null;
        streamThread = null;
    }
    
/**
 * Initializes the sound library.
 */
    public void init() throws SoundSystemException
    {
        Channel channel = null;
        
        // create the streaming channels:
        for( int x = 0; x < SoundSystemConfig.getNumberStreamingChannels(); x++ )
        {
            channel = createChannel( SoundSystemConfig.TYPE_STREAMING );
            if( channel == null )
                break;
            streamingChannels.add( channel );
        }
        // create the non-streaming channels:
        for( int x = 0; x < SoundSystemConfig.getNumberNormalChannels(); x++ )
        {
            channel = createChannel( SoundSystemConfig.TYPE_NORMAL );
            if( channel == null )
                break;
            normalChannels.add( channel );
        }
    }
    
/**
 * Checks if the no-sound library type is compatible.
 * @return True or false.
 */
    public static boolean libraryCompatible()
    {
        return true;  // the no-sound library is always compatible.
    }
    
/**
 * Creates a new channel of the specified type (normal or streaming).  Possible 
 * values for channel type can be found in the 
 * {@link paulscode.sound.SoundSystemConfig SoundSystemConfig} class.
 * @param type Type of channel.
 * @return The new channel.
 */
    protected Channel createChannel( int type )
    {
        return new Channel( type );
    }
    
/**
 * Pre-loads a sound into memory.  
 * @param filenameURL Filename/URL of the sound file to load.
 * @return True if the sound loaded properly.
 */
    public boolean loadSound( FilenameURL filenameURL )
    {
        return true;
    }

/**
 * Saves the specified sample data, under the specified identifier.  This
 * identifier can be later used in place of 'filename' parameters to reference
 * the sample data.
 * @param buffer the sample data and audio format to save.
 * @param identifier What to call the sample.
 * @return True if there weren't any problems.
 */
    public boolean loadSound( SoundBuffer buffer, String identifier )
    {
        return true;
    }

/**
 * Returns the filenames of all previously loaded sounds.
 * @return LinkedList of String filenames.
 */
    public LinkedList<String> getAllLoadedFilenames()
    {
        LinkedList<String> filenames = new LinkedList<String>();
        Set<String> keys = bufferMap.keySet();
        Iterator<String> iter = keys.iterator();

        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            filenames.add( iter.next() );
        }

        return filenames;
    }

/**
 * Returns the sourcenames of all sources.
 * @return LinkedList of String sourcenames.
 */
    public LinkedList<String> getAllSourcenames()
    {
        LinkedList<String> sourcenames = new LinkedList<String>();
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();

        if( midiChannel != null )
            sourcenames.add( midiChannel.getSourcename() );

        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            sourcenames.add( iter.next() );
        }

        return sourcenames;
    }

/**
 * Removes a pre-loaded sound from memory.  This is a good method to use for
 * freeing up memory after a large sound file is no longer needed.  NOTE: the
 * source will remain in memory after this method has been called, for as long
 * as the sound is attached to an existing source.
 * @param filename Filename/identifier of the sound file to unload.
 */
    public void unloadSound( String filename )
    {
        bufferMap.remove( filename );
    }

/**
 * Opens a direct line for streaming audio data.
 * @param audioFormat Format that the data will be in.
 * @param sourcename A unique identifier for this source.  Two sources may not use the same sourcename.
 * @param posX X position for this source.
 * @param posY Y position for this source.
 * @param posZ Z position for this source.
 * @param attModel Attenuation model to use.
 * @param distOrRoll Either the fading distance or rolloff factor, depending on the value of "attmodel".
 */
    public void rawDataStream( AudioFormat audioFormat, boolean priority,
                               String sourcename, float posX, float posY,
                               float posZ, int attModel, float distOrRoll )
    {
        sourceMap.put( sourcename,
                       new Source( audioFormat, priority, sourcename, posX,
                                   posY, posZ, attModel, distOrRoll ) );
    }
    
/**
 * Creates a new source using the specified information.
 * @param priority Setting this to true will prevent other sounds from overriding this one.
 * @param toStream Setting this to true will load the sound in pieces rather than all at once.
 * @param toLoop Should this source loop, or play only once.
 * @param sourcename A unique identifier for this source.  Two sources may not use the same sourcename.
 * @param filenameURL Filename/URL of the sound file to play at this source.
 * @param posX X position for this source.
 * @param posY Y position for this source.
 * @param posZ Z position for this source.
 * @param attModel Attenuation model to use.
 * @param distOrRoll Either the fading distance or rolloff factor, depending on the value of "attmodel".
 */
    public void newSource( boolean priority, boolean toStream, boolean toLoop,
                           String sourcename, FilenameURL filenameURL,
                           float posX, float posY, float posZ, int attModel,
                           float distOrRoll )
    {
        sourceMap.put( sourcename,
                       new Source( priority, toStream, toLoop, sourcename,
                                   filenameURL, null, posX, posY, posZ,
                                   attModel, distOrRoll, false ) );
    }
    
/**
 * Creates and immediately plays a new source that will be removed when it 
 * finishes playing.
 * @param priority Setting this to true will prevent other sounds from overriding this one.
 * @param toStream Setting this to true will load the sound in pieces rather than all at once.
 * @param toLoop Should this source loop, or play only once.
 * @param sourcename A unique identifier for this source.  Two sources may not use the same sourcename.
 * @param filenameURL The filename/URL of the sound file to play at this source.
 * @param posX X position for this source.
 * @param posY Y position for this source.
 * @param posZ Z position for this source.
 * @param attModel Attenuation model to use.
 * @param distOrRoll Either the fading distance or rolloff factor, depending on the value of "attmodel".
 */
    public void quickPlay( boolean priority, boolean toStream, boolean toLoop,
                           String sourcename, FilenameURL filenameURL,
                           float posX, float posY, float posZ, int attModel,
                           float distOrRoll, boolean tmp )
    {
        sourceMap.put( sourcename,
                       new Source( priority, toStream, toLoop, sourcename,
                                   filenameURL, null, posX, posY, posZ,
                                   attModel, distOrRoll, tmp ) );
    }
    
/**
 * 
 * Defines whether or not the source should be removed after it finishes 
 * playing.  
 * @param sourcename The source's name.
 * @param temporary True or False.
 */
    public void setTemporary( String sourcename, boolean temporary )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setTemporary( temporary );
    }
    
/**
 * Changes the specified source's position.
 * @param sourcename The source's name.
 * @param x Destination X coordinate.
 * @param y Destination Y coordinate.
 * @param z Destination Z coordinate.
 */
    public void setPosition( String sourcename, float x, float y, float z )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setPosition( x, y, z );
    }
    
/**
 * Sets the specified source's priority factor.  A priority source will not be 
 * overriden if there are too many sources playing at once.
 * @param sourcename The source's name.
 * @param pri True or False.
 */
    public void setPriority( String sourcename, boolean pri )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setPriority( pri );
    }
    
/**
 * Sets the specified source's looping parameter.  If parameter lp is false, 
 * the source will play once and stop.
 * @param sourcename The source's name.
 * @param lp True or False.
 */
    public void setLooping( String sourcename, boolean lp )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setLooping( lp );
    }
    
/**
 * Sets the specified source's attenuation model. 
 * @param sourcename The source's name.
 * @param model Attenuation model to use.
 */
    public void setAttenuation( String sourcename, int model )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setAttenuation( model );
    }
    
/**
 * Sets the specified source's fade distance or rolloff factor. 
 * @param sourcename The source's name.
 * @param dr Fade distance or rolloff factor.
 */
    public void setDistOrRoll( String sourcename, float dr)
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setDistOrRoll( dr );
    }

/**
 * Sets the specified source's velocity, for use in Doppler effect.
 * @param sourcename The source's name.
 * @param x Velocity along world x-axis.
 * @param y Velocity along world y-axis.
 * @param z Velocity along world z-axis.
 */
    public void setVelocity( String sourcename, float x, float y, float z )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.setVelocity( x, y, z );
    }

/**
 * Sets the listener's velocity, for use in Doppler effect.
 * @param x Velocity along world x-axis.
 * @param y Velocity along world y-axis.
 * @param z Velocity along world z-axis.
 */
    public void setListenerVelocity( float x, float y, float z )
    {
        listener.setVelocity( x, y, z );
    }

/**
 * Notifies the underlying library that the Doppler parameters have changed.
 */
    public void dopplerChanged()
    {}
    
/**
 * Returns the number of miliseconds since the specified source began playing.
 * @return miliseconds, or -1 if not playing or unable to calculate
 */
    public float millisecondsPlayed( String sourcename )
    {
        if( sourcename == null || sourcename.equals( "" ) )
        {
            errorMessage( "Sourcename not specified in method " +
                          "'millisecondsPlayed'" );
            return -1;
        }

        if( midiSourcename( sourcename ) )
        {
            errorMessage( "Unable to calculate milliseconds for MIDI source." );
            return -1;
        }
        else
        {
            Source source = sourceMap.get( sourcename );
            if( source == null )
            {
                errorMessage( "Source '" + sourcename + "' not found in " +
                              "method 'millisecondsPlayed'" );
            }
            return source.millisecondsPlayed();
        }
    }
/**
 * Feeds raw data through the specified source.  The source must be a
 * streaming source and it can not be already associated with a file or URL to
 * stream from.
 * @param sourcename Name of the streaming source to play from.
 * @param buffer Byte buffer containing raw audio data to stream.
 * @return Number of prior buffers that have been processed, or -1 if unable to queue the buffer (if the source was culled, for example).
 */
    public int feedRawAudioData( String sourcename, byte[] buffer )
    {
        if( sourcename == null || sourcename.equals( "" ) )
        {
            errorMessage( "Sourcename not specified in method " +
                          "'feedRawAudioData'" );
            return -1;
        }

        if( midiSourcename( sourcename ) )
        {
            errorMessage( "Raw audio data can not be fed to the " +
                          "MIDI channel." );
            return -1;
        }
        else
        {
            Source source = sourceMap.get( sourcename );
            if( source == null )
            {
                errorMessage( "Source '" + sourcename + "' not found in " +
                              "method 'feedRawAudioData'" );
            }
            return feedRawAudioData( source, buffer );
        }
    }

/**
 * Feeds raw data through the specified source.  The source must be a
 * streaming source and it can not be already associated with a file or URL to
 * stream from.
 * @param source Streaming source to play from.
 * @param buffer Byte buffer containing raw audio data to stream.
 * @return Number of prior buffers that have been processed, or -1 if unable to queue the buffer (if the source was culled, for example).
 */
    public int feedRawAudioData( Source source, byte[] buffer )
    {
        if( source == null )
        {
            errorMessage( "Source parameter null in method " +
                          "'feedRawAudioData'" );
            return -1;
        }
        if( !source.toStream )
        {
            errorMessage( "Only a streaming source may be specified in " +
                          "method 'feedRawAudioData'" );
            return -1;
        }
        if( !source.rawDataStream )
        {
            errorMessage( "Streaming source already associated with a " +
                          "file or URL in method'feedRawAudioData'" );
            return -1;
        }

        if( !source.playing() || source.channel == null )
        {
            Channel channel;
            if( source.channel != null && ( source.channel.attachedSource ==
                                            source ) )
                channel = source.channel;
            else
                channel = getNextChannel( source );

            int processed = source.feedRawAudioData( channel, buffer );
            channel.attachedSource = source;
            streamThread.watch( source );
            streamThread.interrupt();
            return processed;
        }

        return( source.feedRawAudioData( source.channel, buffer ) );
    }

/**
 * Looks up the specified source and plays it.  
 * @param sourcename Name of the source to play.
 */
    public void play( String sourcename )
    {
        if( sourcename == null || sourcename.equals( "" ) )
        {
            errorMessage( "Sourcename not specified in method 'play'" );
            return;
        }
        
        if( midiSourcename( sourcename ) )
        {
            midiChannel.play();
        }
        else
        {
            Source source = sourceMap.get( sourcename );
            if( source == null )
            {
                errorMessage( "Source '" + sourcename + "' not found in " +
                              "method 'play'" );
            }
            play( source );
        }
    }
    
/**
 * Plays the specified source. 
 * @param source The source to play.
 */
    public void play( Source source )
    {
        if( source == null )
            return;

        // raw data streams will automatically play when data is sent to them,
        // so no need to do anything here.
        if( source.rawDataStream )
            return;

        if( !source.active() )
            return;
        
        if( !source.playing() )
        {
            Channel channel = getNextChannel( source );

            if( source != null && channel != null )
            {
                if( source.channel != null &&
                    source.channel.attachedSource != source )
                    source.channel = null;
                channel.attachedSource = source;
                source.play( channel );
                if( source.toStream )
                {
                    streamThread.watch( source );
                    streamThread.interrupt();
                }
            }
        }
    }
    
/**
 * Stops the specified source. 
 * @param sourcename The source's name.
 */
    public void stop( String sourcename )
    {
        if( sourcename == null || sourcename.equals( "" ) )
        {
            errorMessage( "Sourcename not specified in method 'stop'" );
            return;
        }
        if( midiSourcename( sourcename ) )
        {
            midiChannel.stop();
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.stop();
        }
    }
    
/**
 * Pauses the specified source. 
 * @param sourcename The source's name.
 */
    public void pause( String sourcename )
    {
        if( sourcename == null || sourcename.equals( "" ) )
        {
            errorMessage( "Sourcename not specified in method 'stop'" );
            return;
        }
        if( midiSourcename( sourcename ) )
        {
            midiChannel.pause();
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.pause();
        }
    }
    
/**
 * Rewinds the specified source. 
 * @param sourcename The source's name.
 */
    public void rewind( String sourcename )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.rewind();
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.rewind();
        }
    }

/**
 * Clears all previously queued data from a stream.
 * @param sourcename The source's name.
 */
    public void flush( String sourcename )
    {
        if( midiSourcename( sourcename ) )
            errorMessage( "You can not flush the MIDI channel" );
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.flush();
        }
    }
    
/**
 * Culls the specified source.  A culled source will not play until it has been 
 * activated again.
 * @param sourcename The source's name.
 */
    public void cull( String sourcename )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
            mySource.cull();
    }
    
/**
 * Activates a previously culled source, so it can be played again.  
 * @param sourcename The source's name.
 */
    public void activate( String sourcename )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null )
        {
            mySource.activate();
            if( mySource.toPlay )
                play( mySource );
        }
    }

/**
 * Sets the overall volume to the specified value, affecting all sources.
 * @param value New volume, float value ( 0.0f - 1.0f ).
 */
    public void setMasterVolume( float value )
    {
        SoundSystemConfig.setMasterGain( value );
        if( midiChannel != null )
            midiChannel.resetGain();
    }

/**
 * Manually sets the specified source's volume.  
 * @param sourcename The source's name.
 * @param value A float value ( 0.0f - 1.0f ).
 */
    public void setVolume( String sourcename, float value )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.setVolume( value );
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
            {
                float newVolume = value;
                if( newVolume < 0.0f )
                    newVolume = 0.0f;
                else if( newVolume > 1.0f )
                    newVolume = 1.0f;

                mySource.sourceVolume = newVolume;
                mySource.positionChanged();
            }
        }
    }
    
/**
 * Returns the current volume of the specified source, or zero if the specified 
 * source was not found.
 * @param sourcename Source to read volume from.
 * @return Float value representing the source volume (0.0f - 1.0f).
 */
    public float getVolume( String sourcename )
    {
        if( midiSourcename( sourcename ) )
        {
            return midiChannel.getVolume();
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                return mySource.sourceVolume;
            else
                return 0.0f;
        }
    }
    
/**
 * Manually sets the specified source's pitch.
 * @param sourcename The source's name.
 * @param value A float value ( 0.5f - 2.0f ).
 */
    public void setPitch( String sourcename, float value )
    {
        if( !midiSourcename( sourcename ) )
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
            {
                float newPitch = value;
                if( newPitch < 0.5f )
                    newPitch = 0.5f;
                else if( newPitch > 2.0f )
                    newPitch = 2.0f;

                mySource.setPitch( newPitch );
                mySource.positionChanged();
            }
        }
    }

/**
 * Returns the pitch of the specified source.
 * @param sourcename The source's name.
 * @return Float value representing the source pitch (0.5f - 2.0f).
 */
    public float getPitch( String sourcename )
    {
        if( !midiSourcename( sourcename ) )
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                return mySource.getPitch();
        }
        return 1.0f;
    }

/**
 * Moves the listener relative to the current position. 
 * @param x X offset.
 * @param y Y offset.
 * @param z Z offset.
 */
    public void moveListener( float x, float y, float z )
    {
        setListenerPosition( listener.position.x + x, listener.position.y + y,
                             listener.position.z + z );
    }
    
/**
 * Changes the listener's position. 
 * @param x Destination X coordinate.
 * @param y Destination Y coordinate.
 * @param z Destination Z coordinate.
 */
    public void setListenerPosition( float x, float y, float z )
    {
        // update listener's position
        listener.setPosition( x, y, z );
        
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source source;
        
        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
                source.positionChanged();
        }
    }
    
/**
 * Turn the listener 'angle' radians counterclockwise around the y-Axis, 
 * relative to the current angle.
 * @param angle Angle in radians.
 */
    public void turnListener( float angle )
    {
        setListenerAngle( listener.angle + angle );

        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();
        String sourcename;
        Source source;

        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
                source.positionChanged();
        }
    }
    
/**
 * Changes the listeners orientation to the specified 'angle' radians 
 * counterclockwise around the y-Axis.
 * @param angle Angle in radians.
 */
    public void setListenerAngle( float angle )
    {
        listener.setAngle( angle );
        
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source source;
        
        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
                source.positionChanged();
        }
    }
    
/**
 * Changes the listeners orientation using the specified coordinates.
 * @param lookX X element of the look-at direction.
 * @param lookY Y element of the look-at direction.
 * @param lookZ Z element of the look-at direction.
 * @param upX X element of the up direction.
 * @param upY Y element of the up direction.
 * @param upZ Z element of the up direction.
 */
    public void setListenerOrientation( float lookX, float lookY, float lookZ,
                                        float upX, float upY, float upZ )
    {
        listener.setOrientation( lookX, lookY, lookZ, upX, upY, upZ );
        
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source source;
        
        // loop through and update the volume of all sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
                source.positionChanged();
        }
    }

/**
 * Changes the listeners position and orientation using the specified listener 
 * data.
 * @param l Listener data to use.
 */
    public void setListenerData( ListenerData l )
    {
        listener.setData( l );
    }
    
/**
 * Creates sources based on the source map provided.
 * @param srcMap Sources to copy.
 */
    public void copySources( HashMap<String, Source> srcMap )
    {
        if( srcMap == null )
            return;
        Set<String> keys = srcMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source srcData;
        
        // remove any existing sources before starting:
        sourceMap.clear();
        
        // loop through and copy all the sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            srcData = srcMap.get( sourcename );
            if( srcData != null )
            {
                loadSound( srcData.filenameURL );
                sourceMap.put( sourcename, new Source( srcData, null ) );
            }
        }
    }

/**
 * Stops and deletes the specified source.
 * @param sourcename The source's name.
 */
    public void removeSource( String sourcename )
    {
        Source mySource = sourceMap.get( sourcename );
        if( mySource != null ) {
            // if this is a streaming source just mark it removed - https://github.com/MinecraftForge/MinecraftForge/pull/4765
            if ( mySource.toStream )
                mySource.removed = true;
            else
                mySource.cleanup(); // end the source, free memory
        }
        sourceMap.remove( sourcename );
    }

/**
 * Searches for and removes all temporary sources that have finished playing.
 */
    public void removeTemporarySources()
    {
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();
        String sourcename;
        Source srcData;

        // loop through and cleanup all the sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            srcData = sourceMap.get( sourcename );
            if( (srcData != null) && (srcData.temporary)
                 && (!srcData.playing()) )
            {
                srcData.cleanup(); // end the source, free memory
                iter.remove();
            }
        }
    }

/* ########################################################################## */
/*                         END OVERRIDE METHODS                               */
/* ########################################################################## */

/**
 * Returns a handle to the next available channel.  If the specified
 * source is a normal source, a normal channel is returned, and if it is a
 * streaming source, then a streaming channel is returned.  If all channels of
 * the required type are currently playing, then the next channel playing a
 * non-priority source is returned.  If no channels are available (i.e. they
 * are all playing priority sources) then getNextChannel returns null.
 * @param source Source to find a channel for.
 * @return The next available channel, or null.
 */
    private Channel getNextChannel( Source source )
    {
        if( source == null )
            return null;

        String sourcename = source.sourcename;
        if( sourcename == null )
            return null;

        int x;
        int channels;
        int nextChannel;
        List<Channel> channelList;
        String[] sourceNames;
        String name;

        if( source.toStream )
        {
            nextChannel = nextStreamingChannel;
            channelList = streamingChannels;
            sourceNames = streamingChannelSourceNames;
        }
        else
        {
            nextChannel = nextNormalChannel;
            channelList = normalChannels;
            sourceNames = normalChannelSourceNames;
        }

        channels = channelList.size();

        // Check if this source is already on a channel:
        for( x = 0; x < channels; x++ )
        {
            if( sourcename.equals( sourceNames[x] ) )
                return channelList.get( x );
        }

        int n = nextChannel;
        Source src;
        // Play on the next new or non-playing channel:
        for( x = 0; x < channels; x++ )
        {
            name = sourceNames[n];
            if( name == null )
                src = null;
            else
                src = sourceMap.get( name );

            if( src == null || !src.playing() )
            {
                if( source.toStream )
                {
                    nextStreamingChannel = n + 1;
                    if( nextStreamingChannel >= channels )
                        nextStreamingChannel = 0;
                }
                else
                {
                    nextNormalChannel = n + 1;
                    if( nextNormalChannel >= channels )
                        nextNormalChannel = 0;
                }
                sourceNames[n] = sourcename;
                return channelList.get( n );
            }
            n++;
            if( n >= channels )
                n = 0;
        }

        n = nextChannel;
        // Play on the next non-priority channel:
        for( x = 0; x < channels; x++ )
        {
            name = sourceNames[n];
            if( name == null )
                src = null;
            else
                src = sourceMap.get( name );

            if( src == null || !src.playing() || !src.priority )
            {
                if( source.toStream )
                {
                    nextStreamingChannel = n + 1;
                    if( nextStreamingChannel >= channels )
                        nextStreamingChannel = 0;
                }
                else
                {
                    nextNormalChannel = n + 1;
                    if( nextNormalChannel >= channels )
                        nextNormalChannel = 0;
                }
                sourceNames[n] = sourcename;
                return channelList.get( n );
            }
            n++;
            if( n >= channels )
                n = 0;
        }

        return null;
    }

/**
 * Plays all sources whose 'toPlay' varriable is true but are not currently
 * playing (such as sources which were culled while looping and then
 * reactivated).
 */
    public void replaySources()
    {
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();
        String sourcename;
        Source source;

        // loop through and cleanup all the sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            source = sourceMap.get( sourcename );
            if( source != null )
            {
                if( source.toPlay && !source.playing() )
                {
                    play( sourcename );
                    source.toPlay = false;
                }
            }
        }
    }

/**
 * If the specified source is a streaming source or MIDI source, this method
 * queues up the next sound to play when the previous playback ends.  This
 * method has no effect on non-streaming sources.
 * @param sourcename Source identifier.
 * @param filenameURL Filename/URL of the sound file to play next.
 */
    public void queueSound( String sourcename, FilenameURL filenameURL )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.queueSound( filenameURL );
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.queueSound( filenameURL );
        }
    }

/**
 * Removes the first occurrence of the specified filename from the specified
 * source's list of sounds to play when previous playback ends.  This method
 * has no effect on non-streaming sources.
 * @param sourcename Source identifier.
 * @param filename Filename/identifier of the sound file to remove from the queue.
 */
    public void dequeueSound( String sourcename, String filename )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.dequeueSound( filename );
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.dequeueSound( filename );
        }
    }

/**
 * Fades out the volume of whatever the specified source is currently playing,
 * then begins playing the specified file at the source's previously
 * assigned volume level.  If the filenameURL parameter is null or empty, the
 * specified source 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 specified source's list of queued sounds that would have
 * played next when the current sound finished playing.  This method may only
 * be used for streaming and MIDI sources.
 * @param sourcename Name of the source to fade out.
 * @param filenameURL Filename/URL of the sound file to play next, or null for none.
 * @param milis Number of miliseconds the fadeout should take.
 */
    public void fadeOut( String sourcename, FilenameURL filenameURL,
                         long milis )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.fadeOut( filenameURL, milis );
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.fadeOut( filenameURL, milis );
        }
    }

/**
 * Fades out the volume of whatever the specified source is currently playing,
 * then fades the volume back in playing the specified file.  Final volume
 * after fade-in completes will be equal to the source's 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 specified source's list of queued
 * sounds that would have played next when the current sound finished playing.
 * This method may only be used for streaming and MIDI sources.
 * @param sourcename Name of the source to fade out/in.
 * @param filenameURL Filename/URL of the sound 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( String sourcename, FilenameURL filenameURL,
                           long milisOut, long milisIn )
    {
        if( midiSourcename( sourcename ) )
        {
            midiChannel.fadeOutIn( filenameURL, milisOut, milisIn );
        }
        else
        {
            Source mySource = sourceMap.get( sourcename );
            if( mySource != null )
                mySource.fadeOutIn( filenameURL, milisOut, milisIn );
        }
    }

/**
 * Makes sure the current volume levels of streaming sources and MIDI are
 * correct.  This method is designed to help reduce the "jerky" fading behavior
 * that happens when using some library and codec pluggins (such as
 * LibraryJavaSound and CodecJOrbis).  This method has no effect on normal
 * "non-streaming" sources.  It would normally be called somewhere in the main
 * "game loop".  IMPORTANT: To optimize frame-rates, do not call this method
 * for every frame.  It is better to just call this method at some acceptable
 * "granularity" (play around with different granularities to find what sounds
 * acceptable for a particular situation).
 */
    public void checkFadeVolumes()
    {
        if( midiChannel != null )
            midiChannel.resetGain();
        Channel c;
        Source s;
        for( int x = 0; x < streamingChannels.size(); x++ )
        {
            c = streamingChannels.get( x );
            if( c != null )
            {
                s = c.attachedSource;
                if( s != null )
                    s.checkFadeOut();
            }
        }
        c = null;
        s = null;
    }

/**
 * Loads the specified MIDI file, and saves the source information about it.
 * @param toLoop Midi file should loop or play once.
 * @param sourcename Source identifier.
 * @param filenameURL Filename/URL of the MIDI file to load.
 */
    public void loadMidi( boolean toLoop, String sourcename,
                          FilenameURL filenameURL )
    {
        if( filenameURL == null )
        {
            errorMessage( "Filename/URL not specified in method 'loadMidi'." );
            return;
        }

        if( !filenameURL.getFilename().matches(
                                            SoundSystemConfig.EXTENSION_MIDI ) )
        {
            errorMessage( "Filename/identifier doesn't end in '.mid' or" +
                          "'.midi' in method loadMidi." );
            return;
        }

        if( midiChannel == null )
        {
            midiChannel = new MidiChannel( toLoop, sourcename, filenameURL );
        }
        else
        {
            midiChannel.switchSource( toLoop, sourcename, filenameURL );
        }
    }

/**
 * Unloads the current Midi file.
 */
    public void unloadMidi()
    {
        if( midiChannel != null )
            midiChannel.cleanup();
        midiChannel = null;
    }

/**
 * Checks if the sourcename matches the midi source.
 * @param sourcename Source identifier.
 * @return True if sourcename and midi sourcename match.
 */
    public boolean midiSourcename( String sourcename )
    {
        if( midiChannel == null || sourcename == null )
            return false;

        if( midiChannel.getSourcename() == null || sourcename.equals( "" ) )
            return false;

        if( sourcename.equals( midiChannel.getSourcename() ) )
            return true;

        return false;
    }

/**
 *
 * Returns the Source object identified by the specified name.
 * @param sourcename The source's name.
 * @return The source, or null if not found.
 */
    public Source getSource( String sourcename )
    {
        return sourceMap.get( sourcename );
    }

/**
 *
 * Returns a handle to the MIDI channel, or null if one does not exist.
 * @return The MIDI channel.
 */
    public MidiChannel getMidiChannel()
    {
        return midiChannel;
    }

/**
 *
 * Specifies the MIDI channel to use.
 * @param c New MIDI channel.
 */
    public void setMidiChannel( MidiChannel c )
    {
        if( midiChannel != null && midiChannel != c )
            midiChannel.cleanup();

        midiChannel = c;
    }

/**
 * Tells all the sources that the listener has moved.
 */
    public void listenerMoved()
    {
        Set<String> keys = sourceMap.keySet();
        Iterator<String> iter = keys.iterator();        
        String sourcename;
        Source srcData;
        
        // loop through and copy all the sources:
        while( iter.hasNext() )
        {
            sourcename = iter.next();
            srcData = sourceMap.get( sourcename );
            if( srcData != null )
            {
                srcData.listenerMoved();
            }
        }
    }
    
/**
 * Returns the sources map.
 * @return Map of all sources.
 */
    public HashMap<String, Source> getSources()
    {
        return sourceMap;
    }
    
/**
 * Returns information about the listener.
 * @return A ListenerData object.
 */
    public ListenerData getListenerData()
    {
        return listener;
    }
    
/**
 * Indicates whether or not this library requires some codecs to reverse-order
 * the audio data they generate.
 * @return True if audio data should be reverse-ordered.
 */
    public boolean reverseByteOrder()
    {
        return reverseByteOrder;
    }
/**
 * Returns the short title of this library type.
 * @return A short title.
 */
    public static String getTitle()
    {
        return "No Sound";
    }
    
/**
 * Returns a longer description of this library type.
 * @return A longer description.
 */
    public static String getDescription()
    {
        return "Silent Mode";
    }
    
/**
 * Returns the name of the class.
 * @return "Library" + library title.
 */
    public String getClassName()
    {
        return "Library";
    }
    
/**
 * 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, getClassName(), message, 0 );
    }
    
/**
 * Prints an error message.
 * @param message Message to print.
 */
    protected void errorMessage( String message )
    {
        logger.errorMessage( getClassName(), 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 );
    }
}
