Get our tools from narya.jar and its dependencies. Moved src/java into

src/main/java. Prepared to move tests out into main project.


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1040 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Michael Bayne
2010-10-24 02:21:32 +00:00
parent 8bdd8e636f
commit 12ba2038b2
319 changed files with 19 additions and 35 deletions
@@ -0,0 +1,38 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.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
}
}
@@ -0,0 +1,113 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL10;
/**
* Represents an OpenAL buffer object.
*/
public class Buffer
{
/**
* Creates a new buffer for the specified sound manager.
*/
public Buffer (SoundManager soundmgr)
{
_soundmgr = soundmgr;
IntBuffer idbuf = BufferUtils.createIntBuffer(1);
AL10.alGenBuffers(idbuf);
_id = idbuf.get(0);
}
/**
* Returns this buffer's OpenAL identifier.
*/
public final int getId ()
{
return _id;
}
/**
* Sets the data in this buffer.
*/
public void setData (int format, ByteBuffer data, int frequency)
{
AL10.alBufferData(_id, format, data, frequency);
}
/**
* Sets the data in this buffer.
*/
public void setData (int format, IntBuffer data, int frequency)
{
AL10.alBufferData(_id, format, data, frequency);
}
/**
* Sets the data in this buffer.
*/
public void setData (int format, ShortBuffer data, int frequency)
{
AL10.alBufferData(_id, format, data, frequency);
}
/**
* Returns the size of this buffer.
*/
public int getSize ()
{
return AL10.alGetBufferi(_id, AL10.AL_SIZE);
}
/**
* Deletes this buffer, rendering it unusable.
*/
public void delete ()
{
IntBuffer idbuf = BufferUtils.createIntBuffer(1);
idbuf.put(_id).rewind();
AL10.alDeleteBuffers(idbuf);
_id = 0;
}
@Override
protected void finalize ()
throws Throwable
{
super.finalize();
if (_id > 0) {
_soundmgr.bufferFinalized(_id);
}
}
/** The sound manager responsible for this buffer. */
protected SoundManager _soundmgr;
/** The OpenAL identifier for this buffer. */
protected int _id;
}
@@ -0,0 +1,55 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.nio.ByteBuffer;
import org.lwjgl.openal.AL10;
import org.lwjgl.util.WaveData;
/**
* Contains data for a single sampled sound.
*/
public class Clip
{
public Clip ()
{}
/**
* Fills in a clip from the given wave data.
*/
public Clip (WaveData data)
{
format = data.format;
frequency = data.samplerate;
this.data = data.data;
}
/** 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,281 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.io.IOException;
import org.lwjgl.openal.AL10;
import com.samskivert.util.ObserverList;
import static com.threerings.openal.Log.log;
/**
* 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 String 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 String 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 a reference to this clip's buffer or <code>null</code> if it is not loaded.
*/
public Buffer getBuffer ()
{
return _buffer;
}
/**
* 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
_buffer = new Buffer(_manager);
int errno = AL10.alGetError();
if (errno != AL10.AL_NO_ERROR) {
log.warning("Failed to create buffer [key=" + getKey() +
", errno=" + errno + "].");
_buffer = 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 (_buffer != 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
_buffer.delete();
_buffer = 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)
{
_buffer.setData(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 = _buffer.getSize();
_observers.apply(new ObserverList.ObserverOp<Observer>() {
public boolean apply (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 (_buffer != null) {
_buffer.delete();
_buffer = null;
}
_state = UNLOADED;
_observers.apply(new ObserverList.ObserverOp<Observer>() {
public boolean apply (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 Buffer _buffer;
protected int _size;
protected ObserverList<Observer> _observers =
new ObserverList<Observer>(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$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.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,116 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.ArrayList;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.collect.Lists;
/**
* An audio stream read from one or more files.
*/
public class FileStream extends Stream
{
/**
* Creates a new 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 FileStream (SoundManager soundmgr, File file, boolean loop)
throws IOException
{
super(soundmgr);
_file = new QueuedFile(file, loop);
_decoder = StreamDecoder.createInstance(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 QueuedFile(file, loop));
}
@Override
protected int getFormat ()
{
return _decoder.getFormat();
}
@Override
protected int getFrequency ()
{
return _decoder.getFrequency();
}
@Override
protected int populateBuffer (ByteBuffer buf)
throws IOException
{
int read = _decoder.read(buf);
while (buf.hasRemaining() && (!_queue.isEmpty() || _file.loop)) {
if (!_queue.isEmpty()) {
_file = _queue.remove(0);
}
_decoder = StreamDecoder.createInstance(_file.file);
read = Math.max(0, read);
read += _decoder.read(buf);
}
return read;
}
/** The file currently being played. */
protected QueuedFile _file;
/** The stream decoder for the current file. */
protected StreamDecoder _decoder;
/** The queue of files to play after the current one. */
protected ArrayList<QueuedFile> _queue = Lists.newArrayList();
/** A file queued for play. */
protected class QueuedFile
{
/** 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 QueuedFile (File file, boolean loop)
{
this.file = file;
this.loop = loop;
}
}
}
@@ -0,0 +1,97 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL10;
/**
* Represents the OpenAL listener object.
*/
public class Listener
{
/**
* Sets the position of the listener.
*/
public void setPosition (float x, float y, float z)
{
if (_px != x || _py != y || _pz != z) {
AL10.alListener3f(AL10.AL_POSITION, _px = x, _py = y, _pz = z);
}
}
/**
* Sets the velocity of the listener.
*/
public void setVelocity (float x, float y, float z)
{
if (_vx != x || _vy != y || _vz != z) {
AL10.alListener3f(AL10.AL_VELOCITY, _vx = x, _vy = y, _vz = z);
}
}
/**
* Sets the gain of the listener.
*/
public void setGain (float gain)
{
if (_gain != gain) {
AL10.alListenerf(AL10.AL_GAIN, _gain = gain);
}
}
/**
* Sets the orientation of the listener in terms of an "at" (direction) and "up" vector.
*/
public void setOrientation (float ax, float ay, float az, float ux, float uy, float uz)
{
if (_ax != ax || _ay != ay || _az != az || _ux != ux || _uy != uy || _uz != uz) {
_vbuf.put(_ax = ax).put(_ay = ay).put(_az = az);
_vbuf.put(_ux = ux).put(_uy = uy).put(_uz = uz).rewind();
AL10.alListener(AL10.AL_ORIENTATION, _vbuf);
}
}
/**
* The listener is only to be created by the {@link SoundManager}.
*/
protected Listener ()
{
}
/** The position of the listener. */
protected float _px, _py, _pz;
/** The velocity of the listener. */
protected float _vx, _vy, _vz;
/** The gain of the listener. */
protected float _gain = 1f;
/** The orientation of the listener (initialized to the OpenAL defaults). */
protected float _ax, _ay, _az = -1f, _ux, _uy = 1f, _uz;
/** A buffer for floating point values. */
protected FloatBuffer _vbuf = BufferUtils.createFloatBuffer(6);
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by this package.
*/
public class Log
{
public static Logger log = Logger.getLogger("com.threerings.openal");
}
@@ -0,0 +1,111 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.decoder.SampleBuffer;
import org.lwjgl.openal.AL10;
/**
* Decodes MP3 streams.
*/
public class Mp3StreamDecoder extends StreamDecoder
{
@Override
public void init (InputStream in)
throws IOException
{
_istream = new Bitstream(in);
try {
_header = _istream.readFrame();
} catch (JavaLayerException e) {
throw new IOException(e.toString());
}
_decoder = new Decoder();
}
@Override
public int getFormat ()
{
return AL10.AL_FORMAT_STEREO16;
}
@Override
public int getFrequency ()
{
return _header.frequency();
}
@Override
public int read (ByteBuffer buf)
throws IOException
{
ShortBuffer sbuf = buf.asShortBuffer();
int total = 0;
while (sbuf.hasRemaining() && _header != null) {
if (_buffer == null) {
try {
_buffer = (SampleBuffer)_decoder.decodeFrame(_header, _istream);
_istream.closeFrame();
_header = _istream.readFrame();
} catch (JavaLayerException e) {
throw new IOException(e.toString());
}
}
int blen = _buffer.getBufferLength(),
length = Math.min(sbuf.remaining(), blen - _offset);
sbuf.put(_buffer.getBuffer(), _offset, length);
if ((_offset += length) >= blen) {
_offset = 0;
_buffer = null;
}
total += (length * 2);
}
buf.position(buf.position() + total);
return total;
}
/** Handles reading the mp3 data. */
protected Bitstream _istream;
/** The mp3 header. */
protected Header _header;
/** Handles decoding the mp3 data. */
protected Decoder _decoder;
/** The sample buffer used for output. */
protected SampleBuffer _buffer;
/** Our offset into the currently decoded frame. */
protected int _offset;
}
@@ -0,0 +1,241 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import org.lwjgl.openal.AL10;
import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Block;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Info;
/**
* Decodes Ogg Vorbis streams.
*/
public class OggStreamDecoder extends StreamDecoder
{
@Override
public void init (InputStream in)
throws IOException
{
_in = in;
_sync.init();
_info.init();
// read the first packet "manually" to make sure everything's kosher
readChunk();
if (_sync.pageout(_page) != 1) {
throw new IOException("Input is not an Ogg bitstream.");
}
_stream.init(_page.serialno());
_stream.reset();
if (_stream.pagein(_page) < 0) {
throw new IOException("Error reading first page of Ogg bitstream data.");
}
if (_stream.packetout(_packet) != 1) {
throw new IOException("Error reading initial header packet.");
}
Comment comment = new Comment();
comment.init();
if (_info.synthesis_headerin(comment, _packet) < 0) {
throw new IOException("Ogg bitstream does not contain Vorbis audio data.");
}
// two more packets in header
for (int ii = 0; ii < 2; ii++) {
if (!readPacket()) {
throw new IOException("End of file before reading all Vorbis headers.");
}
_info.synthesis_headerin(comment, _packet);
}
_dsp.synthesis_init(_info);
_block.init(_dsp);
_offsets = new int[_info.channels];
}
@Override
public int getFormat ()
{
return (_info.channels == 1) ? AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_STEREO16;
}
@Override
public int getFrequency ()
{
return _info.rate;
}
@Override
public int read (ByteBuffer buf)
throws IOException
{
ShortBuffer sbuf = buf.asShortBuffer();
int channels = _info.channels;
int total = 0;
while (sbuf.hasRemaining()) {
int samples = Math.min(readSamples(), sbuf.remaining() / channels);
if (samples == 0) {
break;
}
int length = samples * channels;
if (_data.length < length) {
_data = new short[length];
}
short[] data = _data;
for (int ii = 0; ii < channels; ii++) {
float[] pcm = _pcm[0][ii];
int sidx = _offsets[ii], didx = ii;
for (int jj = 0; jj < samples; jj++) {
float value = Math.min(Math.max(pcm[sidx], -1f), +1f);
data[didx] = (short)(value * 32767f);
sidx++;
didx += channels;
}
}
sbuf.put(_data, 0, length);
_dsp.synthesis_read(samples);
total += (length * 2);
}
buf.position(buf.position() + total);
return total;
}
/**
* Reads a buffer's worth of samples.
*
* @return the number of samples read, or zero if we've reached the end of the stream.
*/
protected int readSamples ()
throws IOException
{
int samples;
while ((samples = _dsp.synthesis_pcmout(_pcm, _offsets)) <= 0) {
if (samples == 0 && !readPacket()) {
return 0;
}
if (_block.synthesis(_packet) == 0) {
_dsp.synthesis_blockin(_block);
}
}
return samples;
}
/**
* Reads a packet.
*
* @return true if a packet was read, false if we've reached the end of the stream.
*/
protected boolean readPacket ()
throws IOException
{
int result;
while ((result = _stream.packetout(_packet)) != 1) {
if (result == 0 && !readPage()) {
return false;
}
}
return true;
}
/**
* Reads in a page.
*
* @return true if a page was read, false if we've reached the end of the stream.
*/
protected boolean readPage ()
throws IOException
{
int result;
while ((result = _sync.pageout(_page)) != 1) {
if (result == 0 && !readChunk()) {
return false;
}
}
_stream.pagein(_page);
return true;
}
/**
* Reads in a chunk of data from the underlying input stream.
*
* @return true if a chunk was read, false if we've reached the end of the stream.
*/
protected boolean readChunk ()
throws IOException
{
int offset = _sync.buffer(BUFFER_SIZE);
int bytes = _in.read(_sync.data, offset, BUFFER_SIZE);
if (bytes > 0) {
_sync.wrote(bytes);
return true;
}
return false;
}
/** The underlying input stream. */
protected InputStream _in;
/** The sync state. */
protected SyncState _sync = new SyncState();
/** The stream state. */
protected StreamState _stream = new StreamState();
/** The output page. */
protected Page _page = new Page();
/** The output packet. */
protected Packet _packet = new Packet();
/** The stream info. */
protected Info _info = new Info();
/** The DSP state. */
protected DspState _dsp = new DspState();
/** The output block. */
protected Block _block = new Block(_dsp);
/** Holds the decoded PCM data buffers. */
protected float[][][] _pcm = new float[1][][];
/** The DSP offsets for each channel. */
protected int[] _offsets;
/** Intermediate storage for converted data. */
protected short[] _data = new short[0];
/** The decode buffer size. */
protected static final int BUFFER_SIZE = 4096 * 2;
}
@@ -0,0 +1,469 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.Map;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import org.lwjgl.openal.AL10;
import org.lwjgl.util.WaveData;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.samskivert.util.BasicRunQueue;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.ResultListener;
import com.samskivert.util.RunQueue;
import com.threerings.media.FrameManager;
import com.threerings.media.sound.JavaSoundPlayer;
import com.threerings.media.sound.SoundLoader;
import com.threerings.media.sound.SoundPlayer;
import com.threerings.media.timer.MediaTimer;
import static com.threerings.media.Log.log;
/**
* Implements the abstract pieces of {@link SoundPlayer} via OpenAL.
*/
public class OpenALSoundPlayer extends SoundPlayer
implements ClipProvider
{
public OpenALSoundPlayer (SoundLoader loader)
{
_loader = loader;
try {
_alSoundManager = createSoundManager();
_group = _alSoundManager.createGroup(this, SOURCE_COUNT);
} catch (Throwable t) {
log.warning("Unable to initialize OpenAL", "cause", t);
}
_ticker.start();
}
public Clip loadClip (String path)
throws IOException
{
int bundleEnd = path.lastIndexOf(":");
InputStream sound = _loader.getSound(path.substring(0, bundleEnd),
path.substring(bundleEnd + 1));
if (path.endsWith(".ogg")) {
try {
AudioInputStream instream = JavaSoundPlayer.setupAudioStream(sound);
ByteArrayOutputStream outstream = new ByteArrayOutputStream();
byte[] buf = new byte[16 * 1024];
int read;
do {
read = instream.read(buf, 0, buf.length);
if (read >= 0) {
outstream.write(buf, 0, read);
}
} while (read >= 0);
byte[] audio = outstream.toByteArray();
AudioFormat format = instream.getFormat();
long length = audio.length / format.getFrameSize();
instream = new AudioInputStream(new ByteArrayInputStream(audio), format, length);
outstream = new ByteArrayOutputStream();
AudioSystem.write(instream, AudioFileFormat.Type.WAVE, outstream);
sound = new ByteArrayInputStream(outstream.toByteArray());
} catch (Exception e) {
log.warning("Error decompressing audio clip", "path", path, e);
return new Clip();
}
}
return new Clip(WaveData.create(sound));
}
/**
* Returns the loader used by this player.
*/
public SoundLoader getSoundLoader ()
{
return _loader;
}
@Override
public RunQueue getSoundQueue ()
{
return _ticker;
}
@Override
public void setClipVolume (final float vol)
{
super.setClipVolume(vol);
getSoundQueue().postRunnable(new Runnable() {
public void run () {
_alSoundManager.setBaseGain(vol);
}});
}
@Override
public void lock (String pkgPath, String... keys)
{
for (String key : keys) {
for (final String path : getPaths(pkgPath, key)) {
getSoundQueue().postRunnable(new Runnable() {
public void run () {
if (_locked.containsKey(path)) {
return;
}
_alSoundManager.loadClip(OpenALSoundPlayer.this, path,
new ClipBuffer.Observer() {
public void clipFailed (ClipBuffer buffer) {
log.warning("Unable to load sound", "path", path);
}
public void clipLoaded (ClipBuffer buffer) {
_locked.put(path, buffer);
}
});
}
});
}
}
}
@Override
public void unlock (final String pkgPath, String... keys)
{
for (final String key : keys) {
getSoundQueue().postRunnable(new Runnable() {
public void run () {
for (String path : getPaths(pkgPath, key)) {
_locked.remove(path);
}
}
});
}
}
/**
* Streams ogg files from the given bundle and path.
*/
public void stream (final String bundle, final String path, final boolean loop,
final ResultListener<Stream> listener)
throws IOException
{
if (!path.endsWith(".ogg")) {
log.warning("Unknown file type for streaming", "bundle", bundle, "path", path);
return;
}
InputStream rsrc = _loader.getSound(bundle, path);
final StreamDecoder dec = new OggStreamDecoder();
dec.init(rsrc);
getSoundQueue().postRunnable(new Runnable() {
public void run () {
Stream s = new Stream(_alSoundManager) {
@Override
protected void update (float time) {
super.update(time);
if (_state != AL10.AL_PLAYING) {
return;
}
super.setGain(_clipVol * _streamGain);
}
@Override
public void setGain (float gain) {
_streamGain = gain;
super.setGain(_clipVol * _streamGain);
}
@Override
protected int getFormat () {
return dec.getFormat();
}
@Override
protected int getFrequency () {
return dec.getFrequency();
}
@Override
protected int populateBuffer (ByteBuffer buf) throws IOException {
int read = dec.read(buf);
if (buf.hasRemaining() && loop) {
dec.init(_loader.getSound(bundle, path));
read = Math.max(0, read);
read += dec.read(buf);
}
return read;
}
protected float _streamGain = 1F;
};
s.setGain(_clipVol);
listener.requestCompleted(s);
}});
}
@Override
public Frob loop (String pkgPath, String key, float pan)
{
return loop(pkgPath, key, pan, 1f);
}
public Frob loop (String pkgPath, String key, float pan, float gain)
{
return loop(null, pkgPath, key, gain, null);
}
public Frob loop (SoundType type, String pkgPath, String key, final float gain,
final float[] pos)
{
if (!shouldPlay(type)) {
return null;
}
final SoundGrabber loader = new SoundGrabber(pkgPath, key) {
@Override
protected void soundLoaded () {
sound.setGain(gain);
if (pos != null) {
sound.setPosition(pos[0], pos[1], pos[2]);
}
sound.loop(true);
}};
getSoundQueue().postRunnable(loader);
return new Frob() {
public float getPan () {
return 0;
}
public float getVolume () {
return 0;
}
public void setPan (float pan) {}
public void setVolume (float vol) {}
public void stop () {
getSoundQueue().postRunnable(new Runnable(){
public void run () {
if (loader.sound != null) {
loader.sound.stop();
}
}});
}};
}
@Override
public void play (String pkgPath, String key, float pan)
{
play(pkgPath, key, pan, 1f);
}
public void play (String pkgPath, String key, float pan, final float gain)
{
play(null, pkgPath, key, gain, null);
}
public boolean play (SoundType type, String pkgPath, String key, final float gain,
final float[] pos)
{
if (!shouldPlay(type)) {
return false;
}
getSoundQueue().postRunnable(new SoundGrabber(pkgPath, key) {
@Override
protected void soundLoaded () {
sound.setGain(gain);
if (pos != null) {
sound.setPosition(pos[0], pos[1], pos[2]);
}
sound.play(true);
}
});
return true;
}
@Override
public void shutdown ()
{
getSoundQueue().postRunnable(new Runnable() {
public void run () {
_group.dispose();
_locked.clear();
for (Stream stream : _alSoundManager.getStreams()) {
stream.dispose();
}
}
});
}
/**
* Returns bundle:path for all sounds under key in pkgPath.
*/
protected String[] getPaths (String pkgPath, String key)
{
String bundle = _loader.getBundle(pkgPath);
Preconditions.checkNotNull(bundle,
"Unable to find the bundle name for a package [package=%s, key=%s]", pkgPath, key);
String[] names = _loader.getPaths(pkgPath, key);
Preconditions.checkNotNull(names, "No such sound [package=%s, key=%s]", pkgPath, key);
String[] paths = new String[names.length];
for (int ii = 0; ii < paths.length; ii++) {
paths[ii] = bundle + ":" + names[ii];
}
return paths;
}
/**
* Creates our SoundManager.
*/
protected SoundManager createSoundManager ()
{
return new MediaALSoundManager();
}
/**
* Extends sound manager to allow sounds to be pulled out of the locked map.
*/
protected class MediaALSoundManager extends SoundManager {
protected MediaALSoundManager () {
super(getSoundQueue());
}
@Override
protected ClipBuffer getClip (ClipProvider provider, String path) {
if (_locked.containsKey(path)) {
return _locked.get(path);
}
return super.getClip(provider, path, null);
}
}
/**
* Updates the sound manager's streams every STREAM_UPDATE_INTERVAL and processes sound
* runnables added to its queue.
*/
protected class TickingQueue extends BasicRunQueue
{
public TickingQueue ()
{
super("SoundPlayerQueue");
}
@Override
protected void iterate ()
{
long elapsed = _timer.getElapsedMillis() - _lastTick;
Runnable r;
if (elapsed >= STREAM_UPDATE_INTERVAL) {
r = _queue.getNonBlocking();
} else {
r = _queue.get(STREAM_UPDATE_INTERVAL - elapsed);
}
long newTime;
if (_alSoundManager == null) {
// We weren't able to initialize the sound system, and we logged it earlier, so
// just empty the queue and update the tick time without running the code that
// needs the sound manager.
newTime = _timer.getElapsedMillis();
} else {
if (r != null) {
try {
r.run();
} catch (Throwable t) {
log.warning("Runnable posted to SoundPlayerQueue barfed.", t);
}
}
newTime = _timer.getElapsedMillis();
try {
_alSoundManager.updateStreams((newTime - _lastTick) / 1000F);
} catch (Throwable t) {
log.warning("Updating OpenAL streams barfed.", t);
}
}
_lastTick = newTime;
}
protected MediaTimer _timer = FrameManager.createTimer();
protected long _lastTick;
}
/**
* Loads a sound in its run method and calls subclasses with soundLoaded to let them know it's
* ready.
*/
protected abstract class SoundGrabber
implements Runnable
{
public String path;
public Sound sound;
public SoundGrabber (String pkgPath, String key)
{
String[] paths = getPaths(pkgPath, key);
path = paths[RandomUtil.getInt(paths.length)];
}
public void run ()
{
sound = _group.getSound(path);
soundLoaded();
}
protected abstract void soundLoaded ();
}
/** Number of milliseconds to wait between stream updates. */
protected static final int STREAM_UPDATE_INTERVAL = 100;
protected TickingQueue _ticker = new TickingQueue();
protected Map<String, ClipBuffer> _locked = Maps.newHashMap();
protected SoundLoader _loader;
protected SoundGroup _group;
protected SoundManager _alSoundManager;
/** Number of sounds that can be played simultaneously. */
protected final int SOURCE_COUNT = 10;
}
@@ -0,0 +1,473 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
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 a reference to the group to which the sound belongs.
*/
public SoundGroup getGroup ()
{
return _group;
}
/**
* Returns the buffer of audio data associated with this sound.
*/
public ClipBuffer getBuffer ()
{
return _buffer;
}
/**
* Sets the position of the sound.
*/
public void setPosition (float x, float y, float z)
{
if (_source != null) {
_source.setPosition(x, y, z);
}
_px = x;
_py = y;
_pz = z;
}
/**
* Sets the velocity of the sound.
*/
public void setVelocity (float x, float y, float z)
{
if (_source != null) {
_source.setVelocity(x, y, z);
}
_vx = x;
_vy = y;
_vz = z;
}
/**
* Sets the gain of the sound (which will be multiplied by the base gain).
*/
public void setGain (float gain)
{
_gain = gain;
updateSourceGain();
}
/**
* Sets whether or not the position, velocity, etc., of the sound are relative to the
* listener.
*/
public void setSourceRelative (boolean relative)
{
if (_source != null) {
_source.setSourceRelative(relative);
}
_sourceRelative = relative;
}
/**
* Sets the minimum gain.
*/
public void setMinGain (float gain)
{
if (_source != null) {
_source.setMinGain(gain);
}
_minGain = gain;
}
/**
* Sets the maximum gain.
*/
public void setMaxGain (float gain)
{
if (_source != null) {
_source.setMaxGain(gain);
}
_maxGain = gain;
}
/**
* Sets the reference distance for attenuation.
*/
public void setReferenceDistance (float distance)
{
if (_source != null) {
_source.setReferenceDistance(distance);
}
_referenceDistance = distance;
}
/**
* Sets the rolloff factor for attenuation.
*/
public void setRolloffFactor (float rolloff)
{
if (_source != null) {
_source.setRolloffFactor(rolloff);
}
_rolloffFactor = rolloff;
}
/**
* Sets the maximum distance for attenuation.
*/
public void setMaxDistance (float distance)
{
if (_source != null) {
_source.setMaxDistance(distance);
}
_maxDistance = distance;
}
/**
* Sets the pitch multiplier.
*/
public void setPitch (float pitch)
{
if (_source != null) {
_source.setPitch(pitch);
}
_pitch = pitch;
}
/**
* Sets the direction of the sound.
*/
public void setDirection (float x, float y, float z)
{
if (_source != null) {
_source.setDirection(x, y, z);
}
_dx = x;
_dy = y;
_dz = z;
}
/**
* Sets the inside angle of the sound cone.
*/
public void setConeInnerAngle (float angle)
{
if (_source != null) {
_source.setConeInnerAngle(angle);
}
_coneInnerAngle = angle;
}
/**
* Sets the outside angle of the sound cone.
*/
public void setConeOuterAngle (float angle)
{
if (_source != null) {
_source.setConeOuterAngle(angle);
}
_coneOuterAngle = angle;
}
/**
* Sets the gain outside of the sound cone.
*/
public void setConeOuterGain (float gain)
{
if (_source != null) {
_source.setConeOuterGain(gain);
}
_coneOuterGain = 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 boolean play (StartObserver obs, boolean loop)
{
return 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 (_source != null) {
_source.pause();
} else {
_stateDesired = AL10.AL_PAUSED;
}
}
/**
* Stops this sound and rewinds to its beginning. The audio channel being used to play the
* sound will be released.
*/
public void stop ()
{
if (_source != null) {
_source.stop();
} else {
_stateDesired = AL10.AL_STOPPED;
}
}
/**
* Called to check if this sound is currently playing.
*/
public boolean isPlaying ()
{
return _source != null && _source.isPlaying();
}
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) {
// save the desired state, which may be overridden by calls to play/pause/stop
_stateDesired = AL10.AL_PLAYING;
_loopDesired = loop;
// resolve the buffer and instruct it to play once it is resolved
_buffer.resolve(new ClipBuffer.Observer() {
public void clipLoaded (ClipBuffer buffer) {
if (_stateDesired == AL10.AL_STOPPED) {
return;
}
play(false, _loopDesired, obs);
if (_stateDesired == AL10.AL_PAUSED) {
pause();
}
}
public void clipFailed (ClipBuffer buffer) {
// well, let's pretend like the sound started so that the observer isn't
// left hanging
if (obs != null && _stateDesired != AL10.AL_STOPPED) {
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 (_source == null) {
_source = _group.acquireSource(this);
if (_source == null) {
return false;
}
// bind our clip buffer to the source and notify it
_source.setBuffer(_buffer.getBuffer());
_buffer.sourceBound();
// configure the source with our ephemera
_source.setPosition(_px, _py, _pz);
_source.setVelocity(_vx, _vy, _vz);
updateSourceGain();
_source.setSourceRelative(_sourceRelative);
_source.setMinGain(_minGain);
_source.setMaxGain(_maxGain);
_source.setReferenceDistance(_referenceDistance);
_source.setRolloffFactor(_rolloffFactor);
_source.setMaxDistance(_maxDistance);
_source.setPitch(_pitch);
_source.setDirection(_dx, _dy, _dz);
_source.setConeInnerAngle(_coneInnerAngle);
_source.setConeOuterAngle(_coneOuterAngle);
_source.setConeOuterGain(_coneOuterGain);
}
// configure whether or not we should loop
_source.setLooping(loop);
// and start that damned thing up!
_source.play();
return true;
}
/**
* Updates the source gain according to our configured gain and the base gain.
*/
protected void updateSourceGain ()
{
if (_source != null) {
_source.setGain(_gain * _group.getInheritedBaseGain());
}
}
/**
* 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 (_source != null && _source.isStopped()) {
_source.setBuffer(null);
_buffer.sourceUnbound();
_source = null;
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 Source _source;
/** The desired state of the sound (stopped, playing, paused) after resolution. */
protected int _stateDesired;
/** Whether or not looping is desired after resolution. */
protected boolean _loopDesired;
/** The position of the sound. */
protected float _px, _py, _pz;
/** The velocity of the sound. */
protected float _vx, _vy, _vz;
/** The gain of the sound. */
protected float _gain = 1f;
/** Whether or not the sound's position, velocity, etc. are relative to the listener. */
protected boolean _sourceRelative;
/** The minimum gain. */
protected float _minGain;
/** The maximum gain. */
protected float _maxGain = 1f;
/** The reference distance for attenuation. */
protected float _referenceDistance = 1f;
/** The attenuation rolloff factor. */
protected float _rolloffFactor = 1f;
/** The maximum distance for attenuation. */
protected float _maxDistance = Float.MAX_VALUE;
/** The pitch multiplier. */
protected float _pitch = 1f;
/** The direction of the sound. */
protected float _dx, _dy, _dz;
/** The inside angle of the sound cone. */
protected float _coneInnerAngle = 360f;
/** The outside angle of the sound cone. */
protected float _coneOuterAngle = 360f;
/** The gain outside the sound cone. */
protected float _coneOuterGain;
}
@@ -0,0 +1,195 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.ArrayList;
import org.lwjgl.openal.AL10;
import com.google.common.collect.Lists;
import static com.threerings.openal.Log.log;
/**
* 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
{
/**
* Sets the base gain for this group, or -1 to inherit from the manager (the default).
*/
public void setBaseGain (float gain)
{
if (_baseGain != gain) {
_baseGain = gain;
baseGainChanged();
}
}
/**
* Returns the base gain for this group, or -1 if inherited from the manager.
*/
public float getBaseGain ()
{
return _baseGain;
}
/**
* 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 ()
{
reclaimAll();
for (PooledSource pooled : _sources) {
pooled.source.delete();
}
_sources.clear();
// remove from the manager
_manager.removeGroup(this);
}
/**
* Stops and reclaims all sounds from this sound group but does not free the sources.
*/
public void reclaimAll ()
{
// make sure any bound sources are released
for (PooledSource pooled : _sources) {
if (pooled.holder != null) {
pooled.holder.stop();
pooled.holder.reclaim();
pooled.holder = null;
}
}
}
protected SoundGroup (SoundManager manager, ClipProvider provider, int sources)
{
_manager = manager;
_provider = provider;
// register with the manager
_manager.addGroup(this);
// 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 (or as many of them as we can)
for (int ii = 0; ii < sources; ii++) {
PooledSource pooled = new PooledSource();
pooled.source = new Source(manager);
int errno = AL10.alGetError();
if (errno != AL10.AL_NO_ERROR) {
log.warning("Failed to create sources [cprov=" + provider +
", sources=" + sources + ", errno=" + errno + "].");
return;
}
_sources.add(pooled);
}
}
/**
* Called by a {@link Sound} when it wants to obtain a source on which to play its clip.
*/
protected Source 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++) {
PooledSource pooled = _sources.get(ii);
if (pooled.holder == null || pooled.holder.reclaim()) {
// note this source's new holder
pooled.holder = acquirer;
// move this source to the end of the list
_sources.remove(ii);
_sources.add(pooled);
return pooled.source;
}
}
return null;
}
/**
* Used to pass the base gain through to sound effects.
*/
protected float getInheritedBaseGain ()
{
return (_baseGain < 0f) ? _manager.getBaseGain() : _baseGain;
}
/**
* Called by the manager when the base gain has changed.
*/
protected void baseGainChanged ()
{
// notify any sound currently holding a source
for (int ii = 0, nn = _sources.size(); ii < nn; ii++) {
Sound holder = _sources.get(ii).holder;
if (holder != null) {
holder.updateSourceGain();
}
}
}
/** Used to track which sources are in use. */
protected static class PooledSource
{
public Source source;
public Sound holder;
}
protected SoundManager _manager;
protected ClipProvider _provider;
protected ArrayList<PooledSource> _sources = Lists.newArrayList();
/** The base gain, or -1 to inherit from the manager. */
protected float _baseGain = -1f;
}
@@ -0,0 +1,461 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.Queue;
import com.samskivert.util.RunQueue;
import com.threerings.openal.ClipBuffer.Observer;
import static com.threerings.openal.Log.log;
/**
* 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;
}
/**
* Shuts down the sound manager.
*/
public void shutdown ()
{
if (isInitialized()) {
AL.destroy();
}
}
/**
* 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);
}
/**
* Returns a reference to the listener object.
*/
public Listener getListener ()
{
return _listener;
}
/**
* 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)
{
if (_baseGain == gain) {
return;
}
_baseGain = gain;
// alert the groups that inherite the gain
for (int ii = 0, nn = _groups.size(); ii < nn; ii++) {
SoundGroup group = _groups.get(ii);
if (group.getBaseGain() < 0f) {
group.baseGainChanged();
}
}
}
/**
* 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);
}
// delete any finalized objects
deleteFinalizedObjects();
}
/**
* Loads a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The loaded clip is placed in the cache.
*/
public void loadClip (ClipProvider provider, String path)
{
loadClip(provider, path, null);
}
/**
* Loads a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The loaded clip is placed in the cache.
*/
public void loadClip (ClipProvider provider, String path, Observer observer)
{
getClip(provider, path, observer);
}
/**
* 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.", 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<String, ClipBuffer>() {
public void removedFromMap (LRUHashMap<String, 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<ClipBuffer>();
// 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 the cache, and it will immediately be queued
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path)
{
return getClip(provider, path, null);
}
/**
* Creates a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The clip buffer may come from the cache, and it will immediately be queued
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path, Observer observer)
{
String 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(observer);
return buffer;
} catch (Throwable t) {
log.warning("Failure resolving buffer [key=" + ckey + "].", 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);
}
/**
* Adds a group to the list maintained by the manager. Called by groups when they are created.
*/
protected void addGroup (SoundGroup group)
{
_groups.add(group);
}
/**
* Removes a group from the list maintained by the manager. Called by groups when they are
* disposed.
*/
protected void removeGroup (SoundGroup group)
{
_groups.remove(group);
}
/**
* Called when a source has been finalized.
*/
protected synchronized void sourceFinalized (int id)
{
_finalizedSources = IntListUtil.add(_finalizedSources, id);
}
/**
* Called when a buffer has been finalized.
*/
protected synchronized void bufferFinalized (int id)
{
_finalizedBuffers = IntListUtil.add(_finalizedBuffers, id);
}
/**
* Deletes all finalized objects.
*/
protected synchronized void deleteFinalizedObjects ()
{
if (_finalizedSources != null) {
IntBuffer idbuf = BufferUtils.createIntBuffer(_finalizedSources.length);
idbuf.put(_finalizedSources).rewind();
AL10.alDeleteSources(idbuf);
_finalizedSources = null;
}
if (_finalizedBuffers != null) {
IntBuffer idbuf = BufferUtils.createIntBuffer(_finalizedBuffers.length);
idbuf.put(_finalizedBuffers).rewind();
AL10.alDeleteBuffers(idbuf);
_finalizedBuffers = null;
}
}
/** The thread that loads up sound clips in the background. */
protected Thread _loader = new Thread("SoundManager.Loader") {
@Override
public void run () {
while (true) {
final ClipBuffer buffer = _toLoad.get();
try {
log.debug("Loading " + buffer.getKey() + ".");
final Clip clip = buffer.load();
_rqueue.postRunnable(new Runnable() {
public void run () {
String 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() + "].", 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;
/** The listener object. */
protected Listener _listener = new Listener();
/** 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<String, ClipBuffer> _loading = Maps.newHashMap();
/** Contains a mapping of all loaded clips. */
protected LRUHashMap<String, ClipBuffer> _clips =
new LRUHashMap<String, ClipBuffer>(DEFAULT_CACHE_SIZE, _sizer);
/** Contains a queue of clip buffers waiting to be loaded. */
protected Queue<ClipBuffer> _toLoad;
/** The list of active streams. */
protected ArrayList<Stream> _streams = Lists.newArrayList();
/** The list of active groups. */
protected List<SoundGroup> _groups = Lists.newArrayList();
/** The list of sources to be deleted. */
protected int[] _finalizedSources;
/** The list of buffers to be deleted. */
protected int[] _finalizedBuffers;
/** 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;
}
@@ -0,0 +1,426 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.ArrayList;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.AL11;
import com.google.common.collect.Lists;
/**
* Represents an OpenAL source object.
*/
public class Source
{
/**
* Creates a new source for the specified sound manager.
*/
public Source (SoundManager soundmgr)
{
_soundmgr = soundmgr;
IntBuffer idbuf = BufferUtils.createIntBuffer(1);
AL10.alGenSources(idbuf);
_id = idbuf.get(0);
}
/**
* Returns this source's OpenAL identifier.
*/
public final int getId ()
{
return _id;
}
/**
* Sets the position of the source.
*/
public void setPosition (float x, float y, float z)
{
if (_px != x || _py != y || _pz != z) {
AL10.alSource3f(_id, AL10.AL_POSITION, _px = x, _py = y, _pz = z);
}
}
/**
* Sets the velocity of the source.
*/
public void setVelocity (float x, float y, float z)
{
if (_vx != x || _vy != y || _vz != z) {
AL10.alSource3f(_id, AL10.AL_VELOCITY, _vx = x, _vy = y, _vz = z);
}
}
/**
* Sets the gain of the source.
*/
public void setGain (float gain)
{
if (_gain != gain) {
AL10.alSourcef(_id, AL10.AL_GAIN, _gain = gain);
}
}
/**
* Sets whether or not the position, velocity, etc., of the source are relative to the
* listener.
*/
public void setSourceRelative (boolean relative)
{
if (_sourceRelative != relative) {
_sourceRelative = relative;
AL10.alSourcei(_id, AL10.AL_SOURCE_RELATIVE, relative ? AL10.AL_TRUE : AL10.AL_FALSE);
}
}
/**
* Sets whether or not the source is looping.
*/
public void setLooping (boolean looping)
{
if (_looping != looping) {
_looping = looping;
AL10.alSourcei(_id, AL10.AL_LOOPING, looping ? AL10.AL_TRUE : AL10.AL_FALSE);
}
}
/**
* Sets the minimum gain.
*/
public void setMinGain (float gain)
{
if (_minGain != gain) {
AL10.alSourcef(_id, AL10.AL_MIN_GAIN, _minGain = gain);
}
}
/**
* Sets the maximum gain.
*/
public void setMaxGain (float gain)
{
if (_maxGain != gain) {
AL10.alSourcef(_id, AL10.AL_MAX_GAIN, _maxGain = gain);
}
}
/**
* Sets the reference distance for attenuation.
*/
public void setReferenceDistance (float distance)
{
if (_referenceDistance != distance) {
AL10.alSourcef(_id, AL10.AL_REFERENCE_DISTANCE, _referenceDistance = distance);
}
}
/**
* Sets the rolloff factor for attenuation.
*/
public void setRolloffFactor (float rolloff)
{
if (_rolloffFactor != rolloff) {
AL10.alSourcef(_id, AL10.AL_ROLLOFF_FACTOR, _rolloffFactor = rolloff);
}
}
/**
* Sets the maximum distance for attenuation.
*/
public void setMaxDistance (float distance)
{
if (_maxDistance != distance) {
AL10.alSourcef(_id, AL10.AL_MAX_DISTANCE, _maxDistance = distance);
}
}
/**
* Sets the pitch multiplier.
*/
public void setPitch (float pitch)
{
if (_pitch != pitch) {
AL10.alSourcef(_id, AL10.AL_PITCH, _pitch = pitch);
}
}
/**
* Sets the direction of the source.
*/
public void setDirection (float x, float y, float z)
{
if (_dx != x || _dy != y || _dz != z) {
AL10.alSource3f(_id, AL10.AL_DIRECTION, _dx = x, _dy = y, _dz = z);
}
}
/**
* Sets the inside angle of the sound cone.
*/
public void setConeInnerAngle (float angle)
{
if (_coneInnerAngle != angle) {
AL10.alSourcef(_id, AL10.AL_CONE_INNER_ANGLE, _coneInnerAngle = angle);
}
}
/**
* Sets the outside angle of the sound cone.
*/
public void setConeOuterAngle (float angle)
{
if (_coneOuterAngle != angle) {
AL10.alSourcef(_id, AL10.AL_CONE_OUTER_ANGLE, _coneOuterAngle = angle);
}
}
/**
* Sets the gain outside of the sound cone.
*/
public void setConeOuterGain (float gain)
{
if (_coneOuterGain != gain) {
AL10.alSourcef(_id, AL10.AL_CONE_OUTER_GAIN, _coneOuterGain = gain);
}
}
/**
* Sets the source buffer. Equivalent to unqueueing all buffers, then queuing the provided
* buffer. Cannot be called when the source is playing or paused.
*
* @param buffer the buffer to set, or <code>null</code> to clear.
*/
public void setBuffer (Buffer buffer)
{
_queue.clear();
if (buffer != null) {
_queue.add(buffer);
}
AL10.alSourcei(_id, AL10.AL_BUFFER, buffer == null ? AL10.AL_NONE : buffer.getId());
}
/**
* Enqueues the specified buffers.
*/
public void queueBuffers (Buffer... buffers)
{
IntBuffer idbuf = BufferUtils.createIntBuffer(buffers.length);
for (int ii = 0; ii < buffers.length; ii++) {
Buffer buffer = buffers[ii];
_queue.add(buffer);
idbuf.put(ii, buffer.getId());
}
AL10.alSourceQueueBuffers(_id, idbuf);
}
/**
* Removes the specified buffers from the queue.
*/
public void unqueueBuffers (Buffer... buffers)
{
IntBuffer idbuf = BufferUtils.createIntBuffer(buffers.length);
for (int ii = 0; ii < buffers.length; ii++) {
Buffer buffer = buffers[ii];
_queue.remove(buffer);
idbuf.put(ii, buffer.getId());
}
AL10.alSourceUnqueueBuffers(_id, idbuf);
}
/**
* Determines whether the source is playing.
*/
public boolean isPlaying ()
{
return getSourceState() == AL10.AL_PLAYING;
}
/**
* Determines whether the source is paused.
*/
public boolean isPaused ()
{
return getSourceState() == AL10.AL_PAUSED;
}
/**
* Determines whether the source is stopped.
*/
public boolean isStopped ()
{
return getSourceState() == AL10.AL_STOPPED;
}
/**
* Returns the state of the source: {@link AL10#AL_INITIAL}, {@link AL10#AL_PLAYING},
* {@link AL10#AL_PAUSED}, or {@link AL10#AL_STOPPED}.
*/
public int getSourceState ()
{
return AL10.alGetSourcei(_id, AL10.AL_SOURCE_STATE);
}
/**
* Returns the number of buffers that have been processed.
*/
public int getBuffersProcessed ()
{
return AL10.alGetSourcei(_id, AL10.AL_BUFFERS_PROCESSED);
}
/**
* Returns the position offset of the source within the queued buffers, in seconds.
*/
public float getSecOffset ()
{
return AL10.alGetSourcef(_id, AL11.AL_SEC_OFFSET);
}
/**
* Returns the position offset of the source within the queued buffers, in samples.
*/
public int getSampleOffset ()
{
return AL10.alGetSourcei(_id, AL11.AL_SAMPLE_OFFSET);
}
/**
* Returns the position offset of the source within the queued buffers, in bytes.
*/
public int getByteOffset ()
{
return AL10.alGetSourcei(_id, AL11.AL_BYTE_OFFSET);
}
/**
* Starts playing the source.
*/
public void play ()
{
AL10.alSourcePlay(_id);
}
/**
* Pauses the source.
*/
public void pause ()
{
AL10.alSourcePause(_id);
}
/**
* Stops the source.
*/
public void stop ()
{
AL10.alSourceStop(_id);
}
/**
* Rewinds the source.
*/
public void rewind ()
{
AL10.alSourceRewind(_id);
}
/**
* Deletes this source, rendering it unusable.
*/
public void delete ()
{
IntBuffer idbuf = BufferUtils.createIntBuffer(1);
idbuf.put(_id).rewind();
AL10.alDeleteSources(idbuf);
_id = 0;
_queue.clear();
}
@Override
protected void finalize ()
throws Throwable
{
super.finalize();
if (_id > 0) {
_soundmgr.sourceFinalized(_id);
}
}
/** The sound manager responsible for this source. */
protected SoundManager _soundmgr;
/** The OpenAL identifier for this source. */
protected int _id;
/** The position of the source. */
protected float _px, _py, _pz;
/** The velocity of the source. */
protected float _vx, _vy, _vz;
/** The gain of the source. */
protected float _gain = 1f;
/** Whether or not the source's position, velocity, etc. are relative to the listener. */
protected boolean _sourceRelative;
/** Whether or not the source is looping. */
protected boolean _looping;
/** The minimum gain. */
protected float _minGain;
/** The maximum gain. */
protected float _maxGain = 1f;
/** The reference distance for attenuation. */
protected float _referenceDistance = 1f;
/** The attenuation rolloff factor. */
protected float _rolloffFactor = 1f;
/** The maximum distance for attenuation. */
protected float _maxDistance = Float.MAX_VALUE;
/** The pitch multiplier. */
protected float _pitch = 1f;
/** The direction of the source. */
protected float _dx, _dy, _dz;
/** The inside angle of the sound cone. */
protected float _coneInnerAngle = 360f;
/** The outside angle of the sound cone. */
protected float _coneOuterAngle = 360f;
/** The gain outside the sound cone. */
protected float _coneOuterGain;
/** The source's queue of buffers (storing them keeps them from being garbage-collected). */
protected ArrayList<Buffer> _queue = Lists.newArrayList();
}
@@ -0,0 +1,339 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import org.lwjgl.openal.AL10;
import static com.threerings.openal.Log.log;
/**
* 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
_source = new Source(soundmgr);
for (int ii = 0; ii < _buffers.length; ii++) {
_buffers[ii] = new Buffer(soundmgr);
}
// 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) {
_source.setGain(_gain);
}
}
/**
* Returns a reference to the stream source.
*/
public Source getSource ()
{
return _source;
}
/**
* 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(_buffers.length);
}
_source.play();
_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;
}
_source.pause();
_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;
}
_source.stop();
_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();
}
_source.setGain(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
_source.delete();
for (Buffer buffer : _buffers) {
buffer.delete();
}
// 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 = _source.getBuffersProcessed();
if (played == 0) {
return;
}
for (int ii = 0; ii < played; ii++) {
_source.unqueueBuffers(_buffers[_qidx]);
_qidx = (_qidx + 1) % _buffers.length;
_qlen--;
}
// 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 = _source.getSourceState();
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);
_source.setGain(_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)
{
for (int ii = 0; ii < buffers; ii++) {
Buffer buffer = _buffers[(_qidx + _qlen) % _buffers.length];
if (populateBuffer(buffer)) {
_source.queueBuffers(buffer);
_qlen++;
} else {
break;
}
}
}
/**
* 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 (Buffer buffer)
{
if (_abuf == null) {
_abuf = ByteBuffer.allocateDirect(getBufferSize()).order(ByteOrder.nativeOrder());
}
_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);
buffer.setData(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;
/**
* Returns the size in bytes of the buffers to use.
*/
protected int getBufferSize ()
{
return 131072;
}
/**
* Returns the number of buffers to use.
*/
protected int getNumBuffers ()
{
return 4;
}
/** The manager to which the stream was added. */
protected SoundManager _soundmgr;
/** The source through which the stream plays. */
protected Source _source;
/** The buffers through which we cycle. */
protected Buffer[] _buffers = new Buffer[getNumBuffers()];
/** The starting index and length of the current queue in {@link #_buffers}. */
protected int _qidx, _qlen;
/** 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;
/** Fading modes. */
protected enum FadeMode { NONE, IN, OUT, OUT_DISPOSE }
}
@@ -0,0 +1,112 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.HashMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import com.google.common.collect.Maps;
import static com.threerings.openal.Log.log;
/**
* Decodes audio streams from data read from an {@link InputStream}.
*/
public abstract class StreamDecoder
{
/**
* Registers a class of {@link StreamDecoder} for the specified file extension.
*/
public static void registerExtension (String extension, Class<? extends StreamDecoder> clazz)
{
_extensions.put(extension, clazz);
}
/**
* Creates and initializes a stream decoder for the specified file.
*/
public static StreamDecoder createInstance (File file)
throws IOException
{
String path = file.getPath();
int idx = path.lastIndexOf('.');
if (idx == -1) {
log.warning("Missing extension for file [file=" + path + "].");
return null;
}
String extension = path.substring(idx+1);
Class<? extends StreamDecoder> clazz = _extensions.get(extension);
if (clazz == null) {
log.warning("No decoder registered for extension [extension=" + extension +
", file=" + path + "].");
return null;
}
StreamDecoder decoder;
try {
decoder = clazz.newInstance();
} catch (Exception e) {
log.warning("Error instantiating decoder [file=" + path + ", error=" + e + "].");
return null;
}
decoder.init(new FileInputStream(file));
return decoder;
}
/**
* Initializes the decoder with its input stream.
*/
public abstract void init (InputStream in)
throws IOException;
/**
* Returns the sound format (see {@link Stream#getFormat}).
*/
public abstract int getFormat ();
/**
* Returns the sound frequency (see {@link Stream#getFrequency}).
*/
public abstract int getFrequency ();
/**
* Reads as much data as will fit into the specified buffer.
*
* @return the number of bytes read. If less than or equal to zero, the decoder has reached
* the end of the stream.
*/
public abstract int read (ByteBuffer buf)
throws IOException;
/** Maps file extensions to decoder classes. */
protected static HashMap<String, Class<? extends StreamDecoder>> _extensions =
Maps.newHashMap();
static {
registerExtension("ogg", OggStreamDecoder.class);
registerExtension("mp3", Mp3StreamDecoder.class);
}
}
@@ -0,0 +1,43 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.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
{
WaveData file = WaveData.create(path);
if (file == null) {
throw new IOException("Error loading " + path);
}
return new Clip(file);
}
}