From e4801b954fd01da8250cd382e5dad833c44218df Mon Sep 17 00:00:00 2001 From: Charlie Groves Date: Wed, 13 Aug 2008 21:47:42 +0000 Subject: [PATCH] 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 --- .../media/sound/AbstractSoundManager.java | 201 ++++++++ .../threerings/media/sound/MidiPlayer.java | 178 ------- .../com/threerings/media/sound/ModPlayer.java | 130 ----- .../com/threerings/media/sound/Mp3Player.java | 145 ------ .../threerings/media/sound/MusicManager.java | 349 ------------- .../threerings/media/sound/MusicPlayer.java | 89 ---- .../com/threerings/media/sound/OggPlayer.java | 401 --------------- .../media/sound/OpenALSoundManager.java | 181 +++++++ .../threerings/media/sound/SoundLoader.java | 129 +++++ .../threerings/media/sound/SoundManager.java | 462 +++--------------- .../com/threerings/openal/SoundManager.java | 28 +- 11 files changed, 608 insertions(+), 1685 deletions(-) create mode 100644 src/java/com/threerings/media/sound/AbstractSoundManager.java delete mode 100644 src/java/com/threerings/media/sound/MidiPlayer.java delete mode 100644 src/java/com/threerings/media/sound/ModPlayer.java delete mode 100644 src/java/com/threerings/media/sound/Mp3Player.java delete mode 100644 src/java/com/threerings/media/sound/MusicManager.java delete mode 100644 src/java/com/threerings/media/sound/MusicPlayer.java delete mode 100644 src/java/com/threerings/media/sound/OggPlayer.java create mode 100644 src/java/com/threerings/media/sound/OpenALSoundManager.java create mode 100644 src/java/com/threerings/media/sound/SoundLoader.java diff --git a/src/java/com/threerings/media/sound/AbstractSoundManager.java b/src/java/com/threerings/media/sound/AbstractSoundManager.java new file mode 100644 index 00000000..19f40aca --- /dev/null +++ b/src/java/com/threerings/media/sound/AbstractSoundManager.java @@ -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 _disabledTypes = Sets.newHashSet(); + +} diff --git a/src/java/com/threerings/media/sound/MidiPlayer.java b/src/java/com/threerings/media/sound/MidiPlayer.java deleted file mode 100644 index 7154fbe9..00000000 --- a/src/java/com/threerings/media/sound/MidiPlayer.java +++ /dev/null @@ -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; -} diff --git a/src/java/com/threerings/media/sound/ModPlayer.java b/src/java/com/threerings/media/sound/ModPlayer.java deleted file mode 100644 index 457f30ed..00000000 --- a/src/java/com/threerings/media/sound/ModPlayer.java +++ /dev/null @@ -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; -} diff --git a/src/java/com/threerings/media/sound/Mp3Player.java b/src/java/com/threerings/media/sound/Mp3Player.java deleted file mode 100644 index 36809cc2..00000000 --- a/src/java/com/threerings/media/sound/Mp3Player.java +++ /dev/null @@ -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; -} diff --git a/src/java/com/threerings/media/sound/MusicManager.java b/src/java/com/threerings/media/sound/MusicManager.java deleted file mode 100644 index 8079bb8f..00000000 --- a/src/java/com/threerings/media/sound/MusicManager.java +++ /dev/null @@ -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 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 _musicStack = new LinkedList(); - - /** 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(); - } - }); - } - }; -} diff --git a/src/java/com/threerings/media/sound/MusicPlayer.java b/src/java/com/threerings/media/sound/MusicPlayer.java deleted file mode 100644 index 64babb2d..00000000 --- a/src/java/com/threerings/media/sound/MusicPlayer.java +++ /dev/null @@ -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; -} diff --git a/src/java/com/threerings/media/sound/OggPlayer.java b/src/java/com/threerings/media/sound/OggPlayer.java deleted file mode 100644 index dd116307..00000000 --- a/src/java/com/threerings/media/sound/OggPlayer.java +++ /dev/null @@ -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; -} diff --git a/src/java/com/threerings/media/sound/OpenALSoundManager.java b/src/java/com/threerings/media/sound/OpenALSoundManager.java new file mode 100644 index 00000000..bf3a4ef2 --- /dev/null +++ b/src/java/com/threerings/media/sound/OpenALSoundManager.java @@ -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 _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; +} diff --git a/src/java/com/threerings/media/sound/SoundLoader.java b/src/java/com/threerings/media/sound/SoundLoader.java new file mode 100644 index 00000000..95fc0173 --- /dev/null +++ b/src/java/com/threerings/media/sound/SoundLoader.java @@ -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 + * <packagePath>/sound.properties + */ + 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 _configs = new LRUHashMap(5); +} diff --git a/src/java/com/threerings/media/sound/SoundManager.java b/src/java/com/threerings/media/sound/SoundManager.java index 94396533..5fe2ab42 100644 --- a/src/java/com/threerings/media/sound/SoundManager.java +++ b/src/java/com/threerings/media/sound/SoundManager.java @@ -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(cacheSize, new LRUHashMap.ItemSizer() { 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. onLock 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. onUnock 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 _queue = new Queue(); @@ -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 _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 _lockedClips = new HashMap(); - /** A set of soundTypes for which sound is enabled. */ - protected HashSet _disabledTypes = new HashSet(); - - /** A cache of config objects we've created. */ - protected LRUHashMap _configs = new LRUHashMap(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 = diff --git a/src/java/com/threerings/openal/SoundManager.java b/src/java/com/threerings/openal/SoundManager.java index ce4acae0..6559e159 100644 --- a/src/java/com/threerings/openal/SoundManager.java +++ b/src/java/com/threerings/openal/SoundManager.java @@ -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) {