diff --git a/libs-incl.xml b/libs-incl.xml
index dc716e82..6b0a5985 100644
--- a/libs-incl.xml
+++ b/libs-incl.xml
@@ -22,5 +22,6 @@
+
diff --git a/src/java/com/threerings/openal/Mp3FileStream.java b/src/java/com/threerings/openal/Mp3FileStream.java
new file mode 100644
index 00000000..f0eed8cc
--- /dev/null
+++ b/src/java/com/threerings/openal/Mp3FileStream.java
@@ -0,0 +1,174 @@
+//
+// $Id$
+//
+// Nenya library - tools for developing networked games
+// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/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.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+
+import java.util.ArrayList;
+
+import org.lwjgl.openal.AL10;
+
+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;
+
+/**
+ * An audio stream read from one or more mp3 files.
+ */
+public class Mp3FileStream 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 Mp3FileStream (SoundManager soundmgr, File file, boolean loop)
+ throws IOException
+ {
+ super(soundmgr);
+ _file = new Mp3File(file, loop);
+ _istream = new Bitstream(new FileInputStream(file));
+ _decoder = new Decoder();
+ }
+
+ /**
+ * 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 Mp3File(file, loop));
+ }
+
+ @Override // from Stream
+ public void dispose ()
+ {
+ super.dispose();
+
+ // TODO: close our bitstream...
+ }
+
+ // documentation inherited
+ protected int getFormat ()
+ {
+ return AL10.AL_FORMAT_STEREO16;
+ }
+
+ // documentation inherited
+ protected int getFrequency ()
+ {
+ return _frequency;
+ }
+
+ // documentation inherited
+ protected int populateBuffer (ByteBuffer buf)
+ throws IOException
+ {
+ if (_buffer == null) {
+ try {
+ Header h = _istream.readFrame();
+ for (int ii = 0; ii < 10 && (h == null); ii++) {
+ _istream.close();
+ Mp3File file = getNextFile();
+ if (file == null) {
+ return 0;
+ }
+ _istream = new Bitstream(new FileInputStream(file.file));
+ h = _istream.readFrame();
+ }
+
+ _buffer = (SampleBuffer)_decoder.decodeFrame(h, _istream);
+ _frequency = _buffer.getSampleFrequency();
+
+ } catch (JavaLayerException jle) {
+ throw (IOException)new IOException("JavaLayer decoding error").initCause(jle);
+ }
+ }
+
+ int srem = buf.remaining();
+ ShortBuffer sbuf = buf.asShortBuffer();
+ int wrote = Math.min(_buffer.getBufferLength()-_offset, sbuf.remaining());
+ sbuf.put(_buffer.getBuffer(), _offset, wrote);
+ if (_offset + wrote >= _buffer.getBufferLength()) {
+ _buffer = null;
+ _offset = 0;
+ _istream.closeFrame();
+ }
+
+ return 2 * wrote;
+ }
+
+ protected Mp3File getNextFile ()
+ {
+ if (_file.loop) {
+ return _file;
+ } else if (!_queue.isEmpty()) {
+ return _queue.remove(0);
+ } else {
+ return null;
+ }
+ }
+
+ /** The file currently being played. */
+ protected Mp3File _file;
+
+ /** Handles reading the mp3 data. */
+ protected Bitstream _istream;
+
+ /** Handles decoding the mp3 data. */
+ protected Decoder _decoder;
+
+ /** The currently decoded frame, if any. */
+ protected SampleBuffer _buffer;
+
+ /** Our offset into the currently decoded frame. */
+ protected int _offset;
+
+ /** The frequency of the most recently processed frame. */
+ protected int _frequency;
+
+ /** The queue of files to play after the current one. */
+ protected ArrayList _queue = new ArrayList();
+
+ /** A file queued for play. */
+ protected class Mp3File
+ {
+ /** 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 Mp3File (File file, boolean loop) {
+ this.file = file;
+ this.loop = loop;
+ }
+ }
+}
diff --git a/src/java/com/threerings/openal/Stream.java b/src/java/com/threerings/openal/Stream.java
index 92a06155..5ab811c3 100644
--- a/src/java/com/threerings/openal/Stream.java
+++ b/src/java/com/threerings/openal/Stream.java
@@ -35,16 +35,14 @@ import org.lwjgl.openal.AL10;
public abstract class Stream
{
/**
- * Creates a new stream. Call {@link #dispose} when finished with the
- * 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
+ * @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);
@@ -53,11 +51,11 @@ public abstract class Stream
_nbuf.clear();
AL10.alGenBuffers(_nbuf);
_nbuf.get(_bufferIds);
-
+
// register with sound manager
_soundmgr.addStream(this);
}
-
+
/**
* Sets the base gain of the stream.
*/
@@ -68,7 +66,7 @@ public abstract class Stream
AL10.alSourcef(_sourceId, AL10.AL_GAIN, _gain);
}
}
-
+
/**
* Sets the pitch of the stream.
*/
@@ -77,7 +75,7 @@ public abstract class Stream
_pitch = pitch;
AL10.alSourcef(_sourceId, AL10.AL_PITCH, _pitch);
}
-
+
/**
* Determines whether this stream is currently playing.
*/
@@ -85,7 +83,7 @@ public abstract class Stream
{
return _state == AL10.AL_PLAYING;
}
-
+
/**
* Starts playing this stream.
*/
@@ -102,7 +100,7 @@ public abstract class Stream
AL10.alSourcePlay(_sourceId);
_state = AL10.AL_PLAYING;
}
-
+
/**
* Pauses this stream.
*/
@@ -115,7 +113,7 @@ public abstract class Stream
AL10.alSourcePause(_sourceId);
_state = AL10.AL_PAUSED;
}
-
+
/**
* Stops this stream.
*/
@@ -128,10 +126,10 @@ public abstract class Stream
AL10.alSourceStop(_sourceId);
_state = AL10.AL_STOPPED;
}
-
+
/**
- * Fades this stream in over the specified interval. If the stream isn't
- * playing, it will be started.
+ * Fades this stream in over the specified interval. If the stream isn't playing, it will be
+ * started.
*/
public void fadeIn (float interval)
{
@@ -143,7 +141,7 @@ public abstract class Stream
_fadeInterval = interval;
_fadeElapsed = 0f;
}
-
+
/**
* Fades this stream out over the specified interval.
*
@@ -155,10 +153,9 @@ public abstract class Stream
_fadeInterval = interval;
_fadeElapsed = 0f;
}
-
+
/**
- * Releases the resources held by this stream and removes it from
- * the manager.
+ * Releases the resources held by this stream and removes it from the manager.
*/
public void dispose ()
{
@@ -166,7 +163,7 @@ public abstract class Stream
if (_state != AL10.AL_STOPPED) {
stop();
}
-
+
// delete the source and buffers
_nbuf.clear();
_nbuf.put(_sourceId).flip();
@@ -174,15 +171,14 @@ public abstract class Stream
_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}.
+ * 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
*/
@@ -193,7 +189,7 @@ public abstract class Stream
if (_state != AL10.AL_PLAYING) {
return;
}
-
+
// find out how many buffers have been played and unqueue them
int played = AL10.alGetSourcei(_sourceId, AL10.AL_BUFFERS_PROCESSED);
if (played == 0) {
@@ -207,18 +203,17 @@ public abstract class Stream
}
_nbuf.flip();
AL10.alSourceUnqueueBuffers(_sourceId, _nbuf);
-
+
// enqueue up to the number of buffers played
queueBuffers(played);
-
- // find out if we're still playing; if not and we have buffers queued,
- // we must restart
+
+ // find out if we're still playing; if not and we have buffers queued, we must restart
_state = AL10.alGetSourcei(_sourceId, AL10.AL_SOURCE_STATE);
if (_qlen > 0 && _state != AL10.AL_PLAYING) {
play();
}
}
-
+
/**
* Updates the gain of the stream according to the fade state.
*/
@@ -239,7 +234,7 @@ public abstract class Stream
_fadeMode = FadeMode.NONE;
}
}
-
+
/**
* Queues (up to) the specified number of buffers.
*/
@@ -258,13 +253,12 @@ public abstract class Stream
_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
+ * @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)
{
@@ -285,66 +279,65 @@ public abstract class Stream
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
+ * @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}. */
+ /** The starting index and length of the current queue in {@link #_bufferIds}. */
protected int _qidx, _qlen;
-
+
/** The pitch of the stream. */
protected float _pitch = 1f;
-
+
/** The gain of the stream. */
protected float _gain = 1f;
-
+
/** The interval and elapsed time for fading. */
protected float _fadeInterval, _fadeElapsed;
-
+
/** The type of fading being performed. */
protected FadeMode _fadeMode = FadeMode.NONE;
-
+
/** The buffer used to store names. */
protected IntBuffer _nbuf;
-
+
/** The buffer used to store audio data temporarily. */
protected ByteBuffer _abuf;
-
+
/** The OpenAL state of the stream. */
protected int _state = AL10.AL_INITIAL;
-
+
/** The size of the buffers in bytes. */
protected static final int BUFFER_SIZE = 131072;
-
+
/** The number of buffers to use. */
protected static final int NUM_BUFFERS = 4;
-
+
/** Fading modes. */
protected enum FadeMode { NONE, IN, OUT, OUT_DISPOSE };
}