/** SoundPlayer class
  * @author Martin Stepp (stepp),
  *         University of Arizona graduate student/teacher's assistant
  * @version November 24, 2001
  */

import java.io.*;
import java.util.*;
import javax.sound.sampled.*;
import javax.swing.*;


/** A class to play audio clips.
  * Caches previously-played clips in a hash table,
  * allowing fast playback of previously seen sounds.
  */
public class SoundPlayer implements LineListener {
	private Hashtable myClips = new Hashtable();

	/** Plays the audio file with the given file name.
	  * This function returns instantly, without waiting for
	  * the clip to finish playing.
	  * @return the Clip played (for convenience; this result need not be used/stored).
	  * @throws RuntimeException on an audio or I/O error.
	  */
	public void play(String fileName) {
		Clip clip = getClip(fileName);
		if (clip != null)
			clip.start();
	}
	
	/** Plays the audio file with the given file name,
	  * waiting until the clip is done playing before
	  * returning from the method.
	  */
	public void playAndWait(String fileName) {
		Clip clip = getClip(fileName);
		if (clip != null) {
			clip.start();
			try {
				synchronized (clip) {
					clip.wait();  // sleep until notified
				}
			} catch (InterruptedException ie) {}
		}
	}
	
	/** Plays the clip at the given file name in a loop.
	  * The clip keeps looping until it is later stopped by
	  * calling the stop() method.
	  * This function returns instantly, without waiting for
	  * the clip to finish looping.
	  */
	public void loop(String fileName) {
		loop(fileName, Clip.LOOP_CONTINUOUSLY);
	}
	
	/** Plays the clip at the given file name in a loop.
	  * The clip loops until it has played the given number of times, or 
	  * until it is later stopped by calling the stop() method.
	  * This function returns instantly, without waiting for
	  * the clip to finish looping.
	  */
	public void loop(String fileName, int numTimes) {
		Clip clip = getClip(fileName);
		if (clip != null) {
			clip.loop(numTimes);
		}
	}
	
	/** Plays the clip at the given file name in a loop.
	  * The clip loops until it has played the given number of times, or 
	  * until it is later stopped by calling the stop() method.
	  * This method waits until the clip is done looping before
	  * returning.  
	  *
	  * Note that since the clip loops continuously,
	  * this method will not return unless some other thread
	  * later stops the clip.
	  */
	public void loopAndWait(String fileName) {
		loopAndWait(fileName, Clip.LOOP_CONTINUOUSLY);
	}
	
	/** Plays the clip at the given file name in a loop.
	  * The clip loops until it has played the given number of times, or 
	  * until it is later stopped by calling the stop() method.
	  *
	  * Note that this method will not return until the clip is
	  * done with all of its loops, or until some other thread
	  * stops the clip.
	  */
	public void loopAndWait(String fileName, int numTimes) {
		Clip clip = getClip(fileName);
		if (clip != null) {
			clip.loop(numTimes);
			try {
				clip.wait();  // sleep until notified
			} catch (InterruptedException ie) {}
		}
	}
	
	/** Pauses the clip at the given file name.
	  * If the clip is later played, it will play from the
	  * time at which it was paused.
	  *
	  * Calling this method does not awaken a previous call
	  * to playAndWait or loopAndWait; the clip must be stopped
	  * or finish playing/looping before this will happen.
	  *
	  * If a paused clip has stop() called on it, it will reset
	  * to the start of the clip upon the next play.
	  */
	public void pause(String fileName) {
		Clip clip = getClip(fileName);
		if (clip != null) {
			int pos = clip.getFramePosition();
			clip.stop();
			clip.setFramePosition(pos);
		}
	}
	
	/** Instructs the clip at the given file name to stop playing.
	  * Also awakens anyone doing a waiting play on this Clip.
	  */
	public void stop(String fileName) {
		Clip clip = getClip(fileName);
		stopClip(clip);
	}
	
	/** Stops all currently playing sound clips. */
	public void stopAll() {
		Enumeration clips = myClips.elements();
		while (clips.hasMoreElements()) {
			Clip clip = (Clip)clips.nextElement();
			stopClip(clip);
		}
	}	

	/** Pre-load the clip at the given file name into this
	  * sound player's hash table.  This means the clip will
	  * be loaded quickly later.
	  */
	public void preLoad(String fileName) {
		getClip(fileName);
	}
	
	/** Returns whether or not this sound player contains a clip
	  * with the given file name in its cache.
	  */
	public boolean contains(String fileName) {
		return myClips.containsKey(fileName);
	}
	
	/** Clears this sound player's clip cache. */
	public void clear() {
		myClips.clear();
	}
	
	/** Responds to audio events generated by playing clips. */
	public void update(LineEvent le) {
		if (le.getType() == LineEvent.Type.STOP) {
			// clip is done playing
			stopClip((Clip)le.getSource());
		}
	}


	/* Return an audio Clip by either creating one or looking
	 * one up in this player's hash table.
	 */
	private Clip getClip(String fileName) {
		File f = new File(fileName);
		Clip clip = null;
		AudioInputStream ais = null;
		
		if (myClips.containsKey(fileName)) {
			clip = (Clip)myClips.get(fileName);
		} else {
			// read audio file from disk
			try {
				ais = AudioSystem.getAudioInputStream(f);
				clip = (Clip)AudioSystem.getLine(new Line.Info(Clip.class));
				clip.open(ais);
				clip.addLineListener(this);
				myClips.put(fileName, clip);
			} catch (UnsupportedAudioFileException uafe) {
				throw new RuntimeException("Not a valid supported audio file: \"" + fileName + "\"");
			} catch (LineUnavailableException lue) {
				throw new RuntimeException("Line is not available to play sound \"" + fileName + "\"");
			} catch (IOException ioe) {
				throw new RuntimeException("I/O error while reading file: \"" + fileName + "\"");
			}
		}
		
		return clip;
	}
	
	/* Stops the given audio Clip from playing. */
	private void stopClip(Clip clip) {
		if (clip != null) {
			clip.stop();
			clip.setFramePosition(0);
			synchronized (clip) {
				clip.notifyAll();  // awaken anyone waiting for this Clip
			}
		}
	}
}
