Pull the generic sound playing api from SoundManager up into

AbstractSoundManager and implement the actual sound playing with Java sound in
SoundManager and with OpenAL in OpenALSoundManager.  I left the SoundManager
name alone so nothing using it will need to change, but if nothing besides
yohoho is using it, I can refactor it to a more sane naming scheme.

Rip out all the music and alternative format stuff since it didn't work anyway.



git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@622 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Charlie Groves
2008-08-13 21:47:42 +00:00
parent 1ac283e675
commit e4801b954f
11 changed files with 608 additions and 1685 deletions
@@ -0,0 +1,201 @@
package com.threerings.media.sound;
import java.util.Set;
import com.google.common.collect.Sets;
import com.samskivert.util.Interval;
import com.samskivert.util.RunQueue;
import com.threerings.media.sound.SoundManager.Frob;
import com.threerings.media.sound.SoundManager.SoundType;
public abstract class AbstractSoundManager
{
/**
* Shut the damn thing off.
*/
public abstract void shutdown ();
/**
* 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 (SoundType soundType : _disabledTypes) {
if (ii++ > 0) {
buf.append(", ");
}
buf.append(soundType);
}
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 each of these keys prior to playing, to guarantee that it will be quickly
* available for playing.
*/
public abstract void lock (String pkgPath, String... keys);
/**
*Unlock the specified sounds so that its resources can be freed.
*/
public abstract void unlock (String pkgPath, String... keys);
/**
* Play the specified sound as the specified type of sound, immediately. Note that a sound
* need not be locked prior to playing.
*
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key)
{
return play(type, pkgPath, key, 0, SoundManager.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).
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key, float pan)
{
return play(type, pkgPath, key, 0, pan);
}
/**
* Play the specified sound after the specified delay.
* @param delay the delay in milliseconds.
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key, int delay)
{
return play(type, pkgPath, key, delay, SoundManager.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).
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, final String pkgPath, final String key, int delay,
final float pan)
{
if (!shouldPlay(type)) {
return false;
}
if (delay > 0) {
new Interval(getSoundQueue()) {
@Override
public void expired () {
play(pkgPath, key, pan);
}
}.schedule(delay);
} else {
play(pkgPath, key, pan);
}
return true;
}
/**
* Play the specified sound after the specified delay.
* @param pan a value from -1f (all left) to +1f (all right).
*/
protected abstract void play (String pkgPath, String key, float pan);
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
public Frob loop (SoundType type, String pkgPath, String key)
{
return loop(type, pkgPath, key, SoundManager.PAN_CENTER);
}
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
public Frob loop (SoundType type, String pkgPath, String key, float pan)
{
if (!shouldPlay(type)) {
return null;
}
return loop(pkgPath, key, pan);
}
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
protected abstract Frob loop (String pkgPath, String key, float pan);
protected boolean shouldPlay (SoundType type)
{
if (type == null) {
type = SoundManager.DEFAULT; // let the lazy kids play too
}
return _clipVol != 0f && isEnabled(type);
}
/**
* Gets the run queue on which sound should be played. It defaults to {@link RunQueue#AWT}.
*/
protected abstract RunQueue getSoundQueue ();
/** Volume level for sound clips. */
protected float _clipVol = 1f;
/** A set of soundTypes for which sound is enabled. */
protected Set<SoundType> _disabledTypes = Sets.newHashSet();
}
@@ -1,178 +0,0 @@
//
// $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.BufferedInputStream;
import java.io.InputStream;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
/**
* Plays midi/rmf sounds using Java's sequencer, which is susceptible
* to the accuracy of System.currentTimeMillis() and so currently sounds
* like "ass" under Windows.
*/
public class MidiPlayer extends MusicPlayer
implements MetaEventListener
{
@Override // documentation inherited
public void init ()
throws Exception
{
_sequencer = MidiSystem.getSequencer();
_sequencer.open();
if (_sequencer instanceof Synthesizer) {
_channels = ((Synthesizer) _sequencer).getChannels();
}
}
@Override // documentation inherited
public void shutdown ()
{
_sequencer.close();
}
@Override // documentation inherited
public void start (InputStream stream)
throws Exception
{
_sequencer.setSequence(new BufferedInputStream(stream));
_sequencer.start();
_sequencer.addMetaEventListener(this);
}
@Override // documentation inherited
public void stop ()
{
_sequencer.removeMetaEventListener(this);
_sequencer.stop();
}
@Override // documentation inherited
public void setVolume (float volume)
{
if (_channels != null) {
int setting = (int) (volume * 127.0);
for (int ii=0; ii < _channels.length; ii++) {
_channels[ii].controlChange(VOLUME_CONTROL, setting);
}
}
}
// documentation inherited from interface MetaEventListener
public void meta (MetaMessage msg)
{
if (msg.getType() == END_OF_TRACK) {
_musicListener.musicStopped();
}
}
// STUFF FROM ANOTHER TIME
// /**
// * Get a list of alternate midi devices.
// */
// public MidiDevice.Info[] getAlternateMidiDevices ()
// {
// ArrayList infos = new ArrayList();
// CollectionUtil.addAll(infos, MidiSystem.getMidiDeviceInfo());
//
// // remove the synth/seqs, leaving only hardware midi thingies
// for (Iterator iter=infos.iterator(); iter.hasNext(); ) {
// try {
// MidiDevice dev = MidiSystem.getMidiDevice(
// (MidiDevice.Info) iter.next());
// if ((dev instanceof Sequencer) ||
// (dev instanceof Synthesizer)) {
// iter.remove();
// }
// } catch (MidiUnavailableException mue) {
// iter.remove();
// }
// }
//
// return (MidiDevice.Info[]) infos.toArray(
// new MidiDevice.Info[infos.size()]);
// }
//
// /**
// * Attempt to use the alternate midi device for output.
// * Return true if we're using it.
// */
// public boolean useAlternateDevice (MidiDevice.Info devinfo)
// {
// Log.info("Trying alternate device: " + devinfo);
// try {
// MidiDevice dev = MidiSystem.getMidiDevice(devinfo);
// Receiver rec = dev.getReceiver();
// if (rec == null) {
// Log.info("Got no device!");
// return false;
// }
// _stoppingSong = true;
// _sequencer.stop();
// _sequencer.close();
//
// Receiver old = _sequencer.getTransmitter().getReceiver();
// Log.info("Old receiver: " + old);
// if (old != null) {
// old.close();
// }
// _sequencer.open();
//
// // THIS DOESN'T WORK.
// // See bug #4347135, specifically notes on the bottom.
// _sequencer.getTransmitter().setReceiver(rec);
// playTopSong();
//
// // possibly shut down an old receiver
// if (_receiver != null) {
// _receiver.close();
// }
// // set the new receiver
// _receiver = rec;
//
// return true;
//
// } catch (MidiUnavailableException mue) {
// Log.warning("Use of alternate device failed [e=" + mue +
// ", device=" + devinfo + "].");
// return false;
// }
// }
/** This is apparently the midi code for end of track. Wack. */
protected static final int END_OF_TRACK = 47;
/** The midi control for volume is 7. Ooooooo. */
protected static final int VOLUME_CONTROL = 7;
/** The sequencer. */
protected Sequencer _sequencer;
/** The channels in the sequencer, which we'll use to fuxor volumes. */
protected MidiChannel[] _channels;
}
@@ -1,130 +0,0 @@
//
// $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.DataInputStream;
import java.io.InputStream;
import micromod.MicroMod;
import micromod.Module;
import micromod.ModuleLoader;
import micromod.output.JavaSoundOutputDevice;
import micromod.output.OutputDeviceException;
import micromod.output.converters.SS16LEAudioFormatConverter;
import micromod.resamplers.LinearResampler;
/**
* A player that plays .mod format music.
*/
public class ModPlayer extends MusicPlayer
{
@Override // documentation inherited
public void init ()
throws Exception
{
_device = new NaryaSoundDevice();
_device.start();
}
@Override // documentation inherited
public void shutdown ()
{
_device.stop();
}
@Override // documentation inherited
public void start (InputStream stream)
throws Exception
{
Module module = ModuleLoader.read(new DataInputStream(stream));
final MicroMod mod = new MicroMod(module, _device, new LinearResampler());
_player = new Thread("narya mod player") {
public void run () {
while (mod.getSequenceLoopCount() == 0) {
mod.doRealTimePlayback();
try {
Thread.sleep(20);
} catch (InterruptedException ie) {
// WFCares
}
if (_player != Thread.currentThread()) {
// we were stopped!
return;
}
}
_device.drain();
_musicListener.musicStopped();
}
};
_player.setDaemon(true);
_player.start();
}
@Override // documentation inherited
public void stop ()
{
_player = null;
}
@Override // documentation inherited
public void setVolume (float vol)
{
_device.setVolume(vol);
}
/**
* A class that allows us to access the dataline so we can adjust
* the volume.
*/
protected static class NaryaSoundDevice extends JavaSoundOutputDevice
{
public NaryaSoundDevice ()
throws OutputDeviceException
{
super(new SS16LEAudioFormatConverter(), 44100, 1000);
}
@Override // documentation inherited
public void setVolume (float vol)
{
SoundManager.adjustVolume(sourceDataLine, vol);
}
@Override // documentation inherited
public void drain ()
{
sourceDataLine.drain();
}
}
/** The thread that does the work. */
protected Thread _player;
/** The sound output device. */
protected NaryaSoundDevice _device;
}
@@ -1,145 +0,0 @@
//
// $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.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import static com.threerings.media.Log.log;
/**
* Plays mp3 files. Depends on three external jar files that aren't even
* imported here:
* tritonus_share.jar
* tritonus_mp3.jar
* javalayer.jar
*/
public class Mp3Player extends MusicPlayer
{
@Override
public void init ()
{
// TODO: some stuff needs to move here, like setting up the line
// but we don't yet know the audio format, so I need to figure that
// out (the format might always be known..).
}
@Override
public void shutdown ()
{
}
@Override
public void start (final InputStream stream)
throws Exception
{
// TODO: some stuff needs to come out of here and into init/shutdown
// but we'll deal with all that later, d00d.
_player = new Thread("narya mp3 relay") {
@Override
public void run () {
AudioInputStream inStream = null;
try {
inStream = AudioSystem.getAudioInputStream(
new BufferedInputStream(stream, BUFFER_SIZE));
} catch (Exception e) {
log.warning("MP3 fuckola. [e=" + e + "].");
return;
}
AudioFormat.Encoding targetEnc = AudioFormat.Encoding.PCM_SIGNED;
inStream = AudioSystem.getAudioInputStream(targetEnc, inStream);
AudioFormat format = inStream.getFormat();
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
try {
_line = (SourceDataLine) AudioSystem.getLine(info);
_line.open(format);
} catch (LineUnavailableException lue) {
log.warning("MP3 line unavailable: " + lue);
return;
}
_line.start();
byte[] data = new byte[BUFFER_SIZE];
int count = 0;
while (count >= 0) {
try {
count = inStream.read(data, 0, data.length);
} catch (IOException ioe) {
log.warning("Error reading MP3: " + ioe);
break;
}
if (count >= 0) {
_line.write(data, 0, count);
}
if (_player != Thread.currentThread()) {
return;
}
}
_line.drain();
_line.close();
_musicListener.musicStopped();
}
};
_player.setDaemon(true);
//_player.setPriority(_player.getPriority() + 1);
_player.start();
}
@Override
public void stop ()
{
_player = null;
}
@Override
public void setVolume (float volume)
{
// TODO : line won't be null when we initialize it in the right place
if (_line != null) {
SoundManager.adjustVolume(_line, volume);
}
}
/** The thread that transfers data to the line. */
protected Thread _player;
/** The line that we play through. */
protected SourceDataLine _line;
/** The size of our buffer. */
protected static final int BUFFER_SIZE = 8192;
}
@@ -1,349 +0,0 @@
//
// $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.util.Iterator;
import java.util.LinkedList;
import com.samskivert.util.Config;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.RunQueue;
import static com.threerings.media.Log.log;
/**
* Manages the playing of audio files.
*/
// TODO:
// - fade music out when stopped?
// - be able to pause music
public class MusicManager
{
/**
* Constructs a music manager.
*
* @param smgr The soundManager we work with.
* @param runQueue the client event run queue.
*
*/
public MusicManager (SoundManager smgr, RunQueue runQueue)
{
_smgr = smgr;
_runQueue = runQueue;
}
/**
* Shut the damn thing off.
*/
public void shutdown ()
{
_musicStack.clear();
stopMusicPlayer();
}
/**
* Returns a string summarizing our volume settings and disabled sound types.
*/
public String summarizeState ()
{
StringBuilder buf = new StringBuilder();
buf.append("musicVol=").append(_musicVol);
return buf.append("]").toString();
}
/**
* Sets the volume for music.
*
* @param vol 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 (_musicPlayer != null) {
_musicPlayer.setVolume(_musicVol);
}
if ((oldvol == 0f) && (_musicVol != 0f)) {
playTopMusic();
} else if ((oldvol != 0f) && (_musicVol == 0f)) {
stopMusicPlayer();
}
}
/**
* Get the music volume.
*/
public float getMusicVolume ()
{
return _musicVol;
}
/**
* 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. If no other
* music is pushed, the specified music will play for the number of loops
* specified, and will then be popped off the stack.
*/
public void pushMusic (String pkgPath, String key, int numloops)
{
MusicKey mkey = new MusicKey(pkgPath, key, numloops);
// stop any existing playing music
if (_musicPlayer != null) {
_musicPlayer.stop();
handleMusicStopped();
}
// add the new song
_musicStack.addFirst(mkey);
// and play it
playTopMusic();
}
/**
* 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)
{
MusicKey mkey = new MusicKey(pkgPath, key, -1);
if (!_musicStack.isEmpty()) {
MusicKey current = _musicStack.getFirst();
// if we're currently playing this song..
if (mkey.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<MusicKey> 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=" + mkey + "].");
}
/**
* 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;
}
MusicKey info = _musicStack.getFirst();
Config c = _smgr.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(_playerListener);
} 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(_smgr._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 MusicKey associated with it.
*/
protected void handleMusicStopped ()
{
if (_musicStack.isEmpty()) {
return;
}
// see what was playing
MusicKey current = _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 current music player.
*/
protected void stopMusicPlayer ()
{
if (_musicPlayer != null) {
_musicPlayer.stop();
_musicPlayer.shutdown();
_musicPlayer = null;
}
}
/**
* A class that tracks the information about our playing music files.
*/
protected static class MusicKey extends SoundManager.SoundKey
{
/** How many times to loop, or -1 for forever. */
public int loops;
public MusicKey (String set, String path, int loops)
{
super((byte) -1, set, path, null);
this.loops = loops;
}
}
/** The sound manager we work with. */
protected SoundManager _smgr;
/** The client event run queue. */
protected RunQueue _runQueue;
/** Volume level for music. */
protected float _musicVol = 1f;
/** The stack of songs that we're playing. */
protected LinkedList<MusicKey> _musicStack = new LinkedList<MusicKey>();
/** The current music player, if any. */
protected MusicPlayer _musicPlayer;
/** Event listener for receiving information about a song ending. */
protected MusicPlayer.MusicEventListener _playerListener =
new MusicPlayer.MusicEventListener() {
public void musicStopped () {
_runQueue.postRunnable(new Runnable() {
public void run() {
handleMusicStopped();
playTopMusic();
}
});
}
};
}
@@ -1,89 +0,0 @@
//
// $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.InputStream;
/**
* Abstract music player.
*/
public abstract class MusicPlayer
{
/**
* A watcher interested in music events.
*/
public interface MusicEventListener
{
/**
* A callback that all players should use when the song they are
* playing has finished playing (completely).
*/
public void musicStopped();
}
/**
* Initialize the music player.
*/
public final void init (MusicEventListener musicListener)
throws Exception
{
_musicListener = musicListener;
init();
}
/**
* Do your init here.
*/
public void init ()
throws Exception
{
}
/**
* Shutdown and free all resources.
*/
public void shutdown ()
{
}
/**
* Start playing song data from the specified stream.
*/
public abstract void start (InputStream stream)
throws Exception;
/**
* Stop playing the specified song.
*/
public abstract void stop ();
/**
* Set the volume.
*
* @param volume 0f - 1f, inclusive.
*/
public abstract void setVolume (float volume);
/** Tell this guy about it when a song stops. */
protected MusicEventListener _musicListener;
}
@@ -1,401 +0,0 @@
//
// $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.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import com.jcraft.jorbis.*;
import com.jcraft.jogg.*;
/**
* Plays Ogg Vorbis streams.
*
* Hacked together from NASTY code from JOrbis.
*/
// TODO- this would need to be greatly cleaned up if we were serious about
// using it.
public class OggPlayer extends MusicPlayer
{
// documentation inherited
public void start (final InputStream stream)
{
_player = new Thread("narya ogg player") {
public void run () {
playStream(stream);
}
};
_player.setDaemon(true);
_player.start();
}
// documentation inherited
public void stop ()
{
_player = null;
}
static final int BUFSIZE=4096*2;
static int convsize=BUFSIZE*2;
static byte[] convbuffer=new byte[convsize];
SyncState oy;
StreamState os;
Page og;
Packet op;
Info vi;
Comment vc;
DspState vd;
Block vb;
byte[] buffer=null;
int bytes=0;
int format;
int rate=0;
int channels=0;
SourceDataLine outputLine=null;
int frameSizeInBytes;
int bufferLengthInBytes;
void init_jorbis(){
oy=new SyncState();
os=new StreamState();
og=new Page();
op=new Packet();
vi=new Info();
vc=new Comment();
vd=new DspState();
vb=new Block(vd);
buffer=null;
bytes=0;
oy.init();
}
SourceDataLine getOutputLine(int channels, int rate){
if(outputLine!=null || this.rate!=rate || this.channels!=channels){
if(outputLine!=null){
outputLine.drain();
outputLine.stop();
outputLine.close();
}
init_audio(channels, rate);
outputLine.start();
}
return outputLine;
}
void init_audio(int channels, int rate){
try {
//ClassLoader originalClassLoader=null;
//try{
// originalClassLoader=Thread.currentThread().getContextClassLoader();
// Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
//}
//catch(Exception ee){
// System.out.println(ee);
//}
AudioFormat audioFormat =
new AudioFormat((float)rate,
16,
channels,
true, // PCM_Signed
false // littleEndian
);
DataLine.Info info =
new DataLine.Info(SourceDataLine.class,
audioFormat,
AudioSystem.NOT_SPECIFIED);
if (!AudioSystem.isLineSupported(info)) {
//System.out.println("Line " + info + " not supported.");
return;
}
try{
outputLine = (SourceDataLine) AudioSystem.getLine(info);
//outputLine.addLineListener(this);
outputLine.open(audioFormat);
}
catch (LineUnavailableException ex) {
System.out.println("Unable to open the sourceDataLine: " + ex);
return;
}
catch (IllegalArgumentException ex) {
System.out.println("Illegal Argument: " + ex);
return;
}
frameSizeInBytes = audioFormat.getFrameSize();
int bufferLengthInFrames = outputLine.getBufferSize()/frameSizeInBytes/2;
bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes;
//buffer = new byte[bufferLengthInBytes];
//if(originalClassLoader!=null)
// Thread.currentThread().setContextClassLoader(originalClassLoader);
this.rate=rate;
this.channels=channels;
}
catch(Exception ee){
System.out.println(ee);
}
}
protected void playStream (InputStream stream)
{
init_jorbis();
loop:
while (true) {
int eos = 0;
int index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer, index, BUFSIZE);
} catch (Exception e) {
System.err.println(e);
return;
}
oy.wrote(bytes);
if (oy.pageout(og) != 1) {
if (bytes < BUFSIZE) {
break;
}
System.err.println("Input does not appear to be an Ogg bitstream.");
return;
}
os.init(og.serialno());
os.reset();
vi.init();
vc.init();
if (os.pagein(og) < 0) {
// error; stream version mismatch perhaps
System.err.println("Error reading first page of Ogg bitstream data.");
return;
}
if (os.packetout(op) != 1) {
// no page? must not be vorbis
System.err.println("Error reading initial header packet.");
break;
// return;
}
if (vi.synthesis_headerin(vc, op) < 0) {
// error case; not a vorbis header
System.err.println("This Ogg bitstream does not contain Vorbis audio data.");
return;
}
int i=0;
while (i < 2) {
while (i < 2) {
int result = oy.pageout(og);
if (result==0) {
break; // Need more data
}
if (result==1) {
os.pagein(og);
while (i < 2) {
result = os.packetout(op);
if (result == 0) {
break;
}
if (result == -1) {
System.err.println("Corrupt secondary header. Exiting.");
//return;
break loop;
}
vi.synthesis_headerin(vc, op);
i++;
}
}
}
index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer, index, BUFSIZE);
}
catch(Exception e){
System.err.println(e);
return;
}
if (bytes == 0 && i < 2) {
System.err.println("End of file before finding all Vorbis headers!");
return;
}
oy.wrote(bytes);
}
convsize=BUFSIZE/vi.channels;
vd.synthesis_init(vi);
vb.init(vd);
double[][][] _pcm=new double[1][][];
float[][][] _pcmf=new float[1][][];
int[] _index=new int[vi.channels];
getOutputLine(vi.channels, vi.rate);
while (eos == 0) {
while (eos == 0) {
if (_player != Thread.currentThread()) {
//System.err.println("bye.");
try {
//outputLine.drain();
//outputLine.stop();
//outputLine.close();
stream.close();
} catch(Exception ee) {
}
return;
}
int result = oy.pageout(og);
if (result == 0) {
break; // need more data
}
if (result == -1) { // missing or corrupt data at this page position
// System.err.println("Corrupt or missing data in bitstream; continuing...");
} else {
os.pagein(og);
while (true) {
result = os.packetout(op);
if (result == 0) break; // need more data
if (result == -1) { // missing or corrupt data at this page position
// no reason to complain; already complained above
} else {
// we have a packet. Decode it
int samples;
if (vb.synthesis(op) == 0) { // test for success!
vd.synthesis_blockin(vb);
}
while ((samples =
vd.synthesis_pcmout(_pcmf, _index)) > 0) {
double[][] pcm = _pcm[0];
float[][] pcmf = _pcmf[0];
boolean clipflag = false;
int bout = Math.min(samples, convsize);
// convert doubles to 16 bit signed ints (host order) and
// interleave
for (i = 0; i < vi.channels; i++) {
int ptr = i*2;
//int ptr=i;
int mono = _index[i];
for (int j = 0; j < bout; j++) {
int val = (int)
(pcmf[i][mono+j] * 32767.);
if (val > 32767){
val = 32767;
clipflag = true;
} else if (val < -32768) {
val = -32768;
clipflag = true;
}
if (val < 0) {
val = val | 0x8000;
}
convbuffer[ptr] = (byte)(val);
convbuffer[ptr+1] = (byte)(val>>>8);
ptr += 2 * (vi.channels);
}
}
outputLine.write(convbuffer, 0,
2 * vi.channels * bout);
vd.synthesis_read(bout);
}
}
}
if (og.eos() != 0) {
eos=1;
}
}
}
if (eos==0) {
index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer,index,BUFSIZE);
} catch (Exception e) {
System.err.println(e);
return;
}
if (bytes == -1) {
break;
}
oy.wrote(bytes);
if (bytes==0) {
eos=1;
}
}
}
os.clear();
vb.clear();
vd.clear();
vi.clear();
}
oy.clear();
//System.err.println("Done.");
try {
if (stream != null) {
stream.close();
}
} catch (Exception e) {
}
}
public void setVolume (float volume)
{
// TODO
}
protected Thread _player;
}
@@ -0,0 +1,181 @@
package com.threerings.media.sound;
import static com.threerings.media.Log.log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.lwjgl.openal.AL10;
import com.google.common.collect.Maps;
import com.samskivert.util.RunQueue;
import com.threerings.media.sound.SoundManager.Frob;
import com.threerings.openal.Clip;
import com.threerings.openal.ClipBuffer;
import com.threerings.openal.ClipProvider;
import com.threerings.openal.Sound;
import com.threerings.openal.SoundGroup;
import com.threerings.openal.Stream;
/**
* Plays sounds via OpenAL.
*/
public class OpenALSoundManager extends AbstractSoundManager
implements ClipProvider
{
public OpenALSoundManager (SoundLoader loader)
{
_loader = loader;
_alSoundManager = new MediaALSoundManager();
_group = _alSoundManager.createGroup(this, SOURCE_COUNT);
}
public Clip loadClip (String path)
throws IOException
{
byte[] data;
AudioInputStream in;
int pkgEnd = path.lastIndexOf("/") + 1;
try {
data = _loader.load(path.substring(0, pkgEnd), path.substring(pkgEnd))[0];
in = AudioSystem.getAudioInputStream(new ByteArrayInputStream(data));
} catch (UnsupportedAudioFileException e) {
throw new RuntimeException("Unsupported format: " + path, e);
}
AudioFormat format = in.getFormat();
int alformat = 0;
if (format.getChannels() == 1) {
if (format.getSampleSizeInBits() == 8) {
alformat = AL10.AL_FORMAT_MONO8;
} else if (format.getSampleSizeInBits() == 16) {
alformat = AL10.AL_FORMAT_MONO16;
} else {
throw new IOException("Illegal audio sample size: " + path);
}
} else if (format.getChannels() == 2) {
if (format.getSampleSizeInBits() == 8) {
alformat = AL10.AL_FORMAT_STEREO8;
} else if (format.getSampleSizeInBits() == 16) {
alformat = AL10.AL_FORMAT_STEREO16;
} else {
throw new IOException("Illegal audio sample size: " + path);
}
} else {
throw new IOException("Only mono and stereo are supported: " + path);
}
Clip clip = new Clip();
clip.format = alformat;
clip.data = ByteBuffer.wrap(data);
clip.frequency = (int)format.getFrameRate();
return clip;
}
@Override
public RunQueue getSoundQueue ()
{
return RunQueue.AWT;
}
@Override
public void lock (final String pkgPath, String... keys)
{
for (final String key : keys) {
final String path = pkgPath + key;
if (_locked.containsKey(path)) {
continue;
}
_alSoundManager.loadClip(this, path, new ClipBuffer.Observer() {
public void clipFailed (ClipBuffer buffer) {
log.warning("Unable to load sound", "path", path);
}
public void clipLoaded (ClipBuffer buffer) {
_locked.put(path, buffer);
}});
}
}
@Override
public void unlock (String pkgPath, String... keys)
{
for (String key : keys) {
_locked.remove(pkgPath + key);
}
}
@Override
protected Frob loop (String pkgPath, String key, float pan)
{
final Sound sound = _group.getSound(pkgPath + key);
sound.loop(true);
return new Frob(){
public float getPan () {
return 0;
}
public float getVolume () {
return 0;
}
public void setPan (float pan) {}
public void setVolume (float vol) { }
public void stop () {
sound.stop();
}};
}
@Override
public void play (String pkgPath, String key, float pan)
{
Sound sound = _group.getSound(pkgPath + key);
sound.play(true);
}
@Override
public void shutdown ()
{
_group.dispose();
_locked.clear();
for (Stream stream : _alSoundManager.getStreams()) {
stream.dispose();
}
}
protected class MediaALSoundManager extends com.threerings.openal.SoundManager {
protected MediaALSoundManager () {
super(getSoundQueue());
}
@Override
protected ClipBuffer getClip (ClipProvider provider, String path) {
if (_locked.containsKey(path)) {
return _locked.get(path);
}
return super.getClip(provider, path, null);
}
}
protected Map<String, ClipBuffer> _locked = Maps.newHashMap();
protected final SoundLoader _loader;
protected final SoundGroup _group;
protected final com.threerings.openal.SoundManager _alSoundManager;
/** Number of sounds that can be played simultaneously. */
protected final int SOURCE_COUNT = 10;
}
@@ -0,0 +1,129 @@
package com.threerings.media.sound;
import static com.threerings.media.Log.log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.apache.commons.io.IOUtils;
import com.samskivert.util.Config;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.LRUHashMap;
import com.threerings.resource.ResourceManager;
/**
* Loads sound clips specified by package-based properties files.
*/
public class SoundLoader
{
public SoundLoader (ResourceManager rmgr, String defaultBundle, String defaultPath)
{
_rmgr = rmgr;
_defaultClipBundle = defaultBundle;
_defaultClipPath = defaultPath;
}
/**
* Loads the sounds for key from the config in
* <code>&lt;packagePath&gt;/sound.properties</code>
*/
public byte[][] load (String packagePath, String key)
throws IOException
{
// otherwise, randomize between all available sounds
Config c = getConfig(packagePath);
String[] names = c.getValue(key, (String[])null);
if (names == null) {
log.warning("No such sound", "key", key);
return null;
}
byte[][] 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]);
}
return data;
}
public void shutdown ()
{
_configs.clear();
}
/**
* Get the cached Config.
*/
protected Config getConfig (String packagePath)
{
Config c = _configs.get(packagePath);
if (c == null) {
String propPath = packagePath + 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, ioe);
}
c = new Config(propPath, props);
_configs.put(packagePath, c);
}
return c;
}
/**
* 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 (SoundManager._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);
}
protected ResourceManager _rmgr;
/** The path of the default sound to use for missing sounds. */
protected String _defaultClipBundle, _defaultClipPath;
/** A cache of config objects we've created. */
protected LRUHashMap<String, Config> _configs = new LRUHashMap<String, Config>(5);
}
@@ -26,13 +26,10 @@ import static com.threerings.media.Log.log;
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.Properties;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
@@ -46,9 +43,6 @@ import javax.sound.sampled.UnsupportedAudioFileException;
import org.apache.commons.io.IOUtils;
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;
@@ -61,9 +55,9 @@ import com.threerings.media.MediaPrefs;
import com.threerings.resource.ResourceManager;
/**
* Manages the playing of audio files.
* Manages the playing of audio files via the Java Sound APIs.
*/
public class SoundManager
public class SoundManager extends AbstractSoundManager
{
/** A pan value indicating that a sound should play from the left only. */
public static final float PAN_LEFT = -1f;
@@ -169,11 +163,14 @@ public class SoundManager
*/
public SoundManager (ResourceManager rmgr, String defaultClipBundle, String defaultClipPath,
int cacheSize)
{
this(new SoundLoader(rmgr, defaultClipBundle, defaultClipPath), cacheSize);
}
public SoundManager (SoundLoader loader, int cacheSize)
{
// save things off
_rmgr = rmgr;
_defaultClipBundle = defaultClipBundle;
_defaultClipPath = defaultClipPath;
_loader = loader;
_clipCache = new LRUHashMap<SoundKey, byte[][]>(cacheSize,
new LRUHashMap.ItemSizer<byte[][]>() {
public int computeSize (byte[][] value) {
@@ -186,9 +183,7 @@ public class SoundManager
});
}
/**
* Shut the damn thing off.
*/
@Override
public void shutdown ()
{
// TODO: we need to stop any looping sounds
@@ -200,250 +195,65 @@ public class SoundManager
}
synchronized (_clipCache) {
_lockedClips.clear();
_configs.clear();
_loader.shutdown();
}
}
/**
* Returns a string summarizing our volume settings and disabled sound types.
* Sets the run queue on which sound should be played.
*/
public String summarizeState ()
{
StringBuilder buf = new StringBuilder();
buf.append("clipVol=").append(_clipVol);
buf.append(", disabled=[");
int ii = 0;
for (SoundType soundType : _disabledTypes) {
if (ii++ > 0) {
buf.append(", ");
}
buf.append(soundType);
}
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 run queue on which sound ending runnables are dispatched.
*/
public void setCallbackQueue (RunQueue queue)
public void setSoundQueue (RunQueue queue)
{
_callbackQueue = queue;
}
/**
* Gets the run queue on which sound ending runnables are dispatched. It defaults to
* {@link RunQueue#AWT}.
*/
public RunQueue getCallbackQueue ()
@Override
public RunQueue getSoundQueue ()
{
return _callbackQueue;
}
/**
* 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 each of these keys prior to playing, to guarantee that it will be quickly
* available for playing.
*/
@Override
public void lock (String pkgPath, String... keys)
{
lock(pkgPath, null, keys);
}
/**
* Optionally lock each of these keys prior to playing, to guarantee that it will be quickly
* available for playing. <code>onLock</code> will be called on the run queue from
* {@link #getCallbackQueue()} when locking is complete.
*/
public void lock (String pkgPath, Runnable onLock, String... keys)
{
for (int ii=0; ii < keys.length; ii++) {
enqueue(new SoundKey(LOCK, pkgPath, keys[ii], onLock), (ii == 0));
enqueue(new SoundKey(LOCK, pkgPath, keys[ii]), (ii == 0));
}
}
/**
*Unlock the specified sounds so that its resources can be freed.
*/
@Override
public void unlock (String pkgPath, String... keys)
{
unlock(pkgPath, null, keys);
}
/**
*Unlock the specified sounds so that its resources can be freed. <code>onUnock</code> will
* be called on the run queue from {@link #getCallbackQueue()} when unlocking is complete.
*/
public void unlock (String pkgPath, Runnable onUnlock, String... keys)
{
for (int ii = 0; ii < keys.length; ii++) {
enqueue(new SoundKey(UNLOCK, pkgPath, keys[ii], onUnlock), (ii == 0));
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.
*
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key)
{
return 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).
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key, float pan)
{
return play(type, pkgPath, key, 0, pan);
}
/**
* Play the specified sound after the specified delay.
* @param delay the delay in milliseconds.
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key, int delay)
{
return 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).
* @return true if the sound actually played, or false if its sound type was disabled or if
* sound is off altogether.
*/
public boolean play (SoundType type, String pkgPath, String key, int delay, float pan)
{
return play(type, pkgPath, key, delay, pan, null);
}
/**
* 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).
* @param onStop a runnable that will be run on the queue from {@link #getCallbackQueue()}
* when the sound stops playing.
*/
public boolean play (SoundType type, String pkgPath, String key, int delay, float pan,
Runnable onStop)
{
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, onStop);
if (delay > 0) {
new Interval() {
@Override
public void expired () {
addToPlayQueue(skey);
}
}.schedule(delay);
} else {
addToPlayQueue(skey);
}
return true;
}
return false;
}
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
public Frob loop (SoundType type, String pkgPath, String key)
{
return loop(type, pkgPath, key, PAN_CENTER);
}
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
public Frob loop (SoundType type, String pkgPath, String key, float pan)
{
return loop(type, pkgPath, key, pan, LOOP, null);
}
/**
* Loop the specified sound, stopping as quickly as possible when stop is called.
*/
public Frob loop (SoundType type, String pkgPath, String key, float pan, Runnable onStop)
{
return loop(type, pkgPath, key, pan, LOOP, onStop);
}
/**
* Loop the specified sound, stopping after the current iteration completes when stop is
* called.
*/
public Frob loopToCompletion (SoundType type, String pkgPath, String key)
{
return loop(type, pkgPath, key, PAN_CENTER, LOOP_TO_COMPLETION, null);
}
/**
* Loop the specified sound, stopping after the current iteration completes when stop is
* called.
*/
public Frob loopToCompletion (SoundType type, String pkgPath, String key, Runnable onStop)
{
return loop(type, pkgPath, key, PAN_CENTER, LOOP_TO_COMPLETION, onStop);
}
// ==== End of public methods ====
@Override
protected void play (String pkgPath, String key, float pan)
{
addToPlayQueue(new SoundKey(PLAY, pkgPath, key, 0, _clipVol, pan));
}
@Override
protected Frob loop (String pkgPath, String key, float pan)
{
return loop(pkgPath, key, pan, LOOP);
}
/**
* Loop the specified sound.
*/
protected Frob loop (String pkgPath, String key, float pan, byte cmd)
{
SoundKey skey = new SoundKey(cmd, pkgPath, key, 0, _clipVol, pan);
addToPlayQueue(skey);
return skey; // it is a frob
}
/**
* Add the sound clip key to the queue to be played.
*/
@@ -537,7 +347,6 @@ public class SoundManager
switch (key.cmd) {
case PLAY:
case LOOP:
case LOOP_TO_COMPLETION:
playSound(key);
break;
@@ -564,19 +373,26 @@ public class SoundManager
}
break;
}
if (key.onProcessed != null) {
_callbackQueue.postRunnable(key.onProcessed);
}
}
/**
* 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)
public static AudioInputStream setupAudioStream (byte[] data)
throws UnsupportedAudioFileException, IOException
{
AudioInputStream stream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(data));
return setupAudioStream(new ByteArrayInputStream(data));
}
/**
* 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)
*/
public static AudioInputStream setupAudioStream (InputStream in)
throws UnsupportedAudioFileException, IOException
{
AudioInputStream stream = AudioSystem.getAudioInputStream(in);
AudioFormat format = stream.getFormat();
if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
stream = AudioSystem.getAudioInputStream(
@@ -624,7 +440,7 @@ public class SoundManager
// open the sound line
AudioFormat format = stream.getFormat();
line = (SourceDataLine) AudioSystem.getLine(
line = (SourceDataLine)AudioSystem.getLine(
new DataLine.Info(SourceDataLine.class, format));
line.open(format, LINEBUF_SIZE);
float setVolume = 1;
@@ -639,7 +455,7 @@ public class SoundManager
do {
// play the sound
int count = 0;
while ((key.running || key.cmd == LOOP_TO_COMPLETION) && count != -1) {
while (key.running && count != -1) {
float vol = key.volume;
if (vol != setVolume) {
adjustVolume(line, vol);
@@ -766,19 +582,7 @@ public class SoundManager
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]);
}
data = _loader.load(key.pkgPath, key.key);
}
_clipCache.put(key, data);
@@ -827,9 +631,11 @@ public class SoundManager
return false;
}
});
int size = (list == null) ? 0 : list.length;
if (size > 0) {
File pick = list[RandomUtil.getInt(size)];
if (list == null) {
return null;
}
if (list.length > 0) {
File pick = list[RandomUtil.getInt(list.length)];
try {
return new FileInputStream(pick);
} catch (Exception e) {
@@ -839,114 +645,6 @@ public class SoundManager
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;
}
/**
* Loop the specified sound.
*/
protected Frob loop (SoundType type, String pkgPath, String key, float pan, byte cmd,
Runnable onStop)
{
if (type == null) {
type = DEFAULT;
}
if (!isEnabled(type)) {
return null;
}
SoundKey skey = new SoundKey(cmd, pkgPath, key, 0, _clipVol, pan, onStop);
addToPlayQueue(skey);
return skey; // it is a frob
}
// /**
// * 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.
*/
@@ -1006,9 +704,6 @@ public class SoundManager
/** The player thread, if it's playing us. */
public Thread thread;
/** Run when the processed method is called. */
public Runnable onProcessed;
/**
* Create a SoundKey that just contains the specified command.
*/
@@ -1020,21 +715,20 @@ public class SoundManager
/**
* Quicky constructor for music keys and lock operations.
*/
public SoundKey (byte cmd, String pkgPath, String key, Runnable onProcessed)
public SoundKey (byte cmd, String pkgPath, String key)
{
this(cmd);
this.pkgPath = pkgPath;
this.key = key;
this.onProcessed = onProcessed;
}
/**
* Constructor for a sound effect soundkey.
*/
public SoundKey (byte cmd, String pkgPath, String key, int delay, float volume,
float pan, Runnable onProcessed)
float pan)
{
this(cmd, pkgPath, key, onProcessed);
this(cmd, pkgPath, key);
stamp = System.currentTimeMillis() + delay;
setVolume(volume);
@@ -1088,7 +782,7 @@ public class SoundManager
* If this key is one of the two loop types.
*/
protected boolean isLoop() {
return cmd == LOOP || cmd == LOOP_TO_COMPLETION;
return cmd == LOOP;
}
@Override
@@ -1114,15 +808,12 @@ public class SoundManager
}
}
/** Does our package based sound loading. */
protected SoundLoader _loader;
/** The queue where callbacks for keys being processed are dispatched. */
protected RunQueue _callbackQueue = RunQueue.AWT;
/** 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>();
@@ -1132,30 +823,21 @@ public class SoundManager
/** 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. */
/**
* 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;
protected static final byte LOOP_TO_COMPLETION = 5;
/** A pref that specifies a directory for us to get test sounds from. */
protected static RuntimeAdjust.FileAdjust _testDir =
@@ -21,8 +21,11 @@
package com.threerings.openal;
import static com.threerings.openal.Log.log;
import java.util.ArrayList;
import java.util.HashMap;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
@@ -32,7 +35,7 @@ import com.samskivert.util.LRUHashMap;
import com.samskivert.util.Queue;
import com.samskivert.util.RunQueue;
import static com.threerings.openal.Log.log;
import com.threerings.openal.ClipBuffer.Observer;
/**
* An interface to the OpenAL library that provides a number of additional services:
@@ -142,7 +145,16 @@ public class SoundManager
*/
public void loadClip (ClipProvider provider, String path)
{
getClip(provider, path);
loadClip(provider, path, null);
}
/**
* Loads a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The loaded clip is placed in the cache.
*/
public void loadClip (ClipProvider provider, String path, Observer observer)
{
getClip(provider, path, observer);
}
/**
@@ -195,6 +207,16 @@ public class SoundManager
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path)
{
return getClip(provider, null);
}
/**
* Creates a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The clip buffer may come from the cache, and it will immediately be queued
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path, Observer observer)
{
Comparable ckey = ClipBuffer.makeKey(provider, path);
ClipBuffer buffer = _clips.get(ckey);
@@ -207,7 +229,7 @@ public class SoundManager
_loading.put(ckey, buffer);
}
}
buffer.resolve(null);
buffer.resolve(observer);
return buffer;
} catch (Throwable t) {