From 9d3eb07c06adab25c4299f030f8088f1dc280a07 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 5 Aug 2005 03:12:16 +0000 Subject: [PATCH] A new sound system based on OpenAL. Only supports sound effects at the moment, but OpenAL has the necessary business to later integrate "streams" of audio (music). git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3669 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- src/java/com/threerings/openal/Clip.java | 39 +++ .../com/threerings/openal/ClipBuffer.java | 255 ++++++++++++++++++ .../com/threerings/openal/ClipProvider.java | 33 +++ src/java/com/threerings/openal/Log.java | 56 ++++ src/java/com/threerings/openal/Sound.java | 248 +++++++++++++++++ .../com/threerings/openal/SoundGroup.java | 123 +++++++++ .../com/threerings/openal/SoundManager.java | 236 ++++++++++++++++ .../threerings/openal/TestSoundManager.java | 69 +++++ 8 files changed, 1059 insertions(+) create mode 100644 src/java/com/threerings/openal/Clip.java create mode 100644 src/java/com/threerings/openal/ClipBuffer.java create mode 100644 src/java/com/threerings/openal/ClipProvider.java create mode 100644 src/java/com/threerings/openal/Log.java create mode 100644 src/java/com/threerings/openal/Sound.java create mode 100644 src/java/com/threerings/openal/SoundGroup.java create mode 100644 src/java/com/threerings/openal/SoundManager.java create mode 100644 tests/src/java/com/threerings/openal/TestSoundManager.java diff --git a/src/java/com/threerings/openal/Clip.java b/src/java/com/threerings/openal/Clip.java new file mode 100644 index 000000000..3613bd1d0 --- /dev/null +++ b/src/java/com/threerings/openal/Clip.java @@ -0,0 +1,39 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.nio.ByteBuffer; + +/** + * Contains data for a single sampled sound. + */ +public class Clip +{ + /** The OpenAL format of this clip: {@link AL10#AL_FORMAT_MONO8}, etc. */ + public int format; + + /** The frequency of this clip in samples per second. */ + public int frequency; + + /** The audio data. */ + public ByteBuffer data; +} diff --git a/src/java/com/threerings/openal/ClipBuffer.java b/src/java/com/threerings/openal/ClipBuffer.java new file mode 100644 index 000000000..679e07d5e --- /dev/null +++ b/src/java/com/threerings/openal/ClipBuffer.java @@ -0,0 +1,255 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.io.IOException; +import java.nio.IntBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.ObserverList; + +/** + * Represents a sound that has been loaded into the OpenAL system, or one + * that was loaded but has been flushed and which can be reloaded as + * needed. + */ +public class ClipBuffer + implements LRUHashMap.LRUItem +{ + /** Used to notify parties interested in when a clip is loaded. */ + public static interface Observer + { + /** Called when a clip has completed loading and is ready to be + * played. */ + public void clipLoaded (ClipBuffer buffer); + + /** Called when a clip has failed to prepare itself for one reason + * or other. */ + public void clipFailed (ClipBuffer buffer); + } + + /** + * Create a key that uniquely identifies this combination of clip + * provider and path. + */ + public static Comparable makeKey (ClipProvider provider, String path) + { + // we'll just use a string, amazing! + return provider + path; + } + + /** + * Creates a new clip buffer with the specified path that will obtain + * its clip data from the specified source. The clip will + * automatically queue itself up to be loaded into memory. + */ + public ClipBuffer (SoundManager manager, ClipProvider provider, String path) + { + _manager = manager; + _provider = provider; + _path = path; + } + + /** + * Returns the unique key for this clip buffer. + */ + public Comparable getKey () + { + return makeKey(_provider, _path); + } + + /** + * Returns the provider used to load this clip. + */ + public ClipProvider getClipProvider () + { + return _provider; + } + + /** + * Returns the path that identifies this sound clip. + */ + public String getPath () + { + return _path; + } + + /** + * Returns true if this buffer is loaded and ready to go. + */ + public boolean isPlayable () + { + return (_state == LOADED); + } + + /** + * Returns the identifier for this clip's buffer or -1 if it is not + * loaded. + */ + public int getBufferId () + { + return (_bufferId == null) ? -1 : _bufferId.get(0); + } + + /** + * Returns the size (in bytes) of this clip as reported by OpenAL. + * This value will not be valid until the clip is bound. + */ + public int getSize () + { + return _size; + } + + /** + * Instructs this buffer to resolve its underlying clip and be ready + * to be played ASAP. + */ + public void resolve (Observer observer) + { + // if we're already loaded, this is easy + if (_state == LOADED) { + if (observer != null) { + observer.clipLoaded(this); + } + return; + } + + // queue up the observer + if (observer != null) { + _observers.add(observer); + } + + // if we're already loading, we can stop here + if (_state == LOADING) { + return; + } + + // create our OpenAL buffer and then queue ourselves up to have + // our clip data loaded + _bufferId = BufferUtils.createIntBuffer(1); + AL10.alGenBuffers(_bufferId); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to create buffer [key=" + getKey() + + ", errno=" + errno + "]."); + _bufferId = null; + // queue up a failure notification so that we properly return + // from this method and our sound has a chance to register + // itself as an observer before we jump up and declare failure + _manager.queueClipFailure(this); + + } else { + _state = LOADING; + _manager.queueClipLoad(this); + } + } + + // documentation inherited from interface LRUHashMap.LRUItem + public void removedFromMap (LRUHashMap map) + { + if (_bufferId != null) { + // we've been given the boot, free up our buffer + AL10.alDeleteBuffers(_bufferId); + _bufferId = null; + _state = UNLOADED; + } + } + + /** + * This method is called by the background sound loading thread and + * actually loads the sound data from wherever it cometh. + */ + protected Clip load () + throws IOException + { + return _provider.loadClip(_path); + } + + /** + * This method is called back on the main thread and instructs this + * buffer to bind the clip data to this buffer's OpenAL buffer. + * + * @return true if the binding succeeded, false if we were unable to + * load the sound data into OpenAL. + */ + protected boolean bind (Clip clip) + { + AL10.alBufferData( + _bufferId.get(0), clip.format, clip.data, clip.frequency); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to bind clip [key=" + getKey() + + ", errno=" + errno + "]."); + failed(); + return false; + } + + _state = LOADED; + _size = AL10.alGetBufferi(_bufferId.get(0), AL10.AL_SIZE); + _observers.apply(new ObserverList.ObserverOp() { + public boolean apply (Object observer) { + ((Observer)observer).clipLoaded(ClipBuffer.this); + return true; + } + }); + _observers.clear(); + return true; + } + + /** + * Called when we fail in some part of the process in resolving our + * clip data. Notifies our observers and resets the clip to the + * UNLOADED state. + */ + protected void failed () + { + if (_bufferId != null) { + AL10.alDeleteBuffers(_bufferId); + _bufferId = null; + } + _state = UNLOADED; + + _observers.apply(new ObserverList.ObserverOp() { + public boolean apply (Object observer) { + ((Observer)observer).clipFailed(ClipBuffer.this); + return true; + } + }); + _observers.clear(); + } + + protected SoundManager _manager; + protected ClipProvider _provider; + protected String _path; + protected int _state; + protected IntBuffer _bufferId; + protected int _size; + protected ObserverList _observers = + new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); + + protected static final int UNLOADED = 0; + protected static final int LOADING = 1; + protected static final int LOADED = 2; +} diff --git a/src/java/com/threerings/openal/ClipProvider.java b/src/java/com/threerings/openal/ClipProvider.java new file mode 100644 index 000000000..855be786d --- /dev/null +++ b/src/java/com/threerings/openal/ClipProvider.java @@ -0,0 +1,33 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.io.IOException; + +/** + * Provides a generic mechanism for loading sound data. + */ +public interface ClipProvider +{ + /** Loads the specified clip from the appropriate source. */ + public Clip loadClip (String path) throws IOException; +} diff --git a/src/java/com/threerings/openal/Log.java b/src/java/com/threerings/openal/Log.java new file mode 100644 index 000000000..ff125d442 --- /dev/null +++ b/src/java/com/threerings/openal/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("narya.openal"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/openal/Sound.java b/src/java/com/threerings/openal/Sound.java new file mode 100644 index 000000000..b40d95b5c --- /dev/null +++ b/src/java/com/threerings/openal/Sound.java @@ -0,0 +1,248 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.nio.FloatBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +/** + * Represents an instance of a sound clip which can be positioned in 3D + * space, gain and pitch adjusted and played or looped. + */ +public class Sound +{ + /** + * Returns the buffer of audio data associated with this sound. + */ + public ClipBuffer getBuffer () + { + return _buffer; + } + + /** + * Configures the location of this sound in 3D space. This will not + * affect an already playing sound but will take effect the next time + * it is played. + */ + public void setLocation (float x, float y, float z) + { + if (_position == null) { + _position = BufferUtils.createFloatBuffer(3); + } + _position.put(x).put(y).put(z); + _position.flip(); + } + + /** + * Configures the velocity of this sound in 3D space, in delta + * location per second (see {@link #setLocation}). This will not + * affect an already playing sound but will take effect the next time + * it is played. + */ + public void setVelocity (float dx, float dy, float dz) + { + if (_velocity == null) { + _velocity = BufferUtils.createFloatBuffer(3); + } + _velocity.put(dx).put(dy).put(dz); + _velocity.flip(); + } + + /** + * Configures the sounds's pitch. This value can range from 0.5 to + * 2.0. This will not affect an already playing sound but will take + * effect the next time it is played. + */ + public void setPitch (float pitch) + { + _pitch = pitch; + } + + /** + * Configures the sound's gain (volume). This value can range from 0.0 + * to 1.0 with 1.0 meaning no attenuation, each division by two + * corresponding to a -6db attentuation and each multiplication by two + * corresponding to +6db amplification. This will not affect an + * already playing sound but will take effect the next time it is + * played. + */ + public void setGain (float gain) + { + _gain = gain; + } + + /** + * Plays this sound from the beginning. While the sound is playing, an + * audio channel will be locked and then freed when the sound + * completes. + * + * @param allowDefer if false, the sound will be played immediately or + * not at all. If true, the sound will be queued up for loading if it + * is currently flushed from the cache and played once loaded. + * + * @return true if the sound could be played and was started (or + * queued up to be loaded and played ASAP if it was specified as + * deferrable) or false if the sound could not be played either + * because it was not ready and deferral was not allowed or because + * too many other sounds were playing concurrently. + */ + public boolean play (boolean allowDefer) + { + return play(allowDefer, false); + } + + /** + * Loops this sound, starting from the beginning of the audio data. It + * will continue to loop until {@link #pause}d or {@link #stop}ped. + * While the sound is playing an audio channel will be locked. + * + * @return true if a channel could be obtained to play the sound (and + * the sound was thus started) or false if no channels were available. + */ + public boolean loop (boolean allowDefer) + { + return play(allowDefer, true); + } + + /** + * Pauses this sound. A subsequent call to {@link #play} will resume + * the sound from the precise position that it left off. While the + * sound is paused, its audio channel will remain locked. + */ + public void pause () + { + if (_sourceId != -1) { + AL10.alSourcePause(_sourceId); + } + } + + /** + * Stops this sound and rewinds to its beginning. The audio channel + * being used to play the sound will be released. + */ + public void stop () + { + if (_sourceId != -1) { + AL10.alSourceStop(_sourceId); + } + } + + protected Sound (SoundGroup group, ClipBuffer buffer) + { + _group = group; + _buffer = buffer; + } + + protected boolean play (boolean allowDefer, boolean loop) + { + // if we're not ready to go... + if (!_buffer.isPlayable()) { + if (allowDefer) { + // resolve the buffer and instruct it to play once it is + // resolved + _buffer.resolve(new ClipBuffer.Observer() { + public void clipLoaded (ClipBuffer buffer) { + play(false); + } + public void clipFailed (ClipBuffer buffer) { + // ugh. nothing to do here but be sad + } + }); + + } else { + // sorry charlie... + return false; + } + } + + // if we do not already have a source, obtain one + if (_sourceId == -1) { + _sourceId = _group.acquireSource(this); + if (_sourceId == -1) { + return false; + } + + // bind our clip buffer to the source + AL10.alSourcei(_sourceId, AL10.AL_BUFFER, _buffer.getBufferId()); + } + + // configure the source with our ephemera + AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch); + AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain); + if (_position != null) { + AL10.alSource (_sourceId, AL10.AL_POSITION, _position); + } + if (_velocity != null) { + AL10.alSource (_sourceId, AL10.AL_VELOCITY, _velocity); + } + + // configure whether or not we should loop + AL10.alSourcei(_sourceId, AL10.AL_LOOPING, + loop ? AL10.AL_TRUE : AL10.AL_FALSE); + + // and start that damned thing up! + AL10.alSourcePlay(_sourceId); + + return true; + } + + /** + * Called by the {@link SoundGroup} when it wants to reclaim our + * source. + * + * @return false if we have no source to reclaim or if we're still busy + * playing our sound, true if we gave up our source. + */ + protected boolean reclaim () + { + if (_sourceId != -1 && + AL10.alGetSourcei(_sourceId, AL10.AL_SOURCE_STATE) == + AL10.AL_STOPPED) { + _sourceId = -1; + return true; + } + return false; + } + + /** The sound group with which we are associated. */ + protected SoundGroup _group; + + /** The OpenAL buffer from which we get our sound data. */ + protected ClipBuffer _buffer; + + /** The source via which we are playing our sound currently. */ + protected int _sourceId = -1; + + /** The pitch adjustment. */ + protected float _pitch = 1; + + /** The gain adjustment. */ + protected float _gain = 1; + + /** The starting position in 3D space. */ + protected FloatBuffer _position; + + /** The velocity vector. */ + protected FloatBuffer _velocity; +} diff --git a/src/java/com/threerings/openal/SoundGroup.java b/src/java/com/threerings/openal/SoundGroup.java new file mode 100644 index 000000000..2ffed3f08 --- /dev/null +++ b/src/java/com/threerings/openal/SoundGroup.java @@ -0,0 +1,123 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.nio.IntBuffer; +import java.util.ArrayList; + +import org.lwjgl.BufferUtils; +import org.lwjgl.openal.AL10; + +/** + * Manages a group of sounds, binding them to OpenAL sources as they are + * played and freeing up those sources for use by other sounds when the + * sounds are finished. + */ +public class SoundGroup +{ + /** + * Queues up the specified sound clip for pre-loading into the cache. + */ + public void preloadClip (String path) + { + _manager.getClip(_provider, path); + } + + /** + * Obtains an "instance" of the specified sound which can be + * positioned, played, looped and otherwise used to make noise. + */ + public Sound getSound (String path) + { + return new Sound(this, _manager.getClip(_provider, path)); + } + + /** + * Disposes this sound group, freeing up the OpenAL sources with which + * it is associated. All sounds obtained from this group will no + * longer be usable and should be discarded. + */ + public void dispose () + { + AL10.alDeleteSources(_sourceIds); + } + + protected SoundGroup ( + SoundManager manager, ClipProvider provider, int sources) + { + _manager = manager; + _provider = provider; + + // create our sources + _sourceIds = BufferUtils.createIntBuffer(sources); + AL10.alGenSources(_sourceIds); + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to create sources [cprov=" + provider + + ", sources=" + sources + ", errno=" + errno + "]."); + // we'll have no sources which means all requests to play + // sounds will silently fail as if all sources were in use + + } else { + for (int ii = 0; ii < sources; ii++) { + Source source = new Source(); + source.sourceId = _sourceIds.get(ii); + _sources.add(source); + } + } + } + + /** + * Called by a {@link Sound} when it wants to obtain a source on which + * to play its clip. + */ + protected int acquireSource (Sound acquirer) + { + // start at the beginning of the list looking for an available + // source + for (int ii = 0, ll = _sources.size(); ii < ll; ii++) { + Source source = (Source)_sources.get(ii); + if (source.holder == null || source.holder.reclaim()) { + // note this source's new holder + source.holder = acquirer; + // move this source to the end of the list + _sources.remove(ii); + _sources.add(source); + return source.sourceId; + } + } + return -1; + } + + /** Used to track which sources are in use. */ + protected static class Source + { + public int sourceId; + public Sound holder; + } + + protected SoundManager _manager; + protected ClipProvider _provider; + + protected IntBuffer _sourceIds; + protected ArrayList _sources = new ArrayList(); +} diff --git a/src/java/com/threerings/openal/SoundManager.java b/src/java/com/threerings/openal/SoundManager.java new file mode 100644 index 000000000..227291ae1 --- /dev/null +++ b/src/java/com/threerings/openal/SoundManager.java @@ -0,0 +1,236 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.openal; + +import java.io.IOException; +import java.util.HashMap; +import java.util.WeakHashMap; + +import org.lwjgl.openal.AL; +import org.lwjgl.openal.AL10; + +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.Queue; +import com.samskivert.util.RunQueue; + +/** + * An interface to the OpenAL library that provides a number of additional + * services: + * + * + * + *

Note: the sound manager is not thread safe (other than + * during its interactions with its internal background loading + * thread). It assumes that all sound loading and play requests will be + * made from a single thread. + */ +public class SoundManager +{ + /** + * Creates, initializes and returns the singleton sound manager + * instance. + * + * @param rqueue a queue that the sound manager can use to post short + * runnables that must be executed on the same thread from which all + * other sound methods will be called. + */ + public static SoundManager createSoundManager (RunQueue rqueue) + { + if (_soundmgr != null) { + throw new IllegalStateException( + "A sound manager has already been created."); + } + _soundmgr = new SoundManager(rqueue); + return _soundmgr; + } + + /** + * Configures the size of our sound cache. If this value is larger + * than memory available to the underlying sound system, it will be + * reduced when OpenAL first tells us we're out of memory. + */ + public void setCacheSize (int bytes) + { + } + + /** + * Creates an object that can be used to manage and play a group of + * sounds. Note: the sound group must be disposed + * when it is no longer needed via a call to {@link + * SoundGroup#dispose}. + * + * @param provider indicates from where the sound group will load its + * sounds. + * @param sources indicates the maximum number of simultaneous sounds + * that can play in this group. + */ + public SoundGroup createGroup (ClipProvider provider, int sources) + { + return new SoundGroup(this, provider, sources); + } + + /** + * Creates a sound manager and initializes the OpenAL sound subsystem. + */ + protected SoundManager (RunQueue rqueue) + { + _rqueue = rqueue; + + // initialize the OpenAL sound system + try { + AL.create("", 44100, 15, false); + } catch (Exception e) { + Log.warning("Failed to initialize sound system."); + Log.logStackTrace(e); + // don't start the background loading thread + return; + } + + int errno = AL10.alGetError(); + if (errno != AL10.AL_NO_ERROR) { + Log.warning("Failed to initialize sound system " + + "[errno=" + errno + "]."); + // don't start the background loading thread + return; + } + + // create our loading queue + _toLoad = new Queue(); + + // start up the background loader thread + _loader.setDaemon(true); + _loader.start(); + } + + /** + * Creates a clip buffer for the sound clip loaded via the specified + * provider with the specified path. The clip buffer may come from teh + * cache, and it will immediately be queued for loading if it is not + * already loaded. + */ + protected ClipBuffer getClip (ClipProvider provider, String path) + { + Comparable ckey = ClipBuffer.makeKey(provider, path); + ClipBuffer buffer = (ClipBuffer)_clips.get(ckey); + if (buffer == null) { + // check to see if this clip is currently loading + buffer = (ClipBuffer)_loading.get(ckey); + if (buffer == null) { + buffer = new ClipBuffer(this, provider, path); + _loading.put(ckey, buffer); + } + } + buffer.resolve(null); + return buffer; + } + + /** + * Queues the supplied clip buffer up for resolution. The {@link Clip} + * will be loaded into memory and then bound into OpenAL on the + * background thread. + */ + protected void queueClipLoad (ClipBuffer buffer) + { + if (_toLoad != null) { + _toLoad.append(buffer); + } + } + + /** + * Queues the supplied clip buffer up using our {@link RunQueue} to + * notify its observers that it failed to load. + */ + protected void queueClipFailure (final ClipBuffer buffer) + { + _rqueue.postRunnable(new Runnable() { + public void run () { + _loading.remove(buffer.getKey()); + buffer.failed(); + } + }); + } + + /** The thread that loads up sound clips in the background. */ + protected Thread _loader = new Thread("SoundManager.Loader") { + public void run () { + while (true) { + final ClipBuffer buffer = (ClipBuffer)_toLoad.get(); + try { + final Clip clip = buffer.load(); + _rqueue.postRunnable(new Runnable() { + public void run () { + Comparable ckey = buffer.getKey(); + _loading.remove(ckey); + if (buffer.bind(clip)) { + _clips.put(ckey, buffer); + } else { + // TODO: shrink the cache size if the bind + // failed due to OUT_OF_MEMORY + } + } + }); + + } catch (Throwable t) { + Log.warning("Failed to load clip " + + "[key=" + buffer.getKey() + "]."); + Log.logStackTrace(t); + + // let the clip and its observers know that we are a + // miserable failure + queueClipFailure(buffer); + } + } + } + }; + + /** Used to get back from the background thread to our "main" thread. */ + protected RunQueue _rqueue; + + /** Contains a mapping of all currently-loading clips. */ + protected HashMap _loading = new HashMap(); + + /** Contains a mapping of all loaded clips. */ + protected LRUHashMap _clips = new LRUHashMap(DEFAULT_CACHE_SIZE, _sizer); + + /** Contains a queue of clip buffers waiting to be loaded. */ + protected Queue _toLoad; + + /** The one and only sound manager, here for an exclusive performance + * by special request. Available for all your sound playing needs. */ + protected static SoundManager _soundmgr; + + /** Used to compute the in-memory size of sound samples. */ + protected static LRUHashMap.ItemSizer _sizer = new LRUHashMap.ItemSizer() { + public int computeSize (Object item) { + return ((ClipBuffer)item).getSize(); + } + }; + + /** Default to a cache size of one megabyte. */ + protected static final int DEFAULT_CACHE_SIZE = 1024 * 1024; +} diff --git a/tests/src/java/com/threerings/openal/TestSoundManager.java b/tests/src/java/com/threerings/openal/TestSoundManager.java new file mode 100644 index 000000000..e2ccbe9c9 --- /dev/null +++ b/tests/src/java/com/threerings/openal/TestSoundManager.java @@ -0,0 +1,69 @@ +// +// $Id$ + +package com.threerings.openal; + +import java.io.IOException; + +import org.lwjgl.util.WaveData; + +import com.samskivert.util.Interval; +import com.samskivert.util.Queue; +import com.samskivert.util.RunQueue; + +/** + * Tests the OpenAL sound system. + */ +public class TestSoundManager +{ + public static void main (String[] args) + { + if (args.length == 0) { + System.err.println("Usage: TestSoundManager sound.wav"); + System.exit(-1); + } + + final Thread current = Thread.currentThread(); + RunQueue rqueue = new RunQueue() { + public void postRunnable (Runnable r) { + _queue.append(r); + } + public boolean isDispatchThread () { + return (current == Thread.currentThread()); + } + }; + + SoundManager smgr = SoundManager.createSoundManager(rqueue); + ClipProvider provider = new ClipProvider() { + public Clip loadClip (String path) throws IOException { + Clip clip = new Clip(); + WaveData file = WaveData.create(path); + if (file == null) { + throw new IOException("Error loading " + path); + } + clip.format = file.format; + clip.frequency = file.samplerate; + clip.data = file.data; + return clip; + } + }; + final SoundGroup group = smgr.createGroup(provider, 5); + final String path = args[0]; + + // queue up an interval to play a sound over and over + Interval i = new Interval(rqueue) { + public void expired () { + Sound sound = group.getSound(path); + sound.play(true); + } + }; + i.schedule(100L, true); + + while (true) { + Runnable r = (Runnable)_queue.get(); + r.run(); + } + } + + protected static Queue _queue = new Queue(); +}