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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li> an object oriented interface to the OpenAL system
|
||||
* <li> a mechanism for loading a group of sounds and freeing their
|
||||
* resources all at once
|
||||
* <li> a mechanism for loading sounds in a background thread and
|
||||
* preloading sounds that are likely to be needed soon
|
||||
* </ul>
|
||||
*
|
||||
* <p><em>Note:</em> 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. <em>Note:</em> the sound group <em>must</em> 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user