1b9c9dac62
when I set it to 24? Let's try it like this for a spell. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2734 542714f4-19e9-0310-aa3c-eee0fc999fb1
1258 lines
38 KiB
Java
1258 lines
38 KiB
Java
//
|
|
// $Id: SoundManager.java,v 1.65 2003/07/28 05:32:04 ray Exp $
|
|
|
|
package com.threerings.media.sound;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FilenameFilter;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
|
|
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.FloatControl;
|
|
import javax.sound.sampled.FloatControl.Type;
|
|
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.SourceDataLine;
|
|
import javax.sound.sampled.UnsupportedAudioFileException;
|
|
|
|
import org.apache.commons.io.StreamUtils;
|
|
import org.apache.commons.lang.Constant;
|
|
|
|
import com.samskivert.util.Config;
|
|
import com.samskivert.util.LRUHashMap;
|
|
import com.samskivert.util.Queue;
|
|
import com.samskivert.util.RuntimeAdjust;
|
|
import com.samskivert.util.RunAnywhere;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import com.threerings.resource.ResourceManager;
|
|
import com.threerings.util.RandomUtil;
|
|
|
|
import com.threerings.media.Log;
|
|
import com.threerings.media.MediaPrefs;
|
|
|
|
/**
|
|
* Manages the playing of audio files.
|
|
*/
|
|
// TODO:
|
|
// - fade music out when stopped?
|
|
// - be able to pause music
|
|
public class SoundManager
|
|
implements MusicPlayer.MusicEventListener
|
|
{
|
|
/**
|
|
* Create instances of this for your application to differentiate
|
|
* between different types of sounds.
|
|
*/
|
|
public static class SoundType
|
|
{
|
|
/**
|
|
* Construct a new SoundType.
|
|
* Which should be a static variable stashed somewhere for the
|
|
* entire application to share.
|
|
*
|
|
* @param strname a short string identifier, preferably without spaces.
|
|
*/
|
|
public SoundType (String strname)
|
|
{
|
|
_strname = strname;
|
|
}
|
|
|
|
public String toString ()
|
|
{
|
|
return _strname;
|
|
}
|
|
|
|
protected String _strname;
|
|
}
|
|
|
|
/** The default sound type. */
|
|
public static final SoundType DEFAULT = new SoundType("default");
|
|
|
|
/**
|
|
* Constructs a sound manager.
|
|
*
|
|
* @param defaultClipBundle
|
|
* @param defaultClipPath The pathname of a sound clip to use as a
|
|
* fallback if another sound clip cannot be located.
|
|
*/
|
|
public SoundManager (
|
|
ResourceManager rmgr, String defaultClipBundle, String defaultClipPath)
|
|
{
|
|
// save things off
|
|
_rmgr = rmgr;
|
|
_defaultClipBundle = defaultClipBundle;
|
|
_defaultClipPath = defaultClipPath;
|
|
|
|
if (!SOUND_ENABLED) return;
|
|
|
|
// create a thread to plays sounds and load sound
|
|
// data from the resource manager
|
|
_player = new Thread("narya SoundManager") {
|
|
public void run () {
|
|
Object command = null;
|
|
SoundKey key = null;
|
|
MusicInfo musicInfo = null;
|
|
|
|
while (amRunning()) {
|
|
// Get the next command and arguments
|
|
synchronized (_queue) {
|
|
command = _queue.get();
|
|
|
|
// some commands have an additional argument.
|
|
if ((PLAY == command) ||
|
|
(STOPMUSIC == command) ||
|
|
(LOCK == command) ||
|
|
(UNLOCK == command)) {
|
|
key = (SoundKey) _queue.get();
|
|
|
|
} else if (PLAYMUSIC == command) {
|
|
musicInfo = (MusicInfo) _queue.get();
|
|
}
|
|
}
|
|
|
|
try {
|
|
// execute the command outside of the queue synch
|
|
if (PLAY == command) {
|
|
playSound(key);
|
|
|
|
} else if (PLAYMUSIC == command) {
|
|
playMusic(musicInfo);
|
|
|
|
} else if (STOPMUSIC == command) {
|
|
stopMusic(key);
|
|
|
|
} else if (LOCK == command) {
|
|
if (!isTesting()) {
|
|
try {
|
|
getClipData(key); // preload
|
|
// copy the cached sound into the lock map
|
|
_lockedClips.put(key, _clipCache.get(key));
|
|
} catch (Exception e) {
|
|
// don't whine about LOCK failures
|
|
// unless we have verbose logging on.
|
|
if (_verbose.getValue()) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if (UNLOCK == command) {
|
|
_lockedClips.remove(key);
|
|
|
|
} else if (UPDATE_MUSIC_VOL == command) {
|
|
updateMusicVolume();
|
|
|
|
} else if (DIE == command) {
|
|
LineSpooler.shutdown();
|
|
shutdownMusic();
|
|
_player = null;
|
|
|
|
} else {
|
|
Log.warning("Got unknown command [cmd=" + command +
|
|
", key=" + key +
|
|
", info=" + musicInfo + "].");
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.warning("SoundManager failure [cmd=" + command +
|
|
", key=" + key +
|
|
", info=" + musicInfo + "].");
|
|
Log.logStackTrace(e);
|
|
}
|
|
}
|
|
Log.debug("SoundManager exit.");
|
|
}
|
|
};
|
|
|
|
_player.start();
|
|
}
|
|
|
|
/**
|
|
* Shut the damn thing off.
|
|
*/
|
|
public void shutdown ()
|
|
{
|
|
synchronized (_queue) {
|
|
_queue.clear();
|
|
_queue.append(DIE); // signal death
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used by the sound playing thread to determine whether or not to
|
|
* shut down.
|
|
*/
|
|
protected boolean amRunning ()
|
|
{
|
|
return (_player == Thread.currentThread());
|
|
}
|
|
|
|
/**
|
|
* Returns a string summarizing our volume settings and disabled sound
|
|
* types.
|
|
*/
|
|
public String summarizeState ()
|
|
{
|
|
StringBuffer buf = new StringBuffer();
|
|
buf.append("musicVol=").append(_musicVol);
|
|
buf.append(", clipVol=").append(_clipVol);
|
|
buf.append(", disabled=[");
|
|
int ii = 0;
|
|
for (Iterator iter = _disabledTypes.iterator(); iter.hasNext(); ) {
|
|
if (ii++ > 0) {
|
|
buf.append(", ");
|
|
}
|
|
buf.append(iter.next());
|
|
}
|
|
return buf.append("]").toString();
|
|
}
|
|
|
|
/**
|
|
* Is the specified soundtype enabled?
|
|
*/
|
|
public boolean isEnabled (SoundType type)
|
|
{
|
|
// by default, types are enabled..
|
|
return (!_disabledTypes.contains(type));
|
|
}
|
|
|
|
/**
|
|
* Turns on or off the specified sound type.
|
|
*/
|
|
public void setEnabled (SoundType type, boolean enabled)
|
|
{
|
|
if (enabled) {
|
|
_disabledTypes.remove(type);
|
|
} else {
|
|
_disabledTypes.add(type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the volume for all sound clips.
|
|
*
|
|
* @param val a volume parameter between 0f and 1f, inclusive.
|
|
*/
|
|
public void setClipVolume (float vol)
|
|
{
|
|
_clipVol = Math.max(0f, Math.min(1f, vol));
|
|
}
|
|
|
|
/**
|
|
* Get the volume for all sound clips.
|
|
*/
|
|
public float getClipVolume ()
|
|
{
|
|
return _clipVol;
|
|
}
|
|
|
|
/**
|
|
* Sets the volume for music.
|
|
*
|
|
* @param val a volume parameter between 0f and 1f, inclusive.
|
|
*/
|
|
public void setMusicVolume (float vol)
|
|
{
|
|
float oldvol = _musicVol;
|
|
_musicVol = Math.max(0f, Math.min(1f, vol));
|
|
|
|
if ((oldvol == 0f) && (_musicVol != 0f)) {
|
|
_musicAction = START;
|
|
} else if ((oldvol != 0f) && (_musicVol == 0f)) {
|
|
_musicAction = STOP;
|
|
} else {
|
|
_musicAction = NONE;
|
|
}
|
|
_queue.append(UPDATE_MUSIC_VOL);
|
|
}
|
|
|
|
/**
|
|
* Get the music volume.
|
|
*/
|
|
public float getMusicVolume ()
|
|
{
|
|
return _musicVol;
|
|
}
|
|
|
|
/**
|
|
* Optionally lock the sound data prior to playing, to guarantee
|
|
* that it will be quickly available for playing.
|
|
*/
|
|
public void lock (String pkgPath, String key)
|
|
{
|
|
if (!SOUND_ENABLED) return;
|
|
|
|
synchronized (_queue) {
|
|
_queue.append(LOCK);
|
|
_queue.append(new SoundKey(pkgPath, key));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlock the specified sound so that its resources can be freed.
|
|
*/
|
|
public void unlock (String pkgPath, String key)
|
|
{
|
|
if (!SOUND_ENABLED) return;
|
|
|
|
synchronized (_queue) {
|
|
_queue.append(UNLOCK);
|
|
_queue.append(new SoundKey(pkgPath, key));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Batch lock a list of sounds.
|
|
*/
|
|
public void lock (String pkgPath, String[] keys)
|
|
{
|
|
for (int ii=0; ii < keys.length; ii++) {
|
|
lock(pkgPath, keys[ii]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Batch unlock a list of sounds.
|
|
*/
|
|
public void unlock (String pkgPath, String[] keys)
|
|
{
|
|
for (int ii=0; ii < keys.length; ii++) {
|
|
unlock(pkgPath, keys[ii]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play the specified sound of as the specified type of sound.
|
|
* Note that a sound need not be locked prior to playing.
|
|
*/
|
|
public void play (SoundType type, String pkgPath, String key)
|
|
{
|
|
if (!SOUND_ENABLED) return;
|
|
|
|
if (type == null) {
|
|
// let the lazy kids play too
|
|
type = DEFAULT;
|
|
}
|
|
|
|
if (_player != null && (_clipVol != 0f) && isEnabled(type)) {
|
|
SoundKey skey = new SoundKey(pkgPath, key);
|
|
synchronized (_queue) {
|
|
if (_queue.size() < MAX_QUEUE_SIZE) {
|
|
/*
|
|
if (_verbose.getValue()) {
|
|
Log.info("Sound request [key=" + skey + "].");
|
|
}
|
|
*/
|
|
_queue.append(PLAY);
|
|
_queue.append(skey);
|
|
|
|
} else if (_verbose.getValue()) {
|
|
Log.warning("SoundManager not playing sound because " +
|
|
"too many sounds in queue [key=" + skey +
|
|
", type=" + type + ", queue=" + _queue + "].");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start playing the specified music repeatedly.
|
|
*/
|
|
public void pushMusic (String pkgPath, String key)
|
|
{
|
|
pushMusic(pkgPath, key, -1);
|
|
}
|
|
|
|
/**
|
|
* Start playing music for the specified number of loops.
|
|
*/
|
|
public void pushMusic (String pkgPath, String key, int numloops)
|
|
{
|
|
if (!SOUND_ENABLED) {
|
|
return;
|
|
}
|
|
|
|
synchronized (_queue) {
|
|
_queue.append(PLAYMUSIC);
|
|
_queue.append(new MusicInfo(pkgPath, key, numloops));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the specified music from the playlist. If it is currently
|
|
* playing, it will be stopped and the previous song will be started.
|
|
*/
|
|
public void removeMusic (String pkgPath, String key)
|
|
{
|
|
synchronized (_queue) {
|
|
_queue.append(STOPMUSIC);
|
|
_queue.append(new SoundKey(pkgPath, key));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On the SoundManager thread,
|
|
* plays the sound file with the given pathname.
|
|
*/
|
|
protected void playSound (SoundKey key)
|
|
{
|
|
if (key.isExpired()) {
|
|
return;
|
|
}
|
|
try {
|
|
// get the sound data from our LRU cache
|
|
byte[] data = getClipData(key);
|
|
if (data == null) {
|
|
return; // borked!
|
|
}
|
|
|
|
AudioInputStream stream = AudioSystem.getAudioInputStream(
|
|
new ByteArrayInputStream(data));
|
|
|
|
LineSpooler.play(stream, _clipVol, key);
|
|
|
|
} catch (IOException ioe) {
|
|
if (_verbose.getValue()) {
|
|
Log.warning("Error loading sound file [key=" + key +
|
|
", e=" + ioe + "].");
|
|
}
|
|
|
|
} catch (UnsupportedAudioFileException uafe) {
|
|
Log.warning("Unsupported sound format [key=" + key + ", e=" +
|
|
uafe + "].");
|
|
|
|
} catch (LineUnavailableException lue) {
|
|
// this error comes every goddamned time we play a sound on
|
|
// someone with a misconfigured sound card, so let's just keep
|
|
// it to ourselves
|
|
Log.debug("Line not available to play sound [key=" + key +
|
|
", e=" + lue + "].");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play a song from the specified path.
|
|
*/
|
|
protected void playMusic (MusicInfo info)
|
|
{
|
|
// stop whatever's currently playing
|
|
if (_musicPlayer != null) {
|
|
_musicPlayer.stop();
|
|
handleMusicStopped();
|
|
}
|
|
|
|
// add the new song
|
|
_musicStack.addFirst(info);
|
|
|
|
// and play it
|
|
playTopMusic();
|
|
}
|
|
|
|
/**
|
|
* Start the specified sequence.
|
|
*/
|
|
protected void playTopMusic ()
|
|
{
|
|
if (_musicStack.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// if the volume is off, we don't actually want to play anything
|
|
// but we want to at least decrement any loopers by one
|
|
// and keep them on the top of the queue
|
|
if (_musicVol == 0f) {
|
|
handleMusicStopped();
|
|
return;
|
|
}
|
|
|
|
MusicInfo info = (MusicInfo) _musicStack.getFirst();
|
|
|
|
Config c = getConfig(info);
|
|
String[] names = c.getValue(info.key, (String[])null);
|
|
if ((names == null) || (names.length == 0)) {
|
|
Log.warning("No such music [key=" + info + "].");
|
|
_musicStack.removeFirst();
|
|
playTopMusic();
|
|
return;
|
|
}
|
|
String music = names[RandomUtil.getInt(names.length)];
|
|
|
|
Class playerClass = getMusicPlayerClass(music);
|
|
|
|
// if we don't have a player for this song, play the next song
|
|
if (playerClass == null) {
|
|
_musicStack.removeFirst();
|
|
playTopMusic();
|
|
return;
|
|
}
|
|
|
|
// shutdown the old player if we're switching music types
|
|
if (! playerClass.isInstance(_musicPlayer)) {
|
|
if (_musicPlayer != null) {
|
|
_musicPlayer.shutdown();
|
|
}
|
|
|
|
// set up the new player
|
|
try {
|
|
_musicPlayer = (MusicPlayer) playerClass.newInstance();
|
|
_musicPlayer.init(this);
|
|
|
|
} catch (Exception e) {
|
|
Log.warning("Unable to instantiate music player [class=" +
|
|
playerClass + ", e=" + e + "].");
|
|
|
|
// scrap it, try again with the next song
|
|
_musicPlayer = null;
|
|
_musicStack.removeFirst();
|
|
playTopMusic();
|
|
return;
|
|
}
|
|
|
|
_musicPlayer.setVolume(_musicVol);
|
|
}
|
|
|
|
// play!
|
|
String bundle = c.getValue("bundle", (String)null);
|
|
try {
|
|
// TODO: buffer for the music player?
|
|
_musicPlayer.start(_rmgr.getResource(bundle, music));
|
|
} catch (Exception e) {
|
|
Log.warning("Error playing music, skipping [e=" + e +
|
|
", bundle=" + bundle + ", music=" + music + "].");
|
|
_musicStack.removeFirst();
|
|
playTopMusic();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate music player for the specified music file.
|
|
*/
|
|
protected static Class getMusicPlayerClass (String path)
|
|
{
|
|
path = path.toLowerCase();
|
|
|
|
// if (path.endsWith(".mid") || path.endsWith(".rmf")) {
|
|
// return MidiPlayer.class;
|
|
|
|
// } else if (path.endsWith(".mod")) {
|
|
// return ModPlayer.class;
|
|
|
|
// } else if (path.endsWith(".mp3")) {
|
|
// return Mp3Player.class;
|
|
|
|
// } else if (path.endsWith(".ogg")) {
|
|
// return OggPlayer.class;
|
|
|
|
// } else {
|
|
return null;
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Stop whatever song is currently playing and deal with the
|
|
* MusicInfo associated with it.
|
|
*/
|
|
protected void handleMusicStopped ()
|
|
{
|
|
if (_musicStack.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// see what was playing
|
|
MusicInfo current = (MusicInfo) _musicStack.getFirst();
|
|
|
|
// see how many times the song was to loop and act accordingly
|
|
switch (current.loops) {
|
|
default:
|
|
current.loops--;
|
|
break;
|
|
|
|
case 1:
|
|
// sorry charlie
|
|
_musicStack.removeFirst();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the sequence at the specified path.
|
|
*/
|
|
protected void stopMusic (SoundKey key)
|
|
{
|
|
if (! _musicStack.isEmpty()) {
|
|
MusicInfo current = (MusicInfo) _musicStack.getFirst();
|
|
|
|
// if we're currently playing this song..
|
|
if (key.equals(current)) {
|
|
// stop it
|
|
if (_musicPlayer != null) {
|
|
_musicPlayer.stop();
|
|
}
|
|
|
|
// remove it from the stack
|
|
_musicStack.removeFirst();
|
|
// start playing the next..
|
|
playTopMusic();
|
|
return;
|
|
|
|
} else {
|
|
// we aren't currently playing this song. Simply remove.
|
|
for (Iterator iter=_musicStack.iterator(); iter.hasNext(); ) {
|
|
if (key.equals(iter.next())) {
|
|
iter.remove();
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
Log.debug("Sequence stopped that wasn't in the stack anymore " +
|
|
"[key=" + key + "].");
|
|
}
|
|
|
|
/**
|
|
* Attempt to modify the music volume for any playing tracks.
|
|
*
|
|
* @param start
|
|
*/
|
|
protected void updateMusicVolume ()
|
|
{
|
|
if (_musicPlayer != null) {
|
|
_musicPlayer.setVolume(_musicVol);
|
|
}
|
|
switch (_musicAction) {
|
|
case START:
|
|
playTopMusic();
|
|
break;
|
|
|
|
case STOP:
|
|
stopMusicPlayer();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// documentation inherited from interface MusicPlayer.MusicEventListener
|
|
public void musicStopped ()
|
|
{
|
|
handleMusicStopped();
|
|
playTopMusic();
|
|
}
|
|
|
|
/**
|
|
* Shutdown the music subsystem.
|
|
*/
|
|
protected void shutdownMusic ()
|
|
{
|
|
_musicStack.clear();
|
|
stopMusicPlayer();
|
|
}
|
|
|
|
/**
|
|
* Stop the current music player.
|
|
*/
|
|
protected void stopMusicPlayer ()
|
|
{
|
|
if (_musicPlayer != null) {
|
|
_musicPlayer.stop();
|
|
_musicPlayer.shutdown();
|
|
_musicPlayer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if we're using a test sound directory.
|
|
*/
|
|
protected boolean isTesting ()
|
|
{
|
|
return !StringUtil.blank(_testDir.getValue());
|
|
}
|
|
|
|
/**
|
|
* Get the audio data for the specified path.
|
|
*/
|
|
protected byte[] getClipData (SoundKey key)
|
|
throws IOException, UnsupportedAudioFileException
|
|
{
|
|
// if we're testing, clear all non-locked sounds every time
|
|
if (isTesting()) {
|
|
_clipCache.clear();
|
|
}
|
|
|
|
byte[][] data = (byte[][]) _clipCache.get(key);
|
|
|
|
// see if it's in the locked cache (we first look in the regular
|
|
// clip cache so that locked clips that are still cached continue
|
|
// to be moved to the head of the LRU queue)
|
|
if (data == null) {
|
|
data = (byte[][]) _lockedClips.get(key);
|
|
}
|
|
|
|
if (data == null) {
|
|
// if there is a test sound, JUST use the test sound.
|
|
InputStream stream = getTestClip(key);
|
|
if (stream != null) {
|
|
data = new byte[1][];
|
|
data[0] = StreamUtils.streamAsBytes(stream, BUFFER_SIZE);
|
|
|
|
} else {
|
|
// otherwise, randomize between all available sounds
|
|
Config c = getConfig(key);
|
|
String[] names = c.getValue(key.key, (String[])null);
|
|
if (names == null) {
|
|
Log.warning("No such sound [key=" + key + "].");
|
|
return null;
|
|
}
|
|
|
|
data = new byte[names.length][];
|
|
String bundle = c.getValue("bundle", (String)null);
|
|
for (int ii=0; ii < names.length; ii++) {
|
|
data[ii] = loadClipData(bundle, names[ii]);
|
|
}
|
|
}
|
|
|
|
_clipCache.put(key, data);
|
|
}
|
|
|
|
return (data.length > 0) ? data[RandomUtil.getInt(data.length)] : null;
|
|
}
|
|
|
|
protected InputStream getTestClip (SoundKey key)
|
|
{
|
|
String testDirectory = _testDir.getValue();
|
|
if (StringUtil.blank(testDirectory)) {
|
|
return null;
|
|
}
|
|
|
|
final String namePrefix = key.key;
|
|
File f = new File(testDirectory);
|
|
File[] list = f.listFiles(new FilenameFilter() {
|
|
public boolean accept (File f, String name)
|
|
{
|
|
if (name.startsWith(namePrefix)) {
|
|
String backhalf = name.substring(namePrefix.length());
|
|
int dot = backhalf.indexOf('.');
|
|
if (dot == -1) {
|
|
dot = backhalf.length();
|
|
}
|
|
|
|
// allow the file if the portion of the name
|
|
// after the prefix but before the extension is blank
|
|
// or a parsable integer
|
|
String extra = backhalf.substring(0, dot);
|
|
if ("".equals(extra)) {
|
|
return true;
|
|
} else {
|
|
try {
|
|
Integer.parseInt(extra);
|
|
// success!
|
|
return true;
|
|
} catch (NumberFormatException nfe) {
|
|
// not a number, we fall through...
|
|
}
|
|
}
|
|
// else fall through
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
int size = (list == null) ? 0 : list.length;
|
|
if (size > 0) {
|
|
File pick = list[RandomUtil.getInt(size)];
|
|
try {
|
|
return new FileInputStream(pick);
|
|
} catch (Exception e) {
|
|
Log.warning("Error reading test sound [e=" + e + ", file=" +
|
|
pick + "].");
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Read the data from the resource manager.
|
|
*/
|
|
protected byte[] loadClipData (String bundle, String path)
|
|
throws IOException
|
|
{
|
|
InputStream clipin = null;
|
|
try {
|
|
clipin = _rmgr.getResource(bundle, path);
|
|
} catch (FileNotFoundException fnfe) {
|
|
// try from the classpath
|
|
try {
|
|
clipin = _rmgr.getResource(path);
|
|
} catch (FileNotFoundException fnfe2) {
|
|
// only play the default sound if we have verbose sound
|
|
// debuggin turned on.
|
|
if (_verbose.getValue()) {
|
|
Log.warning("Could not locate sound data [bundle=" +
|
|
bundle + ", path=" + path + "].");
|
|
if (_defaultClipPath != null) {
|
|
try {
|
|
clipin = _rmgr.getResource(
|
|
_defaultClipBundle, _defaultClipPath);
|
|
} catch (FileNotFoundException fnfe3) {
|
|
try {
|
|
clipin = _rmgr.getResource(_defaultClipPath);
|
|
} catch (FileNotFoundException fnfe4) {
|
|
Log.warning("Additionally, the default " +
|
|
"fallback sound could not be located " +
|
|
"[bundle=" + _defaultClipBundle +
|
|
", path=" + _defaultClipPath + "].");
|
|
}
|
|
}
|
|
} else {
|
|
Log.warning("No fallback default sound specified!");
|
|
}
|
|
}
|
|
// if we couldn't load the default, rethrow
|
|
if (clipin == null) {
|
|
throw fnfe2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return StreamUtils.streamAsBytes(clipin, BUFFER_SIZE);
|
|
}
|
|
|
|
protected Config getConfig (SoundKey key)
|
|
{
|
|
Config c = (Config) _configs.get(key.pkgPath);
|
|
if (c == null) {
|
|
String propPath = key.pkgPath + Sounds.PROP_NAME;
|
|
c = new Config(propPath);
|
|
_configs.put(key.pkgPath, c);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// /**
|
|
// * Adjust the volume of this clip.
|
|
// */
|
|
// protected static void adjustVolumeIdeally (Line line, float volume)
|
|
// {
|
|
// if (line.isControlSupported(FloatControl.Type.VOLUME)) {
|
|
// FloatControl vol = (FloatControl)
|
|
// line.getControl(FloatControl.Type.VOLUME);
|
|
//
|
|
// float min = vol.getMinimum();
|
|
// float max = vol.getMaximum();
|
|
//
|
|
// float ourval = (volume * (max - min)) + min;
|
|
// Log.debug("adjust vol: [min=" + min + ", ourval=" + ourval +
|
|
// ", max=" + max + "].");
|
|
// vol.setValue(ourval);
|
|
//
|
|
// } else {
|
|
// // fall back
|
|
// adjustVolume(line, volume);
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* Use the gain control to implement volume.
|
|
*/
|
|
protected static void adjustVolume (Line line, float vol)
|
|
{
|
|
FloatControl control = (FloatControl)
|
|
line.getControl(FloatControl.Type.MASTER_GAIN);
|
|
|
|
// the only problem is that gain is specified in decibals,
|
|
// which is a logarithmic scale.
|
|
// Since we want max volume to leave the sample unchanged, our
|
|
// maximum volume translates into a 0db gain.
|
|
float gain;
|
|
if (vol == 0f) {
|
|
gain = control.getMinimum();
|
|
} else {
|
|
gain = (float) ((Math.log(vol) / Math.log(10.0)) * 20.0);
|
|
}
|
|
|
|
control.setValue(gain);
|
|
//Log.info("Set gain: " + gain);
|
|
}
|
|
|
|
/**
|
|
* Handles the playing of sound clip data.
|
|
*/
|
|
protected static class LineSpooler extends Thread
|
|
{
|
|
/**
|
|
* Attempt to play the specified sound.
|
|
*/
|
|
public static void play (
|
|
AudioInputStream stream, float volume, SoundKey key)
|
|
throws LineUnavailableException
|
|
{
|
|
AudioFormat format = stream.getFormat();
|
|
LineSpooler spooler;
|
|
|
|
for (int ii=0, nn=_openSpoolers.size(); ii < nn; ii++) {
|
|
spooler = (LineSpooler) _openSpoolers.get(ii);
|
|
|
|
// we have this thread remove the spooler if it's dead
|
|
// so that we avoid deadlock conditions
|
|
if (spooler.isDead()) {
|
|
_openSpoolers.remove(ii--);
|
|
nn--;
|
|
|
|
} else if (spooler.checkPlay(format, stream, volume, key)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_openSpoolers.size() >= MAX_SPOOLERS) {
|
|
throw new LineUnavailableException("Exceeded maximum number " +
|
|
"of narya sound spoolers.");
|
|
}
|
|
|
|
spooler = new LineSpooler(format);
|
|
_openSpoolers.add(spooler);
|
|
spooler.checkPlay(format, stream, volume, key);
|
|
spooler.start();
|
|
}
|
|
|
|
/**
|
|
* Shutdown the linespooler subsystem.
|
|
*/
|
|
public static void shutdown ()
|
|
{
|
|
// this is all that is needed, after 30 seconds each spooler
|
|
for (int ii=0, nn=_openSpoolers.size(); ii < nn; ii++) {
|
|
// this will stop playback now
|
|
((LineSpooler) _openSpoolers.get(ii)).setDead();
|
|
}
|
|
// and this will remove all the spoolers. They'll still be
|
|
// around while they drain their lines and then wait 30 seconds
|
|
// to die...
|
|
_openSpoolers.clear();
|
|
}
|
|
|
|
/**
|
|
* 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, _outBufSize.getValue());
|
|
_line.start();
|
|
}
|
|
|
|
/**
|
|
* Set this line to dead.
|
|
*/
|
|
protected void setDead ()
|
|
{
|
|
_valid = false;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
SoundKey key)
|
|
{
|
|
if (_valid && (_stream == null) && _format.matches(format)) {
|
|
_stream = stream;
|
|
_key = key;
|
|
SoundManager.adjustVolume(_line, volume);
|
|
notify();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return true if we have sound data to play.
|
|
*/
|
|
protected synchronized boolean waitForData ()
|
|
{
|
|
if (_stream == null) {
|
|
try {
|
|
wait(MAX_WAIT_TIME);
|
|
} catch (InterruptedException ie) {
|
|
// ignore.
|
|
}
|
|
if (_stream == null) {
|
|
setDead(); // we waited 30 seconds and never got a sound.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Main loop for the LineSpooler.
|
|
*/
|
|
public void run ()
|
|
{
|
|
while (waitForData()) {
|
|
playStream();
|
|
}
|
|
|
|
_line.close();
|
|
}
|
|
|
|
/**
|
|
* Play the current stream.
|
|
*/
|
|
protected void playStream ()
|
|
{
|
|
int count = 0;
|
|
byte[] data = new byte[BUFFER_SIZE];
|
|
|
|
if (_verbose.getValue()) {
|
|
Log.info("Sound playing [key=" + _key.key + "].");
|
|
}
|
|
while (_valid && count != -1) {
|
|
try {
|
|
count = _stream.read(data, 0, data.length);
|
|
} catch (IOException e) {
|
|
// this shouldn't ever ever happen because the stream
|
|
// we're given is from a reliable source
|
|
Log.warning("Error reading clip data! [e=" + e + "].");
|
|
_stream = null;
|
|
return;
|
|
}
|
|
|
|
if (count >= 0) {
|
|
_line.write(data, 0, count);
|
|
}
|
|
}
|
|
|
|
// There is a major bug with using drain() under linux. It often
|
|
// causes the thread to just loop forever inside the native
|
|
// implementation of drain(), and we're screwed.
|
|
if (!RunAnywhere.isLinux()) {
|
|
_line.drain();
|
|
|
|
} else {
|
|
// we instead attempt to sleep long enough such that
|
|
// everything should be drained.
|
|
try {
|
|
Thread.sleep(NO_DRAIN_SLEEP_TIME);
|
|
} catch (InterruptedException ie) {
|
|
}
|
|
}
|
|
|
|
// clear it out so that we can wait for more.
|
|
_stream = null;
|
|
}
|
|
|
|
/** The format that our line was opened with. */
|
|
protected AudioFormat _format;
|
|
|
|
/** The stream we're currently spooling out. */
|
|
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;
|
|
|
|
/** Used for debugging. */
|
|
protected SoundKey _key;
|
|
|
|
/** The list of all the currently instantiated spoolers. */
|
|
protected static ArrayList _openSpoolers = 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 = 10;
|
|
|
|
/** The time we sleep if it's not safe to drain. */
|
|
protected static final long NO_DRAIN_SLEEP_TIME = 3000L;
|
|
}
|
|
|
|
/**
|
|
* A key for tracking sounds.
|
|
*/
|
|
protected static class SoundKey
|
|
{
|
|
public String pkgPath;
|
|
|
|
public String key;
|
|
|
|
public long stamp;
|
|
|
|
public SoundKey (String pkgPath, String key)
|
|
{
|
|
this.pkgPath = pkgPath;
|
|
this.key = key;
|
|
|
|
stamp = System.currentTimeMillis();
|
|
}
|
|
|
|
/**
|
|
* Has this sound key expired.
|
|
*/
|
|
public boolean isExpired ()
|
|
{
|
|
return (stamp + MAX_SOUND_DELAY < System.currentTimeMillis());
|
|
}
|
|
|
|
// documentation inherited
|
|
public String toString ()
|
|
{
|
|
return "SoundKey{pkgPath=" + pkgPath + ", key=" + key + "}";
|
|
}
|
|
|
|
// documentation inherited
|
|
public int hashCode ()
|
|
{
|
|
return pkgPath.hashCode() ^ key.hashCode();
|
|
}
|
|
|
|
// documentation inherited
|
|
public boolean equals (Object o)
|
|
{
|
|
if (o instanceof SoundKey) {
|
|
SoundKey that = (SoundKey) o;
|
|
return this.pkgPath.equals(that.pkgPath) &&
|
|
this.key.equals(that.key);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A class that tracks the information about our playing music files.
|
|
*/
|
|
protected static class MusicInfo extends SoundKey
|
|
{
|
|
/** How many times to loop, or -1 for forever. */
|
|
public int loops;
|
|
|
|
// TODO rename
|
|
public MusicInfo (String set, String path, int loops)
|
|
{
|
|
super(set, path);
|
|
this.loops = loops;
|
|
}
|
|
}
|
|
|
|
/** The path of the default sound to use for missing sounds. */
|
|
protected String _defaultClipBundle, _defaultClipPath;
|
|
|
|
/** The resource manager from which we obtain audio files. */
|
|
protected ResourceManager _rmgr;
|
|
|
|
/** The thread that plays sounds. */
|
|
protected Thread _player;
|
|
|
|
/** The queue of sound clips to be played. */
|
|
protected Queue _queue = new Queue();
|
|
|
|
/** Volume levels for both sound clips and music. */
|
|
protected float _clipVol = 1f, _musicVol = 1f;
|
|
|
|
/** The action to take when adjusting music volume. */
|
|
protected int _musicAction = NONE;
|
|
|
|
/** The cache of recent audio clips . */
|
|
protected LRUHashMap _clipCache = new LRUHashMap(10);
|
|
|
|
/** The set of locked audio clips; this is separate from the LRU so
|
|
* that locking clips doesn't booch up an otherwise normal caching
|
|
* agenda. */
|
|
protected HashMap _lockedClips = new HashMap();
|
|
|
|
/** The stack of songs that we're playing. */
|
|
protected LinkedList _musicStack = new LinkedList();
|
|
|
|
/** The current music player, if any. */
|
|
protected MusicPlayer _musicPlayer;
|
|
|
|
/** A set of soundTypes for which sound is enabled. */
|
|
protected HashSet _disabledTypes = new HashSet();
|
|
|
|
/** A cache of config objects we've created. */
|
|
protected HashMap _configs = new HashMap();
|
|
|
|
/** Signals to the queue to do different things. */
|
|
protected Constant PLAY = new Constant("PLAY");
|
|
protected Constant PLAYMUSIC = new Constant("PLAYMUSIC");
|
|
protected Constant STOPMUSIC = new Constant("STOPMUSIC");
|
|
protected Constant UPDATE_MUSIC_VOL = new Constant("UPDATE_MUSIC_VOL");
|
|
protected Constant LOCK = new Constant("LOCK");
|
|
protected Constant UNLOCK = new Constant("UNLOCK");
|
|
protected Constant DIE = new Constant("DIE");
|
|
|
|
/** A pref that specifies a directory for us to get test sounds from. */
|
|
protected static RuntimeAdjust.FileAdjust _testDir =
|
|
new RuntimeAdjust.FileAdjust(
|
|
"Test sound directory", "narya.media.sound.test_dir",
|
|
MediaPrefs.config, true, "");
|
|
|
|
protected static RuntimeAdjust.IntAdjust _outBufSize =
|
|
new RuntimeAdjust.IntAdjust(
|
|
"Sound output buffer size", "narya.media.sound.outbufsize",
|
|
MediaPrefs.config, 8192);
|
|
|
|
protected static RuntimeAdjust.BooleanAdjust _verbose =
|
|
new RuntimeAdjust.BooleanAdjust(
|
|
"Verbose sound event logging", "narya.media.sound.verbose",
|
|
MediaPrefs.config, false);
|
|
|
|
/** Music action constants. */
|
|
protected static final int NONE = 0;
|
|
protected static final int START = 1;
|
|
protected static final int STOP = 2;
|
|
|
|
/** The queue size at which we start to ignore requests to play sounds. */
|
|
protected static final int MAX_QUEUE_SIZE = 25;
|
|
|
|
/** The buffer size in bytes used when reading audio file data. */
|
|
protected static final int BUFFER_SIZE = 1024 * 16;
|
|
|
|
/** Used to disable sound entirely. */
|
|
protected static final boolean SOUND_ENABLED = true;
|
|
|
|
/** The maximum time after which we throw away a sound rather
|
|
* than play it. */
|
|
protected static final long MAX_SOUND_DELAY = 400L;
|
|
}
|