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;
}