d1de5f411d
git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@596 ed5b42cb-e716-0410-a449-f6a68f950b19
1083 lines
34 KiB
Java
1083 lines
34 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Nenya library - tools for developing networked games
|
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/nenya/
|
|
//
|
|
// This library is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License as published
|
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
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.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.Properties;
|
|
|
|
import javax.sound.sampled.AudioFormat;
|
|
import javax.sound.sampled.AudioInputStream;
|
|
import javax.sound.sampled.AudioSystem;
|
|
import javax.sound.sampled.DataLine;
|
|
import javax.sound.sampled.FloatControl;
|
|
import javax.sound.sampled.Line;
|
|
import javax.sound.sampled.LineUnavailableException;
|
|
import javax.sound.sampled.SourceDataLine;
|
|
import javax.sound.sampled.UnsupportedAudioFileException;
|
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
import com.samskivert.swing.RuntimeAdjust;
|
|
import com.samskivert.util.Config;
|
|
import com.samskivert.util.ConfigUtil;
|
|
import com.samskivert.util.Interval;
|
|
import com.samskivert.util.LRUHashMap;
|
|
import com.samskivert.util.Queue;
|
|
import com.samskivert.util.RandomUtil;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import com.threerings.resource.ResourceManager;
|
|
|
|
import com.threerings.media.MediaPrefs;
|
|
|
|
import static com.threerings.media.Log.log;
|
|
|
|
/**
|
|
* Manages the playing of audio files.
|
|
*/
|
|
public class SoundManager
|
|
{
|
|
/** A pan value indicating that a sound should play from the left only. */
|
|
public static final float PAN_LEFT = -1f;
|
|
|
|
/** A pan value indicating that a sound should play from the right only. */
|
|
public static final float PAN_RIGHT = 1f;
|
|
|
|
/** A pan value indicating that a sound should play from center. */
|
|
public static final float PAN_CENTER = 0f;
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
@Override
|
|
public String toString ()
|
|
{
|
|
return _strname;
|
|
}
|
|
|
|
protected String _strname;
|
|
}
|
|
|
|
/**
|
|
* A control for sounds.
|
|
*/
|
|
public static interface Frob
|
|
{
|
|
/**
|
|
* Stop playing or looping the sound.
|
|
* At present, the granularity of this command is limited to the buffer size of the
|
|
* line spooler, or about 8k of data. Thus, if playing an 11khz sample, it could take
|
|
* 8/11ths of a second for the sound to actually stop playing.
|
|
*/
|
|
public void stop ();
|
|
|
|
/**
|
|
* Set the volume of the sound.
|
|
*/
|
|
public void setVolume (float vol);
|
|
|
|
/**
|
|
* Get the volume of this sound.
|
|
*/
|
|
public float getVolume ();
|
|
|
|
/**
|
|
* Set the pan value for the sound. Valid values are
|
|
* -1 for left-only, 0 is centered, all the way to +1 for right-only.
|
|
*/
|
|
public void setPan (float pan);
|
|
|
|
/**
|
|
* Get the pan value of this sound.
|
|
*/
|
|
public float getPan ();
|
|
}
|
|
|
|
/** The default sound type. */
|
|
public static final SoundType DEFAULT = new SoundType("default");
|
|
|
|
/** The default clip cache holds 4 megs. */
|
|
public static final int DEFAULT_CACHE_SIZE = 4 * 1024 * 1024;
|
|
|
|
/**
|
|
* Constructs a sound manager.
|
|
*/
|
|
public SoundManager (ResourceManager rmgr)
|
|
{
|
|
this(rmgr, null, null);
|
|
}
|
|
|
|
/**
|
|
* Constructs a sound manager with the default clip cache size.
|
|
*
|
|
* @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)
|
|
{
|
|
this(rmgr, defaultClipBundle, defaultClipPath, DEFAULT_CACHE_SIZE);
|
|
}
|
|
|
|
/**
|
|
* Constructs a sound manager.
|
|
*
|
|
* @param defaultClipPath The pathname of a sound clip to use as a
|
|
* fallback if another sound clip cannot be located.
|
|
* @param cacheSize the number of bytes of sound clips to cache.
|
|
*/
|
|
public SoundManager (ResourceManager rmgr, String defaultClipBundle, String defaultClipPath, int cacheSize)
|
|
{
|
|
// save things off
|
|
_rmgr = rmgr;
|
|
_defaultClipBundle = defaultClipBundle;
|
|
_defaultClipPath = defaultClipPath;
|
|
_clipCache =
|
|
new LRUHashMap<SoundKey, byte[][]>(cacheSize, new LRUHashMap.ItemSizer<byte[][]>() {
|
|
public int computeSize (byte[][] value) {
|
|
int total = 0;
|
|
for (byte[] bs : value) {
|
|
total += bs.length;
|
|
}
|
|
return total;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shut the damn thing off.
|
|
*/
|
|
public void shutdown ()
|
|
{
|
|
// TODO: we need to stop any looping sounds
|
|
synchronized (_queue) {
|
|
_queue.clear();
|
|
if (_spoolerCount > 0) {
|
|
_queue.append(new SoundKey(DIE)); // signal death
|
|
}
|
|
}
|
|
synchronized (_clipCache) {
|
|
_lockedClips.clear();
|
|
_configs.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a string summarizing our volume settings and disabled sound types.
|
|
*/
|
|
public String summarizeState ()
|
|
{
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append("clipVol=").append(_clipVol);
|
|
buf.append(", disabled=[");
|
|
int ii = 0;
|
|
for (Iterator<SoundType> 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 vol 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;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
enqueue(new SoundKey(LOCK, pkgPath, key), true);
|
|
}
|
|
|
|
/**
|
|
* Unlock the specified sound so that its resources can be freed.
|
|
*/
|
|
public void unlock (String pkgPath, String key)
|
|
{
|
|
enqueue(new SoundKey(UNLOCK, pkgPath, key), true);
|
|
}
|
|
|
|
/**
|
|
* Batch lock a list of sounds.
|
|
*/
|
|
public void lock (String pkgPath, String[] keys)
|
|
{
|
|
for (int ii=0; ii < keys.length; ii++) {
|
|
enqueue(new SoundKey(LOCK, pkgPath, keys[ii]), (ii == 0));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Batch unlock a list of sounds.
|
|
*/
|
|
public void unlock (String pkgPath, String[] keys)
|
|
{
|
|
for (int ii=0; ii < keys.length; ii++) {
|
|
enqueue(new SoundKey(UNLOCK, pkgPath, keys[ii]), (ii == 0));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play the specified sound as the specified type of sound, immediately.
|
|
* Note that a sound need not be locked prior to playing.
|
|
*/
|
|
public void play (SoundType type, String pkgPath, String key)
|
|
{
|
|
play(type, pkgPath, key, 0, PAN_CENTER);
|
|
}
|
|
|
|
/**
|
|
* Play the specified sound as the specified type of sound, immediately,
|
|
* with the specified pan value.
|
|
* Note that a sound need not be locked prior to playing.
|
|
*
|
|
* @param pan a value from -1f (all left) to +1f (all right).
|
|
*/
|
|
public void play (SoundType type, String pkgPath, String key, float pan)
|
|
{
|
|
play(type, pkgPath, key, 0, pan);
|
|
}
|
|
|
|
/**
|
|
* Play the specified sound after the specified delay.
|
|
* @param delay the delay in milliseconds.
|
|
*/
|
|
public void play (SoundType type, String pkgPath, String key, int delay)
|
|
{
|
|
play(type, pkgPath, key, delay, PAN_CENTER);
|
|
}
|
|
|
|
/**
|
|
* Play the specified sound after the specified delay.
|
|
* @param delay the delay in milliseconds.
|
|
* @param pan a value from -1f (all left) to +1f (all right).
|
|
*/
|
|
public void play (SoundType type, String pkgPath, String key, int delay, float pan)
|
|
{
|
|
if (type == null) {
|
|
type = DEFAULT; // let the lazy kids play too
|
|
}
|
|
|
|
if ((_clipVol != 0f) && isEnabled(type)) {
|
|
final SoundKey skey = new SoundKey(PLAY, pkgPath, key, delay, _clipVol, pan);
|
|
if (delay > 0) {
|
|
new Interval() {
|
|
@Override
|
|
public void expired () {
|
|
addToPlayQueue(skey);
|
|
}
|
|
}.schedule(delay);
|
|
} else {
|
|
addToPlayQueue(skey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loop the specified sound.
|
|
*/
|
|
public Frob loop (SoundType type, String pkgPath, String key)
|
|
{
|
|
return loop(type, pkgPath, key, PAN_CENTER);
|
|
}
|
|
|
|
/**
|
|
* Loop the specified sound.
|
|
*/
|
|
public Frob loop (SoundType type, String pkgPath, String key, float pan)
|
|
{
|
|
if (type == null) {
|
|
type = DEFAULT;
|
|
}
|
|
|
|
if (!isEnabled(type)) {
|
|
return null;
|
|
}
|
|
SoundKey skey = new SoundKey(LOOP, pkgPath, key, 0, _clipVol, pan);
|
|
addToPlayQueue(skey);
|
|
return skey; // it is a frob
|
|
}
|
|
|
|
// ==== End of public methods ====
|
|
|
|
/**
|
|
* Add the sound clip key to the queue to be played.
|
|
*/
|
|
protected void addToPlayQueue (SoundKey skey)
|
|
{
|
|
boolean queued = enqueue(skey, true);
|
|
if (queued) {
|
|
if (_verbose.getValue()) {
|
|
log.info("Sound request [key=" + skey.key + "].");
|
|
}
|
|
|
|
} else /* if (_verbose.getValue()) */ {
|
|
log.warning("SoundManager not playing sound because too many sounds in queue " +
|
|
"[key=" + skey + "].");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enqueue a new SoundKey.
|
|
*/
|
|
protected boolean enqueue (SoundKey key, boolean okToStartNew)
|
|
{
|
|
boolean add;
|
|
boolean queued;
|
|
synchronized (_queue) {
|
|
if (key.cmd == PLAY && _queue.size() > MAX_QUEUE_SIZE) {
|
|
queued = add = false;
|
|
} else {
|
|
_queue.appendLoud(key);
|
|
queued = true;
|
|
add = okToStartNew && (_freeSpoolers == 0) && (_spoolerCount < MAX_SPOOLERS);
|
|
if (add) {
|
|
_spoolerCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// and if we need a new thread, add it
|
|
if (add) {
|
|
Thread spooler = new Thread("narya SoundManager line spooler") {
|
|
@Override
|
|
public void run () {
|
|
spoolerRun();
|
|
}
|
|
};
|
|
spooler.setDaemon(true);
|
|
spooler.start();
|
|
}
|
|
|
|
return queued;
|
|
}
|
|
|
|
/**
|
|
* This is the primary run method of the sound-playing threads.
|
|
*/
|
|
protected void spoolerRun ()
|
|
{
|
|
while (true) {
|
|
try {
|
|
SoundKey key;
|
|
synchronized (_queue) {
|
|
_freeSpoolers++;
|
|
key = _queue.get(MAX_WAIT_TIME);
|
|
_freeSpoolers--;
|
|
|
|
if (key == null || key.cmd == DIE) {
|
|
_spoolerCount--;
|
|
// if dying and there are others to kill, do so
|
|
if (key != null && _spoolerCount > 0) {
|
|
_queue.appendLoud(key);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// process the command
|
|
processKey(key);
|
|
|
|
} catch (Exception e) {
|
|
log.warning(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the requested command in the specified SoundKey.
|
|
*/
|
|
protected void processKey (SoundKey key)
|
|
throws Exception
|
|
{
|
|
switch (key.cmd) {
|
|
case PLAY:
|
|
case LOOP:
|
|
playSound(key);
|
|
break;
|
|
|
|
case LOCK:
|
|
if (!isTesting()) {
|
|
synchronized (_clipCache) {
|
|
try {
|
|
getClipData(key); // preload
|
|
// copy cached to lock map
|
|
_lockedClips.put(key, _clipCache.get(key));
|
|
} catch (Exception e) {
|
|
// don't whine about LOCK failures unless we are verbosely logging
|
|
if (_verbose.getValue()) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case UNLOCK:
|
|
synchronized (_clipCache) {
|
|
_lockedClips.remove(key);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up an audio stream from the given byte array, and gets it to convert itself to PCM
|
|
* data for writing to our output line (if it isn't already that)
|
|
*/
|
|
protected AudioInputStream setupAudioStream (byte[] data)
|
|
throws UnsupportedAudioFileException, IOException
|
|
{
|
|
AudioInputStream stream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(data));
|
|
AudioFormat format = stream.getFormat();
|
|
if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
|
|
stream = AudioSystem.getAudioInputStream(
|
|
new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
|
format.getSampleRate(),
|
|
16,
|
|
format.getChannels(),
|
|
format.getChannels() * 2,
|
|
format.getSampleRate(),
|
|
false), stream);
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* On a spooling thread,
|
|
*/
|
|
protected void playSound (SoundKey key)
|
|
{
|
|
if (!key.running) {
|
|
return;
|
|
}
|
|
key.thread = Thread.currentThread();
|
|
SourceDataLine line = null;
|
|
try {
|
|
// get the sound data from our LRU cache
|
|
byte[] data = getClipData(key);
|
|
if (data == null) {
|
|
return; // borked!
|
|
|
|
} else if (key.isExpired()) {
|
|
if (_verbose.getValue()) {
|
|
log.info("Sound expired [key=" + key.key + "].");
|
|
}
|
|
return;
|
|
|
|
}
|
|
|
|
AudioInputStream stream = setupAudioStream(data);
|
|
|
|
if (key.cmd == LOOP && stream.markSupported()) {
|
|
stream.mark(data.length);
|
|
}
|
|
|
|
// open the sound line
|
|
AudioFormat format = stream.getFormat();
|
|
line = (SourceDataLine) AudioSystem.getLine(
|
|
new DataLine.Info(SourceDataLine.class, format));
|
|
line.open(format, LINEBUF_SIZE);
|
|
float setVolume = 1;
|
|
float setPan = PAN_CENTER;
|
|
line.start();
|
|
|
|
_soundSeemsToWork = true;
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
byte[] buffer = new byte[LINEBUF_SIZE];
|
|
int totalRead = 0;
|
|
do {
|
|
// play the sound
|
|
int count = 0;
|
|
while (key.running && count != -1) {
|
|
float vol = key.volume;
|
|
if (vol != setVolume) {
|
|
adjustVolume(line, vol);
|
|
setVolume = vol;
|
|
}
|
|
float pan = key.pan;
|
|
if (pan != setPan) {
|
|
adjustPan(line, pan);
|
|
setPan = pan;
|
|
}
|
|
try {
|
|
count = stream.read(buffer, 0, buffer.length);
|
|
totalRead += count; // The final -1 will make us slightly off, but that's ok
|
|
|
|
} 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 + "].");
|
|
return;
|
|
}
|
|
|
|
if (count >= 0) {
|
|
line.write(buffer, 0, count);
|
|
}
|
|
}
|
|
|
|
if (key.cmd == LOOP) {
|
|
// if we're going to loop, reset the stream to the beginning if we can,
|
|
// otherwise just remake the stream
|
|
if (stream.markSupported()) {
|
|
stream.reset();
|
|
} else {
|
|
stream = setupAudioStream(data);
|
|
}
|
|
}
|
|
} while (key.cmd == LOOP && key.running);
|
|
|
|
// sleep the drain time. We never trust line.drain() because
|
|
// it is buggy and locks up on natively multithreaded systems
|
|
// (linux, winXP with HT).
|
|
float sampleRate = format.getSampleRate();
|
|
if (sampleRate == AudioSystem.NOT_SPECIFIED) {
|
|
sampleRate = 11025; // most of our sounds are
|
|
}
|
|
int sampleSize = format.getSampleSizeInBits();
|
|
if (sampleSize == AudioSystem.NOT_SPECIFIED) {
|
|
sampleSize = 16;
|
|
}
|
|
|
|
int drainTime = (int) Math.ceil((totalRead * 8 * 1000) / (sampleRate * sampleSize));
|
|
|
|
// subtract out time we've already spent doing things.
|
|
drainTime -= System.currentTimeMillis() - startTime;
|
|
|
|
drainTime = Math.max(0, drainTime);
|
|
|
|
// add in a fudge factor of half a second
|
|
drainTime += 500;
|
|
|
|
try {
|
|
Thread.sleep(drainTime);
|
|
} catch (InterruptedException ie) { }
|
|
|
|
} catch (IOException ioe) {
|
|
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) {
|
|
String err = "Line not available to play sound [key=" + key.key + ", e=" + lue + "].";
|
|
if (_soundSeemsToWork) {
|
|
log.warning(err);
|
|
} else {
|
|
// 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(err);
|
|
}
|
|
|
|
} finally {
|
|
if (line != null) {
|
|
line.close();
|
|
}
|
|
key.thread = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if we're using a test sound directory.
|
|
*/
|
|
protected boolean isTesting ()
|
|
{
|
|
return !StringUtil.isBlank(_testDir.getValue());
|
|
}
|
|
|
|
/**
|
|
* Called by spooling threads, loads clip data from the resource manager or the cache.
|
|
*/
|
|
protected byte[] getClipData (SoundKey key)
|
|
throws IOException, UnsupportedAudioFileException
|
|
{
|
|
byte[][] data;
|
|
synchronized (_clipCache) {
|
|
// if we're testing, clear all non-locked sounds every time
|
|
if (isTesting()) {
|
|
_clipCache.clear();
|
|
}
|
|
|
|
data = _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 = _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] = IOUtils.toByteArray(stream);
|
|
|
|
} 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.isBlank(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 debugging 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 IOUtils.toByteArray(clipin);
|
|
}
|
|
|
|
/**
|
|
* Get the cached Config.
|
|
*/
|
|
protected Config getConfig (SoundKey key)
|
|
{
|
|
Config c = _configs.get(key.pkgPath);
|
|
if (c == null) {
|
|
String propPath = key.pkgPath + Sounds.PROP_NAME;
|
|
Properties props = new Properties();
|
|
try {
|
|
props = ConfigUtil.loadInheritedProperties(
|
|
propPath + ".properties", _rmgr.getClassLoader());
|
|
} catch (IOException ioe) {
|
|
log.warning("Failed to load sound properties " +
|
|
"[path=" + propPath + ", error=" + ioe + "].");
|
|
}
|
|
c = new Config(propPath, props);
|
|
_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);
|
|
}
|
|
|
|
/**
|
|
* Set the pan value for the specified line.
|
|
*/
|
|
protected static void adjustPan (Line line, float pan)
|
|
{
|
|
try {
|
|
FloatControl control = (FloatControl) line.getControl(FloatControl.Type.PAN);
|
|
control.setValue(pan);
|
|
} catch (Exception e) {
|
|
log.debug("Cannot set pan on line: " + e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A key for tracking sounds.
|
|
*/
|
|
protected static class SoundKey
|
|
implements Frob
|
|
{
|
|
public byte cmd;
|
|
|
|
public String pkgPath;
|
|
|
|
public String key;
|
|
|
|
public long stamp;
|
|
|
|
/** Should we still be running? */
|
|
public volatile boolean running = true;
|
|
|
|
public volatile float volume;
|
|
|
|
/** The pan, or 0 to center the sound. */
|
|
public volatile float pan;
|
|
|
|
/** The player thread, if it's playing us. */
|
|
public Thread thread;
|
|
|
|
/**
|
|
* Create a SoundKey that just contains the specified command.
|
|
* DIE.
|
|
*/
|
|
public SoundKey (byte cmd)
|
|
{
|
|
this.cmd = cmd;
|
|
}
|
|
|
|
/**
|
|
* Quicky constructor for music keys and lock operations.
|
|
*/
|
|
public SoundKey (byte cmd, String pkgPath, String key)
|
|
{
|
|
this(cmd);
|
|
this.pkgPath = pkgPath;
|
|
this.key = key;
|
|
}
|
|
|
|
/**
|
|
* Constructor for a sound effect soundkey.
|
|
*/
|
|
public SoundKey (byte cmd, String pkgPath, String key, int delay, float volume, float pan)
|
|
{
|
|
this(cmd, pkgPath, key);
|
|
|
|
stamp = System.currentTimeMillis() + delay;
|
|
setVolume(volume);
|
|
setPan(pan);
|
|
}
|
|
|
|
// documentation inherited from interface Frob
|
|
public void stop ()
|
|
{
|
|
running = false;
|
|
Thread t = thread;
|
|
if (t != null) {
|
|
// doesn't actually ever seem to do much
|
|
t.interrupt();
|
|
}
|
|
}
|
|
|
|
// documentation inherited from interface Frob
|
|
public void setVolume (float vol)
|
|
{
|
|
volume = Math.max(0f, Math.min(1f, vol));
|
|
}
|
|
|
|
// documentation inherited from interface Frob
|
|
public float getVolume ()
|
|
{
|
|
return volume;
|
|
}
|
|
|
|
// documentation inherited from interface Frob
|
|
public void setPan (float newPan)
|
|
{
|
|
pan = Math.max(PAN_LEFT, Math.min(PAN_RIGHT, newPan));
|
|
}
|
|
|
|
// documentation inherited from interface Frob
|
|
public float getPan ()
|
|
{
|
|
return pan;
|
|
}
|
|
|
|
/**
|
|
* Has this sound key expired.
|
|
*/
|
|
public boolean isExpired ()
|
|
{
|
|
return (stamp + MAX_SOUND_DELAY < System.currentTimeMillis());
|
|
}
|
|
|
|
@Override
|
|
public String toString ()
|
|
{
|
|
return "SoundKey{cmd=" + cmd + ", pkgPath=" + pkgPath + ", key=" + key + "}";
|
|
}
|
|
|
|
@Override
|
|
public int hashCode ()
|
|
{
|
|
return pkgPath.hashCode() ^ key.hashCode();
|
|
}
|
|
|
|
@Override
|
|
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;
|
|
}
|
|
}
|
|
|
|
/** 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 queue of sound clips to be played. */
|
|
protected Queue<SoundKey> _queue = new Queue<SoundKey>();
|
|
|
|
/** The number of currently active LineSpoolers. */
|
|
protected int _spoolerCount, _freeSpoolers;
|
|
|
|
/** If we every play a sound successfully, this is set to true. */
|
|
protected boolean _soundSeemsToWork = false;
|
|
|
|
/** Volume level for sound clips. */
|
|
protected float _clipVol = 1f;
|
|
|
|
/** The cache of recent audio clips . */
|
|
protected LRUHashMap<SoundKey, byte[][]> _clipCache;
|
|
|
|
/** 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<SoundKey,byte[][]> _lockedClips = new HashMap<SoundKey,byte[][]>();
|
|
|
|
/** A set of soundTypes for which sound is enabled. */
|
|
protected HashSet<SoundType> _disabledTypes = new HashSet<SoundType>();
|
|
|
|
/** A cache of config objects we've created. */
|
|
protected LRUHashMap<String,Config> _configs = new LRUHashMap<String,Config>(5);
|
|
|
|
/** Soundkey command constants. */
|
|
protected static final byte PLAY = 0;
|
|
protected static final byte LOCK = 1;
|
|
protected static final byte UNLOCK = 2;
|
|
protected static final byte DIE = 3;
|
|
protected static final byte LOOP = 4;
|
|
|
|
/** 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.BooleanAdjust _verbose =
|
|
new RuntimeAdjust.BooleanAdjust(
|
|
"Verbose sound event logging", "narya.media.sound.verbose", MediaPrefs.config, false);
|
|
|
|
/** The queue size at which we start to ignore requests to play sounds. */
|
|
protected static final int MAX_QUEUE_SIZE = 25;
|
|
|
|
/** The maximum time after which we throw away a sound rather than play it. */
|
|
protected static final long MAX_SOUND_DELAY = 400L;
|
|
|
|
/** The size of the line's buffer. */
|
|
protected static final int LINEBUF_SIZE = 16 * 1024;
|
|
|
|
/** 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 = 12;
|
|
}
|