Behold, Nenya, Ring of Water and repository for our media and animation related

goodies, both Java 2D and LWJGL/JME 3D.


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Michael Bayne
2006-06-23 18:07:28 +00:00
commit c2117ee86d
570 changed files with 61913 additions and 0 deletions
@@ -0,0 +1,38 @@
//
// $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;
/**
* A blank sound used when we cannot initialize the sound system.
*/
public class BlankSound extends Sound
{
protected BlankSound ()
{
super(null, null);
}
protected boolean play (boolean allowDefer, boolean loop)
{
return false; // nothing doing
}
}
+41
View File
@@ -0,0 +1,41 @@
//
// $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;
import org.lwjgl.openal.AL10;
/**
* 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,287 @@
//
// $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.ObserverList;
/**
* Represents a sound that has been loaded into the OpenAL system.
*/
public class ClipBuffer
{
/** 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 were waiting to unload, cancel that
if (_state == UNLOADING) {
_state = LOADED;
_manager.restoreClip(this);
}
// 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);
}
}
/**
* Frees up the internal audio buffers associated with this clip.
*/
public void dispose ()
{
if (_bufferId != null) {
// if there are sources bound to this buffer, we must wait
// for them to be unbound
if (_bound > 0) {
_state = UNLOADING;
return;
}
// 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();
}
/**
* Notifies the buffer that a source has been bound to it.
*/
protected void sourceBound ()
{
_bound++;
}
/**
* Notifies the buffer that a source has been unbound from it.
*/
protected void sourceUnbound ()
{
// dispose of the buffer when the last source is unbound
if (--_bound == 0 && _state == UNLOADING) {
dispose();
}
}
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 int _bound;
protected static final int UNLOADED = 0;
protected static final int LOADING = 1;
protected static final int LOADED = 2;
protected static final int UNLOADING = 3;
}
@@ -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;
}
+56
View File
@@ -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,121 @@
//
// $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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import org.lwjgl.openal.AL10;
import com.jmex.sound.openAL.objects.util.OggInputStream;
/**
* An audio stream read from one or more Ogg Vorbis files.
*/
public class OggFileStream extends Stream
{
/**
* Creates a new Ogg stream for the specified file.
*
* @param loop whether or not to play the file in a continuous loop
* if there's nothing on the queue
*/
public OggFileStream (SoundManager soundmgr, File file, boolean loop)
throws IOException
{
super(soundmgr);
_file = new OggFile(file, loop);
_istream = new OggInputStream(new FileInputStream(file));
}
/**
* Adds a file to the queue of files to play.
*
* @param loop if true, play this file in a loop if there's nothing else
* on the queue
*/
public void queueFile (File file, boolean loop)
{
_queue.add(new OggFile(file, loop));
}
// documentation inherited
protected int getFormat ()
{
return (_istream.getFormat() == OggInputStream.FORMAT_MONO16) ?
AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_STEREO16;
}
// documentation inherited
protected int getFrequency ()
{
return _istream.getRate();
}
// documentation inherited
protected int populateBuffer (ByteBuffer buf)
throws IOException
{
int read = _istream.read(buf, buf.position(), buf.remaining());
while (buf.hasRemaining() && (!_queue.isEmpty() || _file.loop)) {
if (!_queue.isEmpty()) {
_file = _queue.remove(0);
}
_istream = new OggInputStream(new FileInputStream(_file.file));
read = Math.max(0, read);
read += _istream.read(buf, buf.position(), buf.remaining());
}
return read;
}
/** The file currently being played. */
protected OggFile _file;
/** The underlying Ogg input stream for the current file. */
protected OggInputStream _istream;
/** The queue of files to play after the current one. */
protected ArrayList<OggFile> _queue = new ArrayList<OggFile>();
/** A file queued for play. */
protected class OggFile
{
/** The file to play. */
public File file;
/** Whether or not to play the file in a loop when there's nothing
* in the queue. */
public boolean loop;
public OggFile (File file, boolean loop)
{
this.file = file;
this.loop = loop;
}
}
}
+298
View File
@@ -0,0 +1,298 @@
//
// $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
{
/** Used to await notification of the starting of a sound which may be
* delayed in loading. */
public interface StartObserver
{
/** Called when the specified sound has started playing. If
* sound is null then the sound failed to play but soundStarted
* was called anyway to perform whatever actions were waiting
* on the sound. */
public void soundStarted (Sound 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.
*
* <p><em>Note:</em> this value is multiplied by the base gain configured
* in the sound manager.
*/
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, null);
}
/**
* 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, null);
}
/**
* Plays this sound from the beginning, notifying the supplied observer
* when the audio starts.
*
* @param loop whether or not to loop the sampe until {@link #stop}ped.
*/
public void play (StartObserver obs, boolean loop)
{
play(true, loop, obs);
}
/**
* 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, final boolean loop, final StartObserver obs)
{
// if we were unable to get our buffer, fail immediately
if (_buffer == null) {
if (obs != null) {
obs.soundStarted(null);
}
return false;
}
// 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, loop, obs);
}
public void clipFailed (ClipBuffer buffer) {
// well, let's pretend like the sound started so that
// the observer isn't left hanging
if (obs != null) {
obs.soundStarted(Sound.this);
}
}
});
return true;
} else {
// sorry charlie...
if (obs != null) {
obs.soundStarted(null);
}
return false;
}
}
// let the observer know that (as far as they're concerned), we're
// started
if (obs != null) {
obs.soundStarted(this);
}
// 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 and notify it
AL10.alSourcei(_sourceId, AL10.AL_BUFFER, _buffer.getBufferId());
_buffer.sourceBound();
}
// configure the source with our ephemera
AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch);
AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain * _group.getBaseGain());
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) {
AL10.alSourcei(_sourceId, AL10.AL_BUFFER, 0);
_buffer.sourceUnbound();
_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,155 @@
//
// $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)
{
if (_manager.isInitialized()) {
_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)
{
ClipBuffer buffer = null;
if (_manager.isInitialized()) {
buffer = _manager.getClip(_provider, path);
}
return (buffer == null) ? new BlankSound() : new Sound(this, buffer);
}
/**
* 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 ()
{
if (_sourceIds != null) {
// make sure any bound sources are released
for (Source source : _sources) {
if (source.holder != null) {
source.holder.stop();
source.holder.reclaim();
}
}
_sources.clear();
AL10.alDeleteSources(_sourceIds);
_sourceIds = null;
}
}
protected SoundGroup (
SoundManager manager, ClipProvider provider, int sources)
{
_manager = manager;
_provider = provider;
// if we were unable to initialize the sound system at all, just
// stop here and we'll behave as if we have no available sources
if (!_manager.isInitialized()) {
return;
}
// 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 + "].");
_sourceIds = null;
// 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 = _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 pass the base gain through to sound effects.
*/
protected float getBaseGain ()
{
return _manager.getBaseGain();
}
/** 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<Source> _sources = new ArrayList<Source>();
}
@@ -0,0 +1,347 @@
//
// $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.ArrayList;
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;
}
/**
* Returns true if we were able to initialize the sound system.
*/
public boolean isInitialized ()
{
return (_toLoad != null);
}
/**
* 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)
{
_clips.setMaxSize(bytes);
}
/**
* Configures the base gain (which must be a value between 0 and 1.0) which
* is multiplied to the individual gain assigned to sound effects (but not
* music).
*/
public void setBaseGain (float gain)
{
_baseGain = gain;
}
/**
* Returns the base gain used for sound effects (not music).
*/
public float getBaseGain ()
{
return _baseGain;
}
/**
* 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);
}
/**
* Returns a reference to the list of active streams.
*/
public ArrayList<Stream> getStreams ()
{
return _streams;
}
/**
* Updates all of the streams controlled by the manager. This should be
* called once per frame by the application.
*
* @param time the number of seconds elapsed since the last update
*/
public void updateStreams (float time)
{
// iterate backwards through the list so that streams can dispose of
// themselves during their update
for (int ii = _streams.size() - 1; ii >= 0; ii--) {
_streams.get(ii).update(time);
}
}
/**
* 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;
}
// configure our LRU map with a removal observer
_clips.setRemovalObserver(
new LRUHashMap.RemovalObserver<Comparable,ClipBuffer>() {
public void removedFromMap (LRUHashMap<Comparable,ClipBuffer> map,
final ClipBuffer item) {
_rqueue.postRunnable(new Runnable() {
public void run () {
Log.debug("Flushing " + item.getKey());
item.dispose();
}
});
}
});
// 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 = _clips.get(ckey);
try {
if (buffer == null) {
// check to see if this clip is currently loading
buffer = _loading.get(ckey);
if (buffer == null) {
buffer = new ClipBuffer(this, provider, path);
_loading.put(ckey, buffer);
}
}
buffer.resolve(null);
return buffer;
} catch (Throwable t) {
Log.warning("Failure resolving buffer [key=" + ckey + "].");
Log.logStackTrace(t);
return null;
}
}
/**
* 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();
}
});
}
/**
* Adds the supplied clip buffer back to the cache after it has been marked
* for disposal and subsequently re-requested.
*/
protected void restoreClip (ClipBuffer buffer)
{
_clips.put(buffer.getKey(), buffer);
}
/**
* Adds a stream to the list maintained by the manager. Called by streams
* when they are created.
*/
protected void addStream (Stream stream)
{
_streams.add(stream);
}
/**
* Removes a stream from the list maintained by the manager. Called by
* streams when they are disposed.
*/
protected void removeStream (Stream stream)
{
_streams.remove(stream);
}
/** 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 {
Log.info("Loading " + buffer.getKey() + ".");
final Clip clip = buffer.load();
_rqueue.postRunnable(new Runnable() {
public void run () {
Comparable ckey = buffer.getKey();
Log.debug("Loaded " + ckey + ".");
_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;
/** A base gain that is multiplied by the individual gain assigned to
* sounds. */
protected float _baseGain = 1;
/** Contains a mapping of all currently-loading clips. */
protected HashMap<Comparable,ClipBuffer> _loading =
new HashMap<Comparable,ClipBuffer>();
/** Contains a mapping of all loaded clips. */
protected LRUHashMap<Comparable,ClipBuffer> _clips =
new LRUHashMap<Comparable,ClipBuffer>(DEFAULT_CACHE_SIZE, _sizer);
/** Contains a queue of clip buffers waiting to be loaded. */
protected Queue _toLoad;
/** The list of active streams. */
protected ArrayList<Stream> _streams = new ArrayList<Stream>();
/** 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<ClipBuffer> _sizer =
new LRUHashMap.ItemSizer<ClipBuffer>() {
public int computeSize (ClipBuffer item) {
return item.getSize();
}
};
/** Default to a cache size of one megabyte. */
protected static final int DEFAULT_CACHE_SIZE = 8 * 1024 * 1024;
}
+350
View File
@@ -0,0 +1,350 @@
//
// $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.ByteBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL10;
/**
* Represents a streaming source of sound data.
*/
public abstract class Stream
{
/**
* Creates a new stream. Call {@link #dispose} when finished with the
* stream.
*
* @param soundmgr a reference to the sound manager that will update the
* stream
*/
public Stream (SoundManager soundmgr)
{
_soundmgr = soundmgr;
// create the source and buffers
_nbuf = BufferUtils.createIntBuffer(NUM_BUFFERS);
_nbuf.limit(1);
AL10.alGenSources(_nbuf);
_sourceId = _nbuf.get();
_nbuf.clear();
AL10.alGenBuffers(_nbuf);
_nbuf.get(_bufferIds);
// register with sound manager
_soundmgr.addStream(this);
}
/**
* Sets the base gain of the stream.
*/
public void setGain (float gain)
{
_gain = gain;
if (_fadeMode == FadeMode.NONE) {
AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain);
}
}
/**
* Sets the pitch of the stream.
*/
public void setPitch (float pitch)
{
_pitch = pitch;
AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch);
}
/**
* Determines whether this stream is currently playing.
*/
public boolean isPlaying ()
{
return _state == AL10.AL_PLAYING;
}
/**
* Starts playing this stream.
*/
public void play ()
{
if (_state == AL10.AL_PLAYING) {
Log.warning("Tried to play stream already playing.");
return;
}
if (_state == AL10.AL_INITIAL) {
_qidx = _qlen = 0;
queueBuffers(NUM_BUFFERS);
}
AL10.alSourcePlay(_sourceId);
_state = AL10.AL_PLAYING;
}
/**
* Pauses this stream.
*/
public void pause ()
{
if (_state != AL10.AL_PLAYING) {
Log.warning("Tried to pause stream that wasn't playing.");
return;
}
AL10.alSourcePause(_sourceId);
_state = AL10.AL_PAUSED;
}
/**
* Stops this stream.
*/
public void stop ()
{
if (_state == AL10.AL_STOPPED) {
Log.warning("Tried to stop stream that was already stopped.");
return;
}
AL10.alSourceStop(_sourceId);
_state = AL10.AL_STOPPED;
}
/**
* Fades this stream in over the specified interval. If the stream isn't
* playing, it will be started.
*/
public void fadeIn (float interval)
{
if (_state != AL10.AL_PLAYING) {
play();
}
AL10.alSourcef(_sourceId, AL10.AL_GAIN, 0f);
_fadeMode = FadeMode.IN;
_fadeInterval = interval;
_fadeElapsed = 0f;
}
/**
* Fades this stream out over the specified interval.
*
* @param dispose if true, dispose of the stream when done fading out
*/
public void fadeOut (float interval, boolean dispose)
{
_fadeMode = dispose ? FadeMode.OUT_DISPOSE : FadeMode.OUT;
_fadeInterval = interval;
_fadeElapsed = 0f;
}
/**
* Releases the resources held by this stream and removes it from
* the manager.
*/
public void dispose ()
{
// make sure the stream is stopped
if (_state != AL10.AL_STOPPED) {
stop();
}
// delete the source and buffers
_nbuf.clear();
_nbuf.put(_sourceId).flip();
AL10.alDeleteSources(_nbuf);
_nbuf.clear();
_nbuf.put(_bufferIds).flip();
AL10.alDeleteBuffers(_nbuf);
// remove from manager
_soundmgr.removeStream(this);
}
/**
* Updates the state of this stream, loading data into buffers and
* adjusting gain as necessary. Called periodically by the
* {@link SoundManager}.
*
* @param time the amount of time elapsed since the last update
*/
protected void update (float time)
{
// update fade, which may stop playing
updateFade(time);
if (_state != AL10.AL_PLAYING) {
return;
}
// find out how many buffers have been played and unqueue them
int played = AL10.alGetSourcei(_sourceId, AL10.AL_BUFFERS_PROCESSED);
if (played == 0) {
return;
}
_nbuf.clear();
for (int ii = 0; ii < played; ii++) {
_nbuf.put(_bufferIds[_qidx]);
_qidx = (_qidx + 1) % NUM_BUFFERS;
_qlen--;
}
_nbuf.flip();
AL10.alSourceUnqueueBuffers(_sourceId, _nbuf);
// enqueue up to the number of buffers played
queueBuffers(played);
// find out if we're still playing; if not and we have buffers queued,
// we must restart
_state = AL10.alGetSourcei(_sourceId, AL10.AL_SOURCE_STATE);
if (_qlen > 0 && _state != AL10.AL_PLAYING) {
play();
}
}
/**
* Updates the gain of the stream according to the fade state.
*/
protected void updateFade (float time)
{
if (_fadeMode == FadeMode.NONE) {
return;
}
float alpha = Math.min((_fadeElapsed += time) / _fadeInterval, 1f);
AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain *
(_fadeMode == FadeMode.IN ? alpha : (1f - alpha)));
if (alpha == 1f) {
if (_fadeMode == FadeMode.OUT) {
stop();
} else if (_fadeMode == FadeMode.OUT_DISPOSE) {
dispose();
}
_fadeMode = FadeMode.NONE;
}
}
/**
* Queues (up to) the specified number of buffers.
*/
protected void queueBuffers (int buffers)
{
_nbuf.clear();
for (int ii = 0; ii < buffers; ii++) {
int bufferId = _bufferIds[(_qidx + _qlen) % NUM_BUFFERS];
if (populateBuffer(bufferId)) {
_nbuf.put(bufferId);
_qlen++;
} else {
break;
}
}
_nbuf.flip();
AL10.alSourceQueueBuffers(_sourceId, _nbuf);
}
/**
* Populates the identified buffer with as much data as it can hold.
*
* @return true if data was read into the buffer and it should be enqueued,
* false if the end of the stream has been reached and no data was read
* into the buffer
*/
protected boolean populateBuffer (int bufferId)
{
if (_abuf == null) {
_abuf = ByteBuffer.allocateDirect(BUFFER_SIZE);
}
_abuf.clear();
int read = 0;
try {
read = Math.max(populateBuffer(_abuf), 0);
} catch (IOException e) {
Log.warning("Error reading audio stream [error=" + e + "].");
}
if (read <= 0) {
return false;
}
_abuf.rewind().limit(read);
AL10.alBufferData(bufferId, getFormat(), _abuf, getFrequency());
return true;
}
/**
* Returns the OpenAL audio format of the stream.
*/
protected abstract int getFormat ();
/**
* Returns the stream's playback frequency in samples per second.
*/
protected abstract int getFrequency ();
/**
* Populates the given buffer with audio data.
*
* @return the total number of bytes read into the buffer, or -1 if the
* end of the stream has been reached
*/
protected abstract int populateBuffer (ByteBuffer buf)
throws IOException;
/** The manager to which the stream was added. */
protected SoundManager _soundmgr;
/** The source through which the stream plays. */
protected int _sourceId;
/** The buffers through which we cycle. */
protected int[] _bufferIds = new int[NUM_BUFFERS];
/** The starting index and length of the current queue in
* {@link #_bufferIds}. */
protected int _qidx, _qlen;
/** The pitch of the stream. */
protected float _pitch = 1f;
/** The gain of the stream. */
protected float _gain = 1f;
/** The interval and elapsed time for fading. */
protected float _fadeInterval, _fadeElapsed;
/** The type of fading being performed. */
protected FadeMode _fadeMode = FadeMode.NONE;
/** The buffer used to store names. */
protected IntBuffer _nbuf;
/** The buffer used to store audio data temporarily. */
protected ByteBuffer _abuf;
/** The OpenAL state of the stream. */
protected int _state = AL10.AL_INITIAL;
/** The size of the buffers in bytes. */
protected static final int BUFFER_SIZE = 131072;
/** The number of buffers to use. */
protected static final int NUM_BUFFERS = 4;
/** Fading modes. */
protected enum FadeMode { NONE, IN, OUT, OUT_DISPOSE };
}
@@ -0,0 +1,46 @@
//
// $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 org.lwjgl.util.WaveData;
/**
* Loads clips via the class loader using LWJGL's {@link WaveData} utility
* class.
*/
public class WaveDataClipProvider implements 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;
}
}