don't play sounds with clips, instead have threads that spool sound

data to open lines. Joy.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1986 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Ray Greenwell
2002-11-23 02:09:36 +00:00
parent 2c681d7291
commit ef2c1ccd8b
@@ -1,5 +1,5 @@
// //
// $Id: SoundManager.java,v 1.25 2002/11/22 19:21:12 ray Exp $ // $Id: SoundManager.java,v 1.26 2002/11/23 02:09:36 ray Exp $
package com.threerings.media; package com.threerings.media;
@@ -30,6 +30,7 @@ import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer;
import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.Timer; import javax.swing.Timer;
@@ -134,18 +135,11 @@ public class SoundManager
} else if (UNLOCK == command) { } else if (UNLOCK == command) {
_clipCache.unlock(key); _clipCache.unlock(key);
} else if (FLUSH == command) {
flushResources(false);
// and re-start the resource freer timer
// to do it again in 3 seconds
_resourceFreer.restart();
} else if (UPDATE_MUSIC_VOL == command) { } else if (UPDATE_MUSIC_VOL == command) {
updateMusicVolume(); updateMusicVolume();
} else if (DIE == command) { } else if (DIE == command) {
_resourceFreer.stop(); // TODO: clean up more stuff.
flushResources(true);
shutdownMusic(); shutdownMusic();
} }
} catch (Exception e) { } catch (Exception e) {
@@ -159,9 +153,6 @@ public class SoundManager
_player.setDaemon(true); _player.setDaemon(true);
_player.start(); _player.start();
_resourceFreer.setRepeats(false);
_resourceFreer.start();
} }
/** /**
@@ -329,25 +320,17 @@ public class SoundManager
*/ */
protected void playSound (SoundKey key) protected void playSound (SoundKey key)
{ {
// see if we can restart a previously used sound that's still
// hanging out.
if (restartSound(key)) {
return;
}
try { try {
// get the sound data from our LRU cache // get the sound data from our LRU cache
ClipInfo info = getClipData(key); byte[] data = getClipData(key);
if (info == null) { if (data == null) {
return; // borked! return; // borked!
} }
Clip clip = (Clip) AudioSystem.getLine(info.info); AudioInputStream stream = AudioSystem.getAudioInputStream(
clip.open(info.stream); new ByteArrayInputStream(data));
SoundRecord rec = new SoundRecord(key, clip); LineSpooler.play(stream, _clipVol);
rec.start(_clipVol);
_activeClips.add(rec);
} catch (IOException ioe) { } catch (IOException ioe) {
Log.warning("Error loading sound file [key=" + key + Log.warning("Error loading sound file [key=" + key +
@@ -363,31 +346,11 @@ public class SoundManager
} }
} }
/**
* Attempt to reuse a clip that's already been loaded.
*/
protected boolean restartSound (SoundKey key)
{
long now = System.currentTimeMillis();
// we just go through all the sounds. There'll be 32 max, so fuckit.
for (int ii=0, nn=_activeClips.size(); ii < nn; ii++) {
SoundRecord rec = (SoundRecord) _activeClips.get(ii);
if (rec.key.equals(key) && rec.isStoppedSince(now)) {
rec.restart(_clipVol);
return true;
}
}
return false;
}
/** /**
* Play a song from the specified path. * Play a song from the specified path.
*/ */
protected void playMusic (MusicInfo info) protected void playMusic (MusicInfo info)
{ {
Log.info("Playing: " + info);
// stop whatever's currently playing // stop whatever's currently playing
if (_musicPlayer != null) { if (_musicPlayer != null) {
_musicPlayer.stop(); _musicPlayer.stop();
@@ -502,14 +465,11 @@ public class SoundManager
*/ */
protected void stopMusic (SoundKey key) protected void stopMusic (SoundKey key)
{ {
Log.info("Stopping: " + key);
if (! _musicStack.isEmpty()) { if (! _musicStack.isEmpty()) {
MusicInfo current = (MusicInfo) _musicStack.getFirst(); MusicInfo current = (MusicInfo) _musicStack.getFirst();
// if we're currently playing this song.. // if we're currently playing this song..
if (key.equals(current)) { if (key.equals(current)) {
Log.info("is top song!");
// stop it // stop it
_musicPlayer.stop(); _musicPlayer.stop();
@@ -517,6 +477,7 @@ public class SoundManager
_musicStack.removeFirst(); _musicStack.removeFirst();
// start playing the next.. // start playing the next..
playTopMusic(); playTopMusic();
return;
} else { } else {
// we aren't currently playing this song. Simply remove. // we aren't currently playing this song. Simply remove.
@@ -559,49 +520,20 @@ public class SoundManager
} }
} }
/**
* 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=_activeClips.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. * Get the audio data for the specified path.
*/ */
protected ClipInfo getClipData (SoundKey key) protected byte[] getClipData (SoundKey key)
throws IOException, UnsupportedAudioFileException throws IOException, UnsupportedAudioFileException
{ {
ClipInfo info = (ClipInfo) _clipCache.get(key); byte[] data = (byte[]) _clipCache.get(key);
if (info != null) { if (data == null) {
// we are re-using an old stream, make sure to rewind it. data = StreamUtils.streamAsBytes(
info.stream.reset(); _rmgr.getResource(key.set, key.path), BUFFER_SIZE);
_clipCache.put(key, data);
} else {
// set it up and put it in the cache
AudioInputStream stream = AudioSystem.getAudioInputStream(
new ByteArrayInputStream(StreamUtils.streamAsBytes(
_rmgr.getResource(key.set, key.path), BUFFER_SIZE)));
DataLine.Info dinfo = new DataLine.Info(
Clip.class, stream.getFormat());
info = new ClipInfo(dinfo, stream);
_clipCache.put(key, info);
} }
return info; return data;
} }
// /** // /**
@@ -651,162 +583,187 @@ public class SoundManager
} }
/** /**
* A record to help us manage the use of sound resources. * Handles the playing of sound clip data.
* We don't free the resources associated with a clip immediately, because
* it may be played again shortly.
*/ */
protected static class SoundRecord protected static class LineSpooler extends Thread
implements LineListener
{ {
public SoundKey key;
/** /**
* Construct a SoundRecord. * Attempt to play the specified sound.
*/ */
public SoundRecord (SoundKey key, Clip clip) public static void play (AudioInputStream stream, float volume)
throws LineUnavailableException
{ {
this.key = key; AudioFormat format = stream.getFormat();
_clip = clip; LineSpooler spooler;
// The mess: for (int ii=0, nn=_available.size(); ii < nn; ii++) {
// If we restart a sample, we get two stop events, one immediately spooler = (LineSpooler) _available.get(ii);
// 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(); // we have this thread remove the spooler if it's dead
if (length == AudioSystem.NOT_SPECIFIED) { // so that we avoid deadlock conditions
Log.info("Length of AudioClip not specified, falling back " + if (spooler.isDead()) {
"to listening for stop events."); _available.remove(ii--);
_clip.addLineListener(this); nn--;
_length = AudioSystem.NOT_SPECIFIED;
} else { } else if (spooler.checkPlay(format, stream, volume)) {
// convert microseconds to milliseconds, round up. return;
_length = (length / 1000L) + 1; }
} }
}
/** if (_available.size() >= MAX_SPOOLERS) {
* Start playing the sound. throw new LineUnavailableException("Exceeded maximum number " +
*/ "of narya sound spoolers.");
public void start (float volume)
{
adjustVolume(_clip, volume);
_clip.start();
didStart();
}
/**
* Restart the sound from the beginning.
*/
public void restart (float volume)
{
// this only gets called after the sound has stopped, so
// simply rewind and replay.
_clip.setFramePosition(0);
start(volume);
}
/**
* 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(); spooler = new LineSpooler(format);
_available.add(spooler);
spooler.checkPlay(format, stream, volume);
spooler.start();
}
/**
* Private constructor.
*/
private LineSpooler (AudioFormat format)
throws LineUnavailableException
{
super("narya SoundManager LineSpooler");
_format = format;
_line = (SourceDataLine) AudioSystem.getLine(new DataLine.Info(
SourceDataLine.class, _format));
_line.open(_format);
_line.start();
}
/**
* Has this line been closed?
*/
protected boolean isDead ()
{
return !_valid;
}
/**
* Check-and-set method to see if we can play and to start
* doing so if we can.
*/
protected synchronized boolean checkPlay (
AudioFormat format, AudioInputStream stream, float volume)
{
if (_valid && (_stream == null) && _format.matches(format)) {
_stream = stream;
SoundManager.adjustVolume(_line, volume);
notify();
return true;
} }
_clip.close();
// make sure nobody uses this SoundRecord again return false;
_clip = null;
} }
/** /**
* Indicate that we've started playback of this sound. * @return true if we have sound data to play.
*/ */
protected void didStart () protected synchronized boolean waitForData ()
{ {
if (areListening()) { if (_stream == null) {
// clear out the stamp so that we can't possibly be reaped try {
// and since we're listening, we'll eventually call didStop() wait(MAX_WAIT_TIME);
_stamp = Long.MAX_VALUE; } catch (InterruptedException ie) {
// ignore.
} else { }
_stamp = System.currentTimeMillis() + _length; if (_stream == null) {
_valid = false;
return false;
}
} }
return true;
} }
/** /**
* Indicate that we've stopped playback. * Main loop for the LineSpooler.
*/ */
protected void didStop () public void run ()
{ {
// no matter what, when we stop, we stop while (waitForData()) {
_stamp = System.currentTimeMillis(); playStream();
}
/**
* 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();
} }
_line.close();
} }
/** /**
* Are we registered as a LineListener on our clip? * Play the current stream.
*/ */
private final boolean areListening () protected void playStream ()
{ {
return _length == AudioSystem.NOT_SPECIFIED; int count = 0;
byte[] data = new byte[BUFFER_SIZE];
while (count != -1) {
try {
count = _stream.read(data, 0, data.length);
} catch (IOException e) {
// this shouldn't ever ever happen
Log.warning("Error reading clip data! [e=" + e + "].");
_stream = null;
return;
}
if (count >= 0) {
_line.write(data, 0, count);
}
}
if (_line.isActive()) {
// Log.info("Waiting for drain (" + hashCode() + ", active=" +
// _line.isActive() + ", running=" + _line.isRunning()+
// ") : " + incDrainers());
// wait for it to play all the way
_line.drain();
// Log.info("drained: (" + hashCode() + ") :" + decDrainers());
}
// clear it out so that we can wait for more.
_stream = null;
} }
/** The timestamp of the moment this clip last stopped playing. */ // protected static final synchronized int incDrainers ()
protected long _stamp; // {
// return ++drainers;
// }
//
// protected static final synchronized int decDrainers ()
// {
// return --drainers;
// }
// static int drainers = 0;
/** The length of the clip, in milliseconds, or /** The format that our line was opened with. */
* AudioSystem.NOT_SPECIFIED if unknown. */ protected AudioFormat _format;
protected long _length;
/** The clip we're wrapping. */ /** The stream we're currently spooling out. */
protected Clip _clip; protected AudioInputStream _stream;
/** The line that we spool to. */
protected SourceDataLine _line;
/** Are we still active and usable for spooling sounds, or should
* we be removed. */
protected boolean _valid = true;
/** The list of all the currently instantiated spoolers. */
protected static ArrayList _available = new ArrayList();
/** The maximum time a spooler will wait for a stream before
* deciding to shut down. */
protected static final long MAX_WAIT_TIME = 30000L;
/** The maximum number of spoolers we'll allow. This is a lot. */
protected static final int MAX_SPOOLERS = 24;
} }
/** /**
@@ -848,24 +805,6 @@ public class SoundManager
} }
} }
/**
* A wee helper class that holds clip information in our cache.
*/
protected static class ClipInfo
{
/** The information needed to construct a clip from the stream. */
public DataLine.Info info;
/** The data to be used to make the clip. */
public AudioInputStream stream;
public ClipInfo (DataLine.Info info, AudioInputStream stream)
{
this.info = info;
this.stream = stream;
}
}
/** /**
* A class that tracks the information about our playing music files. * A class that tracks the information about our playing music files.
*/ */
@@ -881,19 +820,6 @@ public class SoundManager
} }
} }
/**
* 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)
{
// request a sweep by the main thread
_queue.append(FLUSH);
}
});
/** The resource manager from which we obtain audio files. */ /** The resource manager from which we obtain audio files. */
protected ResourceManager _rmgr; protected ResourceManager _rmgr;
@@ -928,16 +854,11 @@ public class SoundManager
protected Object UPDATE_MUSIC_VOL = new Object(); protected Object UPDATE_MUSIC_VOL = new Object();
protected Object LOCK = new Object(); protected Object LOCK = new Object();
protected Object UNLOCK = new Object(); protected Object UNLOCK = new Object();
protected Object FLUSH = new Object();
protected Object DIE = new Object(); protected Object DIE = new Object();
/** The queue size at which we start to ignore requests to play sounds. */ /** The queue size at which we start to ignore requests to play sounds. */
protected static final int MAX_QUEUE_SIZE = 100; protected static final int MAX_QUEUE_SIZE = 25;
/** The buffer size in bytes used when reading audio file data. */ /** The buffer size in bytes used when reading audio file data. */
protected static final int BUFFER_SIZE = 2048; protected static final int BUFFER_SIZE = 8192;
/** How long a clip may linger after stopping before we clear
* its resources. */
protected static final long EXPIRE_TIME = 4000L;
} }