Files
narya/src/java/com/threerings/media/sound/SoundManager.java
T
Ray Greenwell 271d0ffb85 a timorous transition from listening to stop events to just calculating
the time at which the sound will stop. Stop events are flawed.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1886 542714f4-19e9-0310-aa3c-eee0fc999fb1
2002-11-02 01:49:20 +00:00

482 lines
14 KiB
Java

//
// $Id: SoundManager.java,v 1.8 2002/11/02 01:49:20 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;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine.Info;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
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.threerings.resource.ResourceManager;
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
{
/**
* 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.
*/
public SoundManager (ResourceManager rmgr)
{
// save things off
_rmgr = rmgr;
// enable default sounds
setEnabled(DEFAULT, true);
// // 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()) {
// Log.info("Waiting for clip");
// 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();
}
/**
* Shut the damn thing off.
*/
public synchronized void shutdown ()
{
_player = null;
synchronized (_queue) {
_queue.clear();
_queue.append(DIE); // signal death
}
}
/**
* Used by the sound playing thread to determine whether or not to
* shut down.
*/
protected synchronized boolean amRunning ()
{
return (_player == Thread.currentThread());
}
/**
* Sets whether sound is enabled.
*/
public void setEnabled (SoundType type, boolean 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 (SoundType type, String path)
{
if (_player != null && isEnabled(type)) {
// Log.info("Queueing sound [path=" + path + "].");
_queue.append(path);
}
}
/**
* Plays the sound file with the given pathname.
*/
protected void playSound (String path)
{
// Log.info("Playing sound [path=" + path + "].");
// 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 {
AudioInputStream stream = AudioSystem.getAudioInputStream(
new ByteArrayInputStream(data));
DataLine.Info info = new DataLine.Info(
Clip.class, stream.getFormat());
Clip clip = (Clip) AudioSystem.getLine(info);
clip.open(stream);
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();
}
}
}
/**
* Get the audio data for the specified path.
*/
protected byte[] getAudioData (String path)
{
byte[] data = (byte[]) _dataCache.get(path);
if (data != null) {
return data;
}
try {
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 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;
// The mess:
// If we restart a sample, we get two stop events, one immediately
// and one later on.
// We can't just ignore the first in that case, because once
// a sample is restarted it seems to not return true
// for isRunning() (maybe because it's already been reset?)
// but will continue to generate stop events each time it
// is started.
//
// So- we can't depend on capturing stop events to know
// when a sound is done playing. Instead we just add the length
// of the sound to the current time. We fall back on the
// LineListener method if the clip length is unavailable.
//
// The good news is that just adding the length is in many
// ways a better system. I don't feel confident in doing
// that right now because I don't know if any sounds
// will ever return NOT_SPECIFIED.
long length = _clip.getMicrosecondLength();
if (length == AudioSystem.NOT_SPECIFIED) {
Log.info("Length of AudioClip not specified, falling back " +
"to listening for stop events.");
_clip.addLineListener(this);
_length = AudioSystem.NOT_SPECIFIED;
} else {
// convert microseconds to milliseconds, round up.
_length = (length / 1000L) + 1;
}
}
/**
* Start playing the sound.
*/
public void start ()
{
_clip.start();
didStart();
}
/**
* Restart the sound from the beginning.
*/
public void restart ()
{
// this seems to be unneeded
// if (_clip.isRunning()) {
// Log.info("Restarted a sound and sent it a stop..");
// _clip.stop();
// }
_clip.setFramePosition(0);
start();
}
/**
* 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 ()
{
if (areListening()) {
_clip.removeLineListener(this);
}
if (_clip.isRunning()) {
_clip.stop();
}
_clip.close();
// make sure nobody uses this SoundRecord again
_clip = null;
}
/**
* Indicate that we've started playback of this sound.
*/
protected void didStart ()
{
if (areListening()) {
// clear out the stamp so that we can't possibly be reaped
// and since we're listening, we'll eventually call didStop()
_stamp = Long.MAX_VALUE;
} else {
_stamp = System.currentTimeMillis() + _length;
}
}
/**
* Indicate that we've stopped playback.
*/
protected void didStop ()
{
// no matter what, when we stop, we stop
_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)
{
// this only gets run if we actually need to listen for the
// stop events.
if (event.getType() == LineEvent.Type.STOP) {
didStop();
}
}
/**
* Are we registered as a LineListener on our clip?
*/
private final boolean areListening ()
{
return _length == AudioSystem.NOT_SPECIFIED;
}
/** The timestamp of the moment this clip last stopped playing. */
protected long _stamp;
/** The length of the clip, in milliseconds, or
* AudioSystem.NOT_SPECIFIED if unknown. */
protected long _length;
/** 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 thread that plays sounds. */
protected Thread _player;
/** The queue of sound clips to be played. */
protected Queue _queue = new Queue();
/** The cache of recent audio clips . */
protected LRUHashMap _dataCache = new LRUHashMap(10);
/** The clips that are currently active. */
protected HashMap _active = new HashMap();
/** 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;
}