diff --git a/src/java/com/threerings/media/sound/SoundManager.java b/src/java/com/threerings/media/sound/SoundManager.java index 18ae63af4..13f5452e5 100644 --- a/src/java/com/threerings/media/sound/SoundManager.java +++ b/src/java/com/threerings/media/sound/SoundManager.java @@ -1,13 +1,18 @@ // -// $Id: SoundManager.java,v 1.6 2002/09/30 06:13:36 shaper Exp $ +// $Id: SoundManager.java,v 1.7 2002/11/02 00:26:55 ray Exp $ package com.threerings.media; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; @@ -22,10 +27,12 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; import javax.sound.sampled.UnsupportedAudioFileException; +import javax.swing.Timer; + import org.apache.commons.io.StreamUtils; +import com.samskivert.util.LRUHashMap; import com.samskivert.util.Queue; -import com.samskivert.util.Tuple; import com.threerings.resource.ResourceManager; @@ -34,9 +41,37 @@ import com.threerings.media.Log; /** * Manages the playing of audio files. */ +// TODO: +// - volume for different sound types +// Clip.getControl(FloatControl.Type.VOLUME); +// - either a limit to the queue length, or a way to put items +// in the queue with a timestamp, and if they're processed after that +// time, we cut them out. +// - looping a sound public class SoundManager - implements LineListener { + /** + * Create instances of this for your application to differentiate + * between different types of sounds. + */ + public static class SoundType + { + public SoundType (String description) + { + _desc = description; + } + + public String toString () + { + return _desc; + } + + protected String _desc; + } + + /** The default sound type. */ + public static final SoundType DEFAULT = null; + /** * Constructs a sound manager. */ @@ -45,32 +80,51 @@ public class SoundManager // save things off _rmgr = rmgr; - // obtain the mixer - _mixer = AudioSystem.getMixer(null); - if (_mixer == null) { - Log.warning("Can't get default mixer, aborting audio " + - "initialization."); - return; - } + // enable default sounds + setEnabled(DEFAULT, true); - // create a thread to manage the sound queue +// // obtain the mixer +// _mixer = AudioSystem.getMixer(null); +// if (_mixer == null) { +// Log.warning("Can't get default mixer, aborting audio " + +// "initialization."); +// return; +// } +// + // create a thread to plays sounds and load sound + // data from the resource manager _player = new Thread() { public void run () { while (amRunning()) { - // wait for a sound - _clips.waitForItem(); +// Log.info("Waiting for clip"); - // play the sound - Object o = _clips.get(); + // wait until there is an item to get from the queue + Object o = _queue.get(); + + // play sounds if (o instanceof String) { playSound((String) o); + + // see if we got the flush rsrcs signal + } else if (o == FLUSH) { + flushResources(false); + // and re-start the resource freer timer to do + // it again in 3 seconds + _resourceFreer.start(); + + // see if we got the die signal + } else if (o == DIE) { + _resourceFreer.stop(); + flushResources(true); } } Log.debug("SoundManager exit."); } }; + _player.setDaemon(true); _player.start(); + _resourceFreer.start(); } /** @@ -79,7 +133,10 @@ public class SoundManager public synchronized void shutdown () { _player = null; - _clips.append(this); // signal death + synchronized (_queue) { + _queue.clear(); + _queue.append(DIE); // signal death + } } /** @@ -94,20 +151,32 @@ public class SoundManager /** * Sets whether sound is enabled. */ - public void setEnabled (boolean enabled) + public void setEnabled (SoundType type, boolean enabled) { - _enabled = enabled; + if (enabled) { + _enabled.add(type); + } else { + _enabled.remove(type); + } + } + + /** + * Is the specified soundtype enabled? + */ + public boolean isEnabled (SoundType type) + { + return _enabled.contains(type); } /** * Queues up the sound file with the given pathname to be played when * the sound manager deems the time appropriate. */ - public void play (String path) + public void play (SoundType type, String path) { - if (_player != null && _enabled) { - // Log.debug("Queueing sound [path=" + path + "]."); - _clips.append(path); + if (_player != null && isEnabled(type)) { +// Log.info("Queueing sound [path=" + path + "]."); + _queue.append(path); } } @@ -116,169 +185,240 @@ public class SoundManager */ protected void playSound (String path) { - // Log.debug("Playing sound [path=" + path + "]."); +// Log.info("Playing sound [path=" + path + "]."); - if (!_enabled) { + // see if we're playing the same sound that was just played. + SoundRecord rec = (SoundRecord) _active.get(path); + if (rec != null) { + rec.restart(); return; } + // get the sound data from our LRU cache + byte[] data = getAudioData(path); + if (data == null) { + return; // borked! + } +// Log.info("got data [length=" + data.length + "]."); + try { - // get the audio input stream - Tuple tup = getAudioInfo(path); - AudioFormat format = (AudioFormat)tup.left; - byte[] data = (byte[])tup.right; + AudioInputStream stream = AudioSystem.getAudioInputStream( + new ByteArrayInputStream(data)); + DataLine.Info info = new DataLine.Info( + Clip.class, stream.getFormat()); - // get the audio output line - Clip clip = (Clip)getLine(Clip.class, format, data.length); - if (clip != null) { - // listen to our audio antics - clip.addLineListener(this); + Clip clip = (Clip) AudioSystem.getLine(info); + clip.open(stream); - // and start the clip playing - clip.open(format, data, 0, data.length); - clip.start(); + rec = new SoundRecord(clip); + rec.start(); + _active.put(path, rec); +// Log.info("clip played [path=" + path + "]"); + + } catch (IOException ioe) { + Log.warning("Error loading sound file [path=" + path + + ", e=" + ioe + "]."); + + } catch (UnsupportedAudioFileException uafe) { + Log.warning("Unsupported sound format [path=" + path + ", e=" + + uafe + "]."); + + } catch (LineUnavailableException lue) { + Log.warning("Line not available to play sound [path=" + path + + ", e=" + lue + "]."); + } + } + + /** + * Called occaisionally to flush sound resources. + * + * @param force if true, shutdown and free all sounds, no matter what. + * If false, frees sounds that haven't been used since EXPIRE_TIME + * ago. + */ + protected void flushResources (boolean force) + { + long then = System.currentTimeMillis() - EXPIRE_TIME; + + for (Iterator iter=_active.values().iterator(); iter.hasNext(); ) { + SoundRecord rec = (SoundRecord) iter.next(); + if (force || rec.isStoppedSince(then)) { + iter.remove(); + rec.close(); } - - } catch (LineUnavailableException lue) { - Log.warning("Line unavailable, shutting down [lue=" + lue + "]."); - shutdown(); - - } catch (Exception e) { - Log.warning("Failed to play sound [path=" + path + - ", e=" + e + "]."); - Log.logStackTrace(e); } } /** - * Returns a tuple detailing the audio file format and raw audio data - * for the given sound file, retrieving the relevant information from - * the cache if already present, or loading it into the cache if not. + * Get the audio data for the specified path. */ - protected Tuple getAudioInfo (String path) - throws IOException, UnsupportedAudioFileException + protected byte[] getAudioData (String path) { - // try to retrieve the audio information from the cache - Tuple tup = (Tuple)_data.get(path); - if (tup != null) { - return tup; + byte[] data = (byte[]) _dataCache.get(path); + if (data != null) { + return data; } - // get the audio input stream - InputStream in = _rmgr.getResource(path); - // AudioInputStream ais = AudioSystem.getAudioInputStream(in); - // ais = getPCMAudioStream(ais); - - // AudioFormat format = ais.getFormat(); - - // note that we have to explicitly specify the file format here - // because using AudioSystem.getAudioInputStream() or any of the - // various other similar methods to determine the audio format - // from the file data requires that the supplied input stream - // support mark()/reset(), and though this works happily in the - // local client, it works not at all when running after launching - // from Java Web Start. we'll doubtless revisit this later; at - // the least, to update the audio format to whatever we eventually - // decide upon, and at best, to fix this so that we can properly - // determine the file format and support automagically playing - // different formats. - AudioFormat format = new AudioFormat(8000, 8, 1, false, false); -// Log.debug("Obtained stream [format=" + format + -// ", encoding=" + format.getEncoding() + -// ", rate=" + format.getSampleRate() + -// ", frameSize=" + format.getFrameSize() + -// ", bits=" + format.getSampleSizeInBits() + -// ", channels=" + format.getChannels() + -// ", isBigEndian=" + format.isBigEndian() + "]."); - - // read in all of the data - byte[] data = StreamUtils.streamAsBytes(in, BUFFER_SIZE); - - // cache the data - tup = new Tuple(format, data); - _data.put(path, tup); - return tup; - } - - // documentation inherited - public void update (LineEvent event) - { - // Log.info("LineListener update [event=" + event + "]."); - Clip clip = (Clip)event.getLine(); - if (event.getType() == LineEvent.Type.STOP) { - // shut the clip down -// clip.stop(); -// clip.close(); - - // Log.debug("Finished playing sound [clip=" + clip + "]."); - } - } - - /** - * Returns an audio stream from the given audio stream with the sound - * data converted to a PCM_SIGNED format playable in JDK - * 1.4-beta3. - */ - protected AudioInputStream getPCMAudioStream (AudioInputStream ais) - { - AudioFormat format = ais.getFormat(); - AudioFormat pcmFormat = new AudioFormat( - AudioFormat.Encoding.PCM_SIGNED, - format.getSampleRate(), - format.getSampleSizeInBits() * 2, - format.getChannels(), - format.getFrameSize() * 2, - format.getFrameRate(), - true); - return AudioSystem.getAudioInputStream(pcmFormat, ais); - } - - /** - * Returns a line suitable for playing the audio in the given stream, - * or null if an error occurred. - */ - protected Line getLine (Class clazz, AudioFormat format, int bufferSize) - { - DataLine.Info info = new DataLine.Info(clazz, format, bufferSize); - if (!AudioSystem.isLineSupported(info)) { - Log.warning("Audio line for clip playback not supported " + - "[format=" + format + "]."); - return null; - } - - // Log.info("Checking available lines " + - // "[num=" + _mixer.getMaxLines(info) + "]."); - - // obtain a reference to the line - Line line; try { - line = AudioSystem.getLine(info); - // line.open(format); - } catch (LineUnavailableException lue) { - Log.warning("Failed to open audio line [lue=" + lue + "]."); - return null; + data = StreamUtils.streamAsBytes( + _rmgr.getResource(path), BUFFER_SIZE); + _dataCache.put(path, data); + + } catch (IOException ioe) { + Log.warning("Error loading sound file [path=" + path + ", e=" + + ioe + "]."); } - return line; + + return data; } + /** + * A record to help us manage the use of sound resources. + * We don't free the resources associated with a clip immediately, because + * it may be played again shortly. + */ + protected static class SoundRecord + implements LineListener + { + + /** + * Construct a SoundRecord. + */ + public SoundRecord (Clip clip) + { + _clip = clip; + + _clip.addLineListener(this); + } + + /** + * Start playing the sound. + */ + public void start () + { + _clip.start(); + didStart(); + } + + /** + * Restart the sound from the beginning. + */ + public void restart () + { + if (_clip.isRunning()) { + _clip.stop(); + } + _clip.setFramePosition(0); + _clip.start(); + didStart(); + } + + /** + * Stop playing the sound. + */ + public void stop () + { + _clip.stop(); + didStop(); + } + + /** + * Close down this SoundRecord and free the resources associated + * with the clip. + */ + public void close () + { + _clip.removeLineListener(this); + if (_clip.isRunning()) { + _clip.stop(); + } + _clip.close(); + } + + /** + * Indicate that we've started playback of this sound. + */ + protected void didStart () + { + _stamp = Long.MAX_VALUE; + } + + /** + * Indicate that we've stopped playback. + */ + protected void didStop () + { + _stamp = System.currentTimeMillis(); + } + + /** + * Has this SoundRecord been stopped since before the specified time? + */ + public boolean isStoppedSince (long then) + { + return (_stamp < then); + } + + // documentation inherited from interface LineListener + public void update (LineEvent event) + { + if (event.getType() == LineEvent.Type.STOP) { + didStop(); + } + } + + /** The timestamp of the moment this clip last stopped playing. */ + protected long _stamp; + + /** The clip we're wrapping. */ + protected Clip _clip; + } + + /** + * Every 3 seconds we look for sounds that haven't been used for 4 and + * free them up. + */ + protected Timer _resourceFreer = new Timer(3000, + new ActionListener() { + public void actionPerformed (ActionEvent e) + { + // stop the timer so we don't queue up loads of FLUSH + // requests when the main thread is bogged down. + _resourceFreer.stop(); + + // and request a sweep by the main thread + _queue.append(FLUSH); + } + }); + /** The resource manager from which we obtain audio files. */ protected ResourceManager _rmgr; - /** The default mixer. */ - protected Mixer _mixer; - +// /** The default mixer. */ +// protected Mixer _mixer; +// /** The thread that plays sounds. */ protected Thread _player; /** The queue of sound clips to be played. */ - protected Queue _clips = new Queue(); + protected Queue _queue = new Queue(); + + /** The cache of recent audio clips . */ + protected LRUHashMap _dataCache = new LRUHashMap(10); - /** The cached audio file data. */ - protected HashMap _data = new HashMap(); + /** The clips that are currently active. */ + protected HashMap _active = new HashMap(); - /** Whether sound is enabled. */ - protected boolean _enabled = true; + /** A set of soundTypes for which sound is enabled. */ + protected HashSet _enabled = new HashSet(); + + /** Signals to the queue to do different things. */ + protected Object FLUSH = new Object(), DIE = new Object(); /** The buffer size in bytes used when reading audio file data. */ protected static final int BUFFER_SIZE = 2048; + + /** How long a clip may linger after stopping before we clear + * its resources. */ + protected static final long EXPIRE_TIME = 4000L; }