diff --git a/build.xml b/build.xml
index e0368558c..8b6b3921e 100644
--- a/build.xml
+++ b/build.xml
@@ -97,6 +97,8 @@
classname="com.jme.scene.Node" classpathref="classpath"/>
+
@@ -106,6 +108,7 @@
+
diff --git a/lib/LIBS b/lib/LIBS
index df43b1484..339f58471 100644
--- a/lib/LIBS
+++ b/lib/LIBS
@@ -8,6 +8,7 @@ commons-logging.jar
jme-awt.jar
jme-bui.jar
jme-model.jar
+jme-sound.jar
jme.jar
junit-3.7.jar
lwjgl.jar
diff --git a/src/java/com/threerings/openal/OggFileStream.java b/src/java/com/threerings/openal/OggFileStream.java
new file mode 100644
index 000000000..ff46de23f
--- /dev/null
+++ b/src/java/com/threerings/openal/OggFileStream.java
@@ -0,0 +1,89 @@
+//
+// $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 org.lwjgl.openal.AL10;
+
+import com.jmex.sound.openAL.objects.util.OggInputStream;
+
+/**
+ * An audio stream read from an Ogg Vorbis file.
+ */
+public class OggFileStream extends Stream
+{
+ /**
+ * Creates a new Ogg stream for the specified Ogg file.
+ *
+ * @param loop whether or not to play the file in a continuous loop
+ */
+ public OggFileStream (SoundManager soundmgr, File file, boolean loop)
+ throws IOException
+ {
+ super(soundmgr);
+ _file = file;
+ _istream = new OggInputStream(new FileInputStream(_file));
+ _loop = 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() && _loop) {
+ _istream = new OggInputStream(new FileInputStream(_file));
+ read = Math.max(0, read);
+ read += _istream.read(buf, buf.position(), buf.remaining());
+ }
+ return read;
+ }
+
+ /** The Ogg file from which to read. */
+ protected File _file;
+
+ /** The underlying Ogg input stream. */
+ protected OggInputStream _istream;
+
+ /** Whether or not to loop back to the beginning of the file when we've
+ * reached the end. */
+ protected boolean _loop;
+}
diff --git a/src/java/com/threerings/openal/SoundManager.java b/src/java/com/threerings/openal/SoundManager.java
index 22b09497c..a74ded411 100644
--- a/src/java/com/threerings/openal/SoundManager.java
+++ b/src/java/com/threerings/openal/SoundManager.java
@@ -22,6 +22,7 @@
package com.threerings.openal;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.WeakHashMap;
@@ -103,6 +104,19 @@ public class SoundManager
return new SoundGroup(this, provider, sources);
}
+ /**
+ * 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)
+ {
+ for (Stream stream : _streams) {
+ stream.update(time);
+ }
+ }
+
/**
* Creates a sound manager and initializes the OpenAL sound subsystem.
*/
@@ -205,6 +219,24 @@ public class SoundManager
});
}
+ /**
+ * 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 () {
@@ -254,6 +286,9 @@ public class SoundManager
/** Contains a queue of clip buffers waiting to be loaded. */
protected Queue _toLoad;
+ /** The list of active streams. */
+ protected ArrayList _streams = new ArrayList();
+
/** 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;
diff --git a/src/java/com/threerings/openal/Stream.java b/src/java/com/threerings/openal/Stream.java
new file mode 100644
index 000000000..6c309c1c7
--- /dev/null
+++ b/src/java/com/threerings/openal/Stream.java
@@ -0,0 +1,272 @@
+//
+// $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;
+ 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);
+ }
+
+ /**
+ * Starts playing this stream.
+ */
+ public void play ()
+ {
+ if (_state == AL10.AL_PLAYING) {
+ Log.warning("Tried to play stream already playing.");
+ return;
+ }
+ if (_state == -1) {
+ _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;
+ }
+
+ /**
+ * 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)
+ {
+ if (_state != AL10.AL_PLAYING) {
+ return;
+ }
+ // find out how many buffers have been played
+ 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);
+ queueBuffers(played);
+ }
+
+ /**
+ * 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;
+
+ /** The gain of the stream. */
+ protected float _gain;
+
+ /** 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 (AL_STOPPED, AL_PLAYING, AL_PAUSED, or
+ * -1 if uninitialized). */
+ protected int _state = -1;
+
+ /** 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;
+}