/*
Copyright (c) 2006, Geomatics and Cartographic Research Centre, Carleton 
University All rights reserved.

Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice, 
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the Geomatics and Cartographic Research Centre, 
   Carleton University nor the names of its contributors may be used to 
   endorse or promote products derived from this software without specific 
   prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.

$Id$
*/
package ca.carleton.gcrc.atlas;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.ArrayList;
import java.util.Timer;

import javax.sound.sampled.Mixer;
import javax.sound.sampled.AudioSystem;

public class TrackManager implements FadeTimerParent<AudioTrack> {

	/**
	 * Map that associates track URL strings to tracks.
	 */
	private Map<String,AudioTrack> m_tracks;
	
	/*
	 * Preferred clip mixer.  The Java Sound Audio Engine.  A pure java mixer with
	 * low latency on gain setting changes, etc.  When loading audio clips, this 
	 * mixer is tried first.  However, requests for this mixer sometimes fail in which
	 * case the Clip will attempt to load using the default mixwer.  The most common
	 * failure seems to be that the clip is too big for the Java Sound Audio Engine
	 * (limit is equivalent of 1048576 samples of 4 bytes each (i.e. a 4M buffer)).
	 */
	private Mixer.Info m_preferredClipMixer;
	
	/*
	 * boolean indicating whether a global mute is in effect.
	 */
	private boolean m_allTracksMuted;
	
	/**
	 * Base URL of the document containing the applet. All relative
	 * URL for tracks should be computed from this URL.
	 */
	private URL m_documentBase;
	
	/**
	 * FadeTimerTask used to manage gain fades for all tracks managed by this track manager.  The
	 * track itself will be used as the key to allow fast access to setting the track's gain during
	 * a fade operation.
	 * 
	 * The manager handles the creation and destruction of the fade timer but the calls to do so are
	 * initiated by the tracks which need their gain controlled.  This allows a single thread to control
	 * gain setting for all tracks.
	 */
	private Timer m_fadeTimer;
	private FadeTimerTask<AudioTrack> m_trackGainFaderTask;
	
	/**
	 * Constructor based on a URL that represents the document's base.
	 * @param documentBase_
	 */
	public TrackManager( URL documentBase_ ) {
		init(documentBase_);
	}
	
	/**
	 * Constructor based on a string that represents the document base URL.
	 * @param documentBase_ URL of the document where all relative URL are computed from.
	 * @throws MalformedURLException Thrown if the given URL string is not a well formed 
	 * URL.  
	 */
	public TrackManager( String documentBase_ ) throws MalformedURLException {
		URL temp = new URL(documentBase_);
		init(temp);
	}
	
	private void init(URL documentBase_) {
		m_tracks = new HashMap<String,AudioTrack>();
		m_allTracksMuted = false;
		m_documentBase = documentBase_;
		m_trackGainFaderTask = null;
		m_fadeTimer = null;
		
		try {
			Mixer.Info[] mi = AudioSystem.getMixerInfo();
			// debug... what mixers exist?
			//for (int i=0; i<mi.length; i++) {
			//	System.out.println(i+") vendor:" + mi[i].getVendor() + " name:" + mi[i].getName() +
			//			" description:" + mi[i].getDescription() + " version:" + mi[i].getVersion());
			//}
		
			m_preferredClipMixer = null;
			for (int i=0; i<mi.length; i++) {
				if ("Java Sound Audio Engine" == mi[i].getName()) {
					m_preferredClipMixer = mi[i];
				}
			}
		} catch (Exception ex) {
			// Auto-generated catch block
			ex.printStackTrace();
		}
	} // end init
	
	protected final class Debug {
		  protected static final int standardLevel = 0;
		  protected static final int detailLevel = 1;
		  
		  protected static final boolean ON = false;
		  protected static final int LEVEL = standardLevel;
	} 
	
	protected void DebugLog(String str) {
		if (Debug.ON) System.out.println(str);
	}
	
	/**
	 * This method should be called by an applet 'start' method.
	 */
	public void resume() {
		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			track.setPaused(false);
		}
	}

	/**
	 * This method should be called to stop all sound activity immediately. It is useful
	 * for applets and should be called from the method 'stop' in the applet.
	 */
	public void pause() {
		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			track.setPaused(true);
		}
	}

	/**
	 * This method should be called to release all resources held by tracks. Useful
	 * for applets when 'destroy' is called.
	 */
	public void cleanUp() {
		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			unloadSound(track.getOriginalUrlString(), false); // flag means don't remove from m_tracks ... done below
		}
		m_tracks.clear();
		if (null != m_trackGainFaderTask) {
			destroyFadeTimer();
		}
	} // cleanUp()
	
	/**
	 * This method converts a string to a URL. It attempts to convert relative URL
	 * to absolute ones using the document base URL.
	 * @param urlString_ Desired URL in a String. 
	 * @return A URL instance representing the string given in paramter.
	 * @throws SoundException If the resulting URL is not valid.
	 */
	private URL urlFromStringAddress(String urlString_) throws SoundException {
		URL result;
		try {
			result = new URL(urlString_);
		} catch(MalformedURLException e) {
			URI tempURI;
			try {
				tempURI = m_documentBase.toURI();
			} catch (URISyntaxException e2) {
				DebugLog("SoundApplet TrackManager malformed URI: " + m_documentBase.toString());
				throw new SoundException("Malformed URI: " + m_documentBase.toString());
			}
			try {
				result = tempURI.resolve(urlString_).toURL();
			} catch (MalformedURLException e1) {
				DebugLog("SoundApplet TrackManager malformed URL: " + urlString_);
				throw new SoundException("Malformed URL: " + urlString_);
			}
		}
		
		return result;
	}
	
	/**
	 * Fade out and unload all sounds of the type specified by suffix_ except for those listed in urlStrings[].  Note
	 * there is one gain setting, one loop flag, and one type_ for all of the tracks to be left behind and these are only
	 * used if the specified sounds need to be created (i.e., are not already running).  Aditional interfaces may
	 * be needed later.
	 * 
	 * @param urlStrings_, set of urlStings to keep running or to start running is not already running.
	 * @param fadeDuration_, duration over which tracks to be removed should be faded.
	 * @param gain_, gain setting for tracks desired but not yet running.
	 * @param loop_, should the tracks to be left running be looped?
	 * @param type_, type enum indicating which kind of track to create for the AudioTracks specified by the URLs (if required).
	 */
	private void fadeOutAndUnloadSoundsExceptWithType(String[] urlStrings_, long fadeDuration_, float gain_,
													  boolean loop_, TrackManagerTrackType type_) {
		// remove unwanted sounds 
		ArrayList<String> tracksToRemove = new ArrayList<String>(); // to collect tracks to unload til after HashMap iteration.
		int index;
		
		Set entries = m_tracks.entrySet();
		Iterator entryIter = entries.iterator();
		while (entryIter.hasNext()) {
			Map.Entry entry = (Map.Entry)entryIter.next();
			String key = (String) entry.getKey();  // Get the key from the entry.
			boolean found = false;
			//System.out.println("checking: " + key);
			for (index=0; index<urlStrings_.length; ++index) {
				if (true == key.contentEquals(urlStrings_[index])) {
					found = true; // string is in except list - leave this one alone
					break;
				}
			} // end for
			if (!found) {
				// not in except list - store info for removal below.  Trying to remove the track now
				// while in the middle of a HashMap iteration will result in exceptions.
				tracksToRemove.add(key);
			}
		} // end while
		
		// now that iterating the HashMap is complete - actually  fade and remove things
		for (index=0; index<tracksToRemove.size(); index++) {
			//System.out.println("removing: " + tracksToRemove.get(index));
			fadeOutAndUnloadSound(tracksToRemove.get(index), fadeDuration_);
		}
		
		// add streams desired that aren't already running.
		for (index=0; index<urlStrings_.length; ++index) {
			if (false == urlStrings_[index].contentEquals("")) {  // not the null string and ...
				if (false == m_tracks.containsKey(urlStrings_[index])) { // not already loaded - load it!
					//System.out.println("adding: " + urlStrings_[index]);
					AudioTrack track = null;
					if (type_ == TrackManagerTrackType.StreamTrack) {
						track = addStreamTrackToMap(urlStrings_[index]);
					} else if (type_ == TrackManagerTrackType.ClipTrack) {
						track = addTrackToMap(urlStrings_[index]);
					}
					if (null != track) {// was added successfully
						track.setShouldLoop(loop_);
						track.setGain((float) 0.0);
						if (true == m_allTracksMuted) {
							track.setMuting(true);
						}
						track.setPlaying(true);
						startFadingGain(gain_, fadeDuration_, track);
					}
				}
			}
		} // end for		
	} // end fadeOutAndUnloadSoundsExceptWithType()
	
//	private void dumpKeys() {
//		Iterator tracks_iter = m_tracks.keySet().iterator();
//		while( tracks_iter.hasNext() ) {
//			String key = (String) tracks_iter.next();
//			System.out.println("TrackManager:dumpKeys: " + key);
//		}
//	}
	
	public void playAllSounds() {
		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			track.setPlaying(true);
		}
	}

	/**
	 * Mute or Unmute all currently loaded sounds according to the specified input flag.  The 
	 * input flag is stored - if true, subsequently loaded sounds will also be muted until at least
	 * one sound is unmuted.
	 * 
	 * @param flag_ true means muting all sounds while false means unmuting all sounds.
	 */
	public void setMutingForAllSounds(boolean flag_) {
		m_allTracksMuted = flag_;

		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			track.setMuting(flag_);
		}
	}

	/**
	 * This method checks for the presence of a AudioTrack associated with the input URL parameter. 
	 * It does NOT load the AudioTrack if it does not already exist.  If the AudioTrack exists, it is
	 * returned.
	 * 
	 * @param urlString_ Desired URL in a String.
	 * @return Returns the desired AudioTrack instance, or null.
	 */
	private AudioTrack checkTrack(String urlString_) {
		if( m_tracks.containsKey(urlString_) ) {
			return (AudioTrack)m_tracks.get(urlString_);
		}
		return null;
	}
	
	/**
	 * Add a ClipTrack associated with the input URL to the track map and return the AudioTrack.  If
	 * the URL is already in the map, simply return the AudioTrack.
	 * @param urlString_ URL associated with the AudioTrack as a String.
	 * @return Returns the desired AudioTrack,, or null if the AudioTrack can not be created.
	 */
	private AudioTrack addTrackToMap(String urlString_) {
		ClipTrack clip;
		if( !m_tracks.containsKey(urlString_) ) {
			try {
				clip = new ClipTrack(urlFromStringAddress(urlString_), urlString_, m_preferredClipMixer);
			} catch (SoundException e) {
				return null;
			}
			
			if (null != clip) {
				m_tracks.put(urlString_, clip);					
			} else {
				DebugLog("TrackManager::addTrackToMap: unable to load ClipTrack - " + urlString_);
			}
		}
		return (AudioTrack)m_tracks.get(urlString_);
	}

	/**
	 * Add a StreamTrack associated with the input URL to the track map and return the AudioTrack.  If
	 * the URL is already in the map, simply return the AudioTrack.
	 * @param urlString_ URL associated with the AudioTrack as a String.
	 * @return Returns the desired AudioTrack,, or null if the AudioTrack can not be created.
	 */
	private AudioTrack addStreamTrackToMap(String urlString_) {
		StreamTrack stream;
		if( !m_tracks.containsKey(urlString_) ) {
			try {
				stream = new StreamTrack(urlFromStringAddress(urlString_), urlString_);
			} catch (SoundException e) {
				return null;
			}
			
			if (null != stream) {
				m_tracks.put(urlString_, stream);					
			} else {
				DebugLog("TrackManager::addStreamTrackToMap: unable to load StreamTrack - " + urlString_);
			}
		}
		return (AudioTrack)m_tracks.get(urlString_);
	}

	/**
	 * Remove the AudioTrack associated with the specified URL from the track map.
	 * @param urlString_ URL of track to be removed.
	 */
	synchronized private void removeTrackFromMap(String urlString_) {
		// caller already verified this string was in the map.
		String urlClipString = urlString_;
		m_tracks.remove(urlClipString);
//		dumpKeys();
	}
	
	//
	// ********************** public clip interfaces *******************************
	//
	
	public void loadClipSound(String urlString_) {
		addTrackToMap(urlString_);
	}
	
	public void loadClipSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			loadClipSound(urlStrings_[loop]);
		}
	}
	
	/**
	 * Fade out and unload all tracks except for those listed in urlStrings[].  Note
	 * there is one gain setting and one loop flag for all of the tracks to be left behind.  Aditional interfaces may
	 * be needed later.
	 * 
	 * @param urlStrings_ set of urlStings to keep running or to start running if not already running.
	 * @param fadeDuration_ duration over which tracks to be removed should be faded.
	 * @param gain_ gain setting for tracks desired but not yet running.
	 * @param loop_ should the tracks to be left running be looped?
	 */
	public void fadeOutAndUnloadSoundsExceptClips(String[] urlStrings_, long fadeDuration_, float gain_,  boolean loop_) {
		fadeOutAndUnloadSoundsExceptWithType(urlStrings_, fadeDuration_, gain_,  loop_, TrackManagerTrackType.ClipTrack);
	}
	
	//
	// ********************** public stream interfaces *******************************
	//
	
	public void loadStreamSound(String urlString_) {
		addStreamTrackToMap(urlString_);
	}
	
	public void loadStreamSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			loadStreamSound(urlStrings_[loop]);
		}
	}
	
	/**
	 * Fade out and unload all tracks except for those listed in urlStrings[].  The tracks specified by urlStrings_,
	 * if not already running, will be loaded as StreamTracks.  Note
	 * there is one gain setting and one loop flag for all of the tracks to be left behind.  Aditional interfaces may
	 * be needed later.
	 * 
	 * @param urlStrings_ set of urlStings to keep running or to start running if not already running.
	 * @param fadeDuration_ duration over which tracks to be removed should be faded.
	 * @param gain_ gain setting for tracks desired but not yet running.
	 * @param loop_ should the tracks to be left running be looped?
	 */
	public void fadeOutAndUnloadSoundsExceptStreams(String[] urlStrings_, long fadeDuration_, float gain_,  boolean loop_) {
		fadeOutAndUnloadSoundsExceptWithType(urlStrings_, fadeDuration_, gain_,  loop_, TrackManagerTrackType.StreamTrack);
	}

	//
	// ********************** public generic interfaces (apply to all track types) *******************************
	//
	
	public void playSound(String urlString_) {
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			if (true == m_allTracksMuted) {
				aTrack.setMuting(true);
			}
			aTrack.setPlaying(true);
		}
	}
	
	public void playSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			playSound(urlStrings_[loop]);
		}
	}
	
	public void loopSound(String urlString_) {
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.setShouldLoop(true);
			if (true == m_allTracksMuted) {
				aTrack.setMuting(true);
			}
			aTrack.setPlaying(true);
		}
	}
	
	public void loopSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			loopSound(urlStrings_[loop]);
		}
	}
	
	public void stopSound(String urlString_) {
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.setPlaying(false);
		}
	}
	
	public void stopSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			stopSound(urlStrings_[loop]);
		}
	}
	
	public void setGain(String urlString_, float gain_) {	
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.setGain(gain_);
			aTrack.setConfiguredGain(gain_);
		}
	}

	/**
	 * Mute the specified sound.  Subsequent gain setting changes will be accepted,
	 * stored and ignored until this sound is unmuted.
	 * 
	 * @param urlString_ AudioTrack to be muted
	 */
	public void muteSound(String urlString_) {	
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.setMuting(true);
		}
	}

	/**
	 * Unmute the specified sound.
	 * 
	 * @param urlString_ AudioTrack to be muted
	 */
	public void unmuteSound(String urlString_) {
		m_allTracksMuted = false; // clearing one mute flag means no longer in "all muted" mode
		
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.setMuting(false);
		}
	}

	public void fadeToGain(String urlString_, float gain_, long duration_) {	
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			startFadingGain(gain_, duration_, aTrack);
		}
	}
	
	/**
	 * Fade out the AudioTrack specified by urlString_ and unload it from the track manager
	 * when faded.
	 * @param urlString_ URL string for the track to fade and unload.
	 * @param fadeDuration_ period over which the track should be faded (in milliseconds).
	 */
	public void fadeOutAndUnloadSound(String urlString_, long fadeDuration_) {
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			startFadingGain((float) 0.0, fadeDuration_, aTrack);
			aTrack.setUnloadAfterFade(true);
		}		
	}
	
	/**
	 * Fade out the set of AudioTracks specified by urlStrings_ and unload them from the track manager
	 * when faded.
	 * @param urlStrings_ URL strings for the track to fade and unload.
	 * @param fadeDuration_ period over which the tracks should be faded (in milliseconds).
	 */
	public void fadeOutAndUnloadSounds(String[] urlStrings_, long fadeDuration_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			fadeOutAndUnloadSound(urlStrings_[loop], fadeDuration_);
		}
	}

	synchronized private void unloadSound(String urlString_, boolean removeFromMap_) { // synchronized because of access to gain fader task
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			if (null != m_trackGainFaderTask) { // fade timers running - pull this one out of list
				m_trackGainFaderTask.remove(aTrack);
			}
			aTrack.unload();
			if (removeFromMap_) {
				removeTrackFromMap(urlString_);
			}
		}
	}
	
	public void unloadSound(String urlString_) {
		unloadSound(urlString_, true);
	}
	
	public void unloadSounds(String[] urlStrings_) {
		for(int loop = 0; loop < urlStrings_.length; ++loop) {
			unloadSound(urlStrings_[loop], true);
		}
	}
	
	/**
	 * This method sets all attributes associated with a sound track at once. If the track is not yet loaded,
	 * then this method loads it.
	 * @param urlString_ URL of the sound track in question.  
	 * @param playing_ If set, the track should be playing.
	 * @param looping_ If set, the track should begin again when it reaches the end.
	 * @param paused_ If set, the track should be paused.
	 * @param gain_ Represents the volume of the track. It should be a value between 0.0 and 1.0
	 */
	public void manageTrack(String urlString_, boolean playing_, boolean looping_, boolean paused_, float gain_) {
		AudioTrack aTrack = checkTrack(urlString_);
		if( null != aTrack ) {
			aTrack.manageAttributes(playing_, looping_, paused_);
			if (gain_ != aTrack.getConfiguredGain()) {
				startFadingGain(gain_, 300, aTrack); // fade track over 300 ms
			}
		}
	}
	
	/*
	 * I N T E R F A C E :  FadeTimerParent routines START
	 * 
	 * Gain fade timer handling.
	 * 
	 * The gain values are doubles in the range 0.0 .. 1.0.
	 * TrackManager maintains a single fade timer task on behalf of all managed tracks.  The
	 * key into the fade timer's map of running timers is the Track itself.
	 */
	public Float getCurrentFadeTimerValue(AudioTrack timerId_) throws IllegalArgumentException {
		return(timerId_.getGain());
	} // getCurrentFadeTimerValue()
	
	public void setCurrentFadeTimerValue(Number newValue_, AudioTrack timerId_) throws IllegalArgumentException {
		Float val = (Float) newValue_;
		timerId_.setGain((float) val);
	} // setCurrentFadeTimerValue()

	public void fadeCompleted(AudioTrack timerId_) {
		if (timerId_.isUnloadAfterFadeSet()) {
			unloadSound(timerId_.getOriginalUrlString(), true);
		}
	} // fadeCompleted()
	
	public void allFadeTimersFinished() {
		destroyFadeTimer();
	} // fadeTimerFinished()

	/*
	 * I N T E R F A C E :  FadeTimerParent routines END
	 */

	/**
	 * Creates a fade timer task, if one is not already running, and adds the current fade request to 
	 * the list of running fades managed by that task.  Not synchronized because all callers are synchronized.
	 * @param gain_ target gain value for the new request
	 * @param increment_ calculated increment for the fade operation (+ve means fade up and -ve means fade down).
	 * @param timerId_ timer identifier (the fading track, actually).
	 */
	public void createOrUpdateFadeTimerTask(float gain_, float increment_, AudioTrack timerId_) {
		if (null == m_fadeTimer) { // no fade in progress - start a new fade...
			m_fadeTimer = new Timer();	
			m_trackGainFaderTask = new FadeTimerTask<AudioTrack>(this, (Float)gain_, (Float)increment_, timerId_);
			m_fadeTimer.schedule(m_trackGainFaderTask, (long) 0, FadeTimerParent.TIMER_PERIOD);
		} else { // else - reset the target gain and increment on the existing task ...
			     // timer thread already scheduled
			m_trackGainFaderTask.setTargetValueAndIncrement((Float)gain_, (Float)increment_, timerId_);
		}		
	} // createOrUpdateTimerTask()
	
	/**
	 * Fades the track to the specified master gain setting over the specified duration.  The gain
	 * is adjusted in increments.  Synchronized because of interactions with the
	 * fadeTimerTask object.
	 * @param gain_ target gain value
	 * @param fadeDuration_ period over which the target is to be reached.
	 */
	synchronized public void startFadingGain(float gain_, long fadeDuration_, AudioTrack timerId_) {		
		long fadeDelay = FadeTimerParent.TIMER_PERIOD;
		long steps = fadeDuration_ / fadeDelay;
		
		if (steps > 0 || null != m_fadeTimer) {
			float increment = (gain_ - timerId_.getGain()) / steps; // approx - timerTask will do extra step if needed.
		
			if (increment != 0.0) { // with float => insignificant fade.
				createOrUpdateFadeTimerTask(gain_, increment, timerId_);
			}
		} else {
			timerId_.setGain(gain_); // duration too small - just set the target gain immediately
		}
		timerId_.setConfiguredGain(gain_); // store configured gain for change tracking.
	}
	
	/**
	 * synchronized function to clean up fade timer and its associated timer task.  Checks
	 * first to verify that the Track's current gain settings have all been adjusted according
	 * to the fade timer tasks own data structures.  If not, then a new fade command must have
	 * just been received and the fade timer has been reconfigured so let it run.
	 */
	synchronized private void destroyFadeTimer() {
		if (m_trackGainFaderTask.allTargetsReached()) {
			// if this is not the case, then another fade command must have just been issued -
			// leave the fadeTimerTask running.
			m_trackGainFaderTask.cancel();
			m_trackGainFaderTask = null;
			m_fadeTimer.cancel();
			m_fadeTimer = null;
		}	
	} // destroyFadeTimer()
	
	/*
	 * D e b u g   d i s p l a y   r o u t i n e s
	 */
	
	/**
	 * Display status of tracks.
	 */
	public void displayAppletState() {
		Iterator tracks_iter = m_tracks.values().iterator();
		while( tracks_iter.hasNext() ) {
			AudioTrack track = (AudioTrack) tracks_iter.next();
			track.displayOnLog();
		}
		
//		tracks_iter = m_tracks.keySet().iterator();
//		while( tracks_iter.hasNext() ) {
//			String key = (String) tracks_iter.next();
//			System.out.println("track map key: " + key);
//		}		
	} // displayAppletState()

	public static void main(String[] args) {
		
		String[] testArray = new String[2];
		testArray[0] = "/~amoshayes/test.au";
		testArray[1] = "/~amoshayes/test2.wav";
		
		
		TrackManager trackManager;
		try {
			trackManager = new TrackManager("http://devel0.gcrc.carleton.ca");
		} catch (MalformedURLException e) {
			// Auto-generated catch block
			e.printStackTrace();
			return;
		}
		trackManager.playSounds(testArray);
	}
}
