From fc5459fb7d76e6c00af47d9305cb285d0ea6de8a Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Fri, 9 Feb 2007 02:10:32 +0000 Subject: [PATCH] Fixed mp3 playing, refactored to avoid duplicating code. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@153 ed5b42cb-e716-0410-a449-f6a68f950b19 --- .../{OggFileStream.java => FileStream.java} | 108 +++++++---- .../com/threerings/openal/Mp3FileStream.java | 174 ------------------ .../threerings/openal/Mp3StreamDecoder.java | 111 +++++++++++ .../threerings/openal/OggStreamDecoder.java | 68 +++++++ src/java/com/threerings/openal/Stream.java | 3 +- .../com/threerings/openal/StreamDecoder.java | 58 ++++++ .../threerings/openal/TestSoundManager.java | 35 +++- 7 files changed, 341 insertions(+), 216 deletions(-) rename src/java/com/threerings/openal/{OggFileStream.java => FileStream.java} (50%) delete mode 100644 src/java/com/threerings/openal/Mp3FileStream.java create mode 100644 src/java/com/threerings/openal/Mp3StreamDecoder.java create mode 100644 src/java/com/threerings/openal/OggStreamDecoder.java create mode 100644 src/java/com/threerings/openal/StreamDecoder.java diff --git a/src/java/com/threerings/openal/OggFileStream.java b/src/java/com/threerings/openal/FileStream.java similarity index 50% rename from src/java/com/threerings/openal/OggFileStream.java rename to src/java/com/threerings/openal/FileStream.java index 562f4980..9ef73813 100644 --- a/src/java/com/threerings/openal/OggFileStream.java +++ b/src/java/com/threerings/openal/FileStream.java @@ -1,5 +1,5 @@ // -// $Id$ +// $Id: OggFileStream.java 119 2007-01-24 00:22:12Z dhoover $ // // Narya library - tools for developing networked games // Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved @@ -21,6 +21,8 @@ package com.threerings.openal; +import java.util.HashMap; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -32,27 +34,33 @@ import java.util.ArrayList; import org.lwjgl.openal.AL10; -import com.jmex.sound.openAL.objects.util.OggInputStream; - /** - * An audio stream read from one or more Ogg Vorbis files. + * An audio stream read from one or more files. */ -public class OggFileStream extends Stream +public class FileStream extends Stream { /** - * Creates a new Ogg stream for the specified file. + * Registers a class of {@link StreamDecoder} for the specified file extension. + */ + public static void registerExtension (String extension, Class clazz) + { + _extensions.put(extension, clazz); + } + + /** + * 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 OggFileStream (SoundManager soundmgr, File file, boolean loop) + public FileStream (SoundManager soundmgr, File file, boolean loop) throws IOException { super(soundmgr); - _file = new OggFile(file, loop); - _istream = new OggInputStream(new FileInputStream(file)); + _file = new QueuedFile(file, loop); + _decoder = createDecoder(file); } - + /** * Adds a file to the queue of files to play. * @@ -61,61 +69,97 @@ public class OggFileStream extends Stream */ public void queueFile (File file, boolean loop) { - _queue.add(new OggFile(file, loop)); + _queue.add(new QueuedFile(file, loop)); } - + // documentation inherited protected int getFormat () { - return (_istream.getFormat() == OggInputStream.FORMAT_MONO16) ? - AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_STEREO16; + return _decoder.getFormat(); } - + // documentation inherited protected int getFrequency () { - return _istream.getRate(); + return _decoder.getFrequency(); } - + // documentation inherited protected int populateBuffer (ByteBuffer buf) throws IOException { - int read = _istream.read(buf, buf.position(), buf.remaining()); + int read = _decoder.read(buf); while (buf.hasRemaining() && (!_queue.isEmpty() || _file.loop)) { if (!_queue.isEmpty()) { _file = _queue.remove(0); } - _istream = new OggInputStream(new FileInputStream(_file.file)); + _decoder = createDecoder(_file.file); read = Math.max(0, read); - read += _istream.read(buf, buf.position(), buf.remaining()); + read += _decoder.read(buf); } return read; } - + + /** + * Creates and initializes a stream decoder for the specified file. + */ + protected StreamDecoder createDecoder (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 clazz = _extensions.get(extension); + if (clazz == null) { + Log.warning("No decoder registered for extension [extension=" + extension + + ", file=" + path + "]."); + return null; + } + StreamDecoder decoder; + try { + decoder = (StreamDecoder)clazz.newInstance(); + } catch (Exception e) { + Log.warning("Error instantiating decoder [file=" + path + ", error=" + e + "]."); + return null; + } + decoder.init(new FileInputStream(file)); + return decoder; + } + /** The file currently being played. */ - protected OggFile _file; - - /** The underlying Ogg input stream for the current file. */ - protected OggInputStream _istream; - + 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 _queue = new ArrayList(); - + protected ArrayList _queue = new ArrayList(); + /** A file queued for play. */ - protected class OggFile + 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 OggFile (File file, boolean loop) + + public QueuedFile (File file, boolean loop) { this.file = file; this.loop = loop; } } + + /** Registered decoder classes. */ + protected static HashMap _extensions = new HashMap(); + static { + registerExtension("ogg", OggStreamDecoder.class); + registerExtension("mp3", Mp3StreamDecoder.class); + } } diff --git a/src/java/com/threerings/openal/Mp3FileStream.java b/src/java/com/threerings/openal/Mp3FileStream.java deleted file mode 100644 index f0eed8cc..00000000 --- a/src/java/com/threerings/openal/Mp3FileStream.java +++ /dev/null @@ -1,174 +0,0 @@ -// -// $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/Mp3StreamDecoder.java b/src/java/com/threerings/openal/Mp3StreamDecoder.java new file mode 100644 index 00000000..225229c5 --- /dev/null +++ b/src/java/com/threerings/openal/Mp3StreamDecoder.java @@ -0,0 +1,111 @@ +// +// $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.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; + +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; + +/** + * Decodes MP3 streams. + */ +public class Mp3StreamDecoder + implements StreamDecoder +{ + // documentation inherited + 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(); + } + + // documentation inherited + public int getFormat () + { + return AL10.AL_FORMAT_STEREO16; + } + + // documentation inherited + public int getFrequency () + { + return _header.frequency(); + } + + // documentation inherited + 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; +} diff --git a/src/java/com/threerings/openal/OggStreamDecoder.java b/src/java/com/threerings/openal/OggStreamDecoder.java new file mode 100644 index 00000000..5cafa6d9 --- /dev/null +++ b/src/java/com/threerings/openal/OggStreamDecoder.java @@ -0,0 +1,68 @@ +// +// $Id: OggFileStream.java 119 2007-01-24 00:22:12Z dhoover $ +// +// 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.io.InputStream; + +import java.nio.ByteBuffer; + +import org.lwjgl.openal.AL10; + +import com.jmex.sound.openAL.objects.util.OggInputStream; + +/** + * Decodes Ogg Vorbis streams. + */ +public class OggStreamDecoder + implements StreamDecoder +{ + // documentation inherited + public void init (InputStream in) + throws IOException + { + _istream = new OggInputStream(in); + } + + // documentation inherited + public int getFormat () + { + return (_istream.getFormat() == OggInputStream.FORMAT_MONO16) ? + AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_STEREO16; + } + + // documentation inherited + public int getFrequency () + { + return _istream.getRate(); + } + + // documentation inherited + public int read (ByteBuffer buf) + throws IOException + { + return _istream.read(buf, buf.position(), buf.remaining()); + } + + /** The underlying Ogg input stream. */ + protected OggInputStream _istream; +} diff --git a/src/java/com/threerings/openal/Stream.java b/src/java/com/threerings/openal/Stream.java index 5ab811c3..d98082b8 100644 --- a/src/java/com/threerings/openal/Stream.java +++ b/src/java/com/threerings/openal/Stream.java @@ -24,6 +24,7 @@ package com.threerings.openal; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.IntBuffer; import org.lwjgl.BufferUtils; @@ -263,7 +264,7 @@ public abstract class Stream protected boolean populateBuffer (int bufferId) { if (_abuf == null) { - _abuf = ByteBuffer.allocateDirect(BUFFER_SIZE); + _abuf = ByteBuffer.allocateDirect(BUFFER_SIZE).order(ByteOrder.nativeOrder()); } _abuf.clear(); int read = 0; diff --git a/src/java/com/threerings/openal/StreamDecoder.java b/src/java/com/threerings/openal/StreamDecoder.java new file mode 100644 index 00000000..858918ce --- /dev/null +++ b/src/java/com/threerings/openal/StreamDecoder.java @@ -0,0 +1,58 @@ +// +// $Id: OggFileStream.java 119 2007-01-24 00:22:12Z dhoover $ +// +// 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.io.InputStream; + +import java.nio.ByteBuffer; + +/** + * Decodes audio streams from data read from an {@link InputStream}. + */ +public interface StreamDecoder +{ + /** + * 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; +} diff --git a/tests/src/java/com/threerings/openal/TestSoundManager.java b/tests/src/java/com/threerings/openal/TestSoundManager.java index caf3fdc6..9fcde9d5 100644 --- a/tests/src/java/com/threerings/openal/TestSoundManager.java +++ b/tests/src/java/com/threerings/openal/TestSoundManager.java @@ -3,6 +3,7 @@ package com.threerings.openal; +import java.io.File; import java.io.IOException; import com.samskivert.util.Interval; @@ -17,7 +18,7 @@ public class TestSoundManager public static void main (String[] args) { if (args.length == 0) { - System.err.println("Usage: TestSoundManager sound.wav"); + System.err.println("Usage: TestSoundManager sound.[wav|mp3|ogg]"); System.exit(-1); } @@ -31,19 +32,35 @@ public class TestSoundManager } }; - SoundManager smgr = SoundManager.createSoundManager(rqueue); + final SoundManager smgr = SoundManager.createSoundManager(rqueue); ClipProvider provider = new WaveDataClipProvider(); final SoundGroup group = smgr.createGroup(provider, 5); final String path = args[0]; - // queue up an interval to play a sound over and over - Interval i = new Interval(rqueue) { - public void expired () { - Sound sound = group.getSound(path); - sound.play(true); + String lpath = path.toLowerCase(); + if (lpath.endsWith("mp3") || lpath.endsWith(".ogg")) { + // play the mp3/ogg file in a loop + try { + new FileStream(smgr, new File(args[0]), true).play(); + } catch (IOException e) { + e.printStackTrace(); } - }; - i.schedule(100L, true); + Interval i = new Interval(rqueue) { + public void expired () { + smgr.updateStreams(0.1f); + } + }; + i.schedule(100L, true); + } else { + // queue up an interval to play a sound over and over + Interval i = new Interval(rqueue) { + public void expired () { + Sound sound = group.getSound(path); + sound.play(true); + } + }; + i.schedule(100L, true); + } while (true) { Runnable r = (Runnable)_queue.get();