diff --git a/core/pom.xml b/core/pom.xml index cffc232e..1d8b2551 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,15 +32,9 @@ true - org.lwjgl.lwjgl - lwjgl - 2.9.3 - true - - - org.lwjgl.lwjgl - lwjgl_util - 2.9.3 + org.lwjgl + lwjgl-openal + 3.3.6 true diff --git a/core/src/main/java/com/threerings/openal/Clip.java b/core/src/main/java/com/threerings/openal/Clip.java index 95a2007a..86fc7540 100644 --- a/core/src/main/java/com/threerings/openal/Clip.java +++ b/core/src/main/java/com/threerings/openal/Clip.java @@ -22,7 +22,7 @@ package com.threerings.openal; import java.nio.ByteBuffer; import org.lwjgl.openal.AL10; -import org.lwjgl.util.WaveData; +import com.threerings.openal.util.WaveData; /** * Contains data for a single sampled sound. diff --git a/core/src/main/java/com/threerings/openal/Listener.java b/core/src/main/java/com/threerings/openal/Listener.java index f49c24a6..0ce6c605 100644 --- a/core/src/main/java/com/threerings/openal/Listener.java +++ b/core/src/main/java/com/threerings/openal/Listener.java @@ -19,9 +19,6 @@ package com.threerings.openal; -import java.nio.FloatBuffer; - -import org.lwjgl.BufferUtils; import org.lwjgl.openal.AL10; /** @@ -89,9 +86,13 @@ public class Listener 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); + _vbuf[0] = (_ax = ax); + _vbuf[1] = (_ay = ay); + _vbuf[2] = (_az = az); + _vbuf[3] = (_ux = ux); + _vbuf[4] = (_uy = uy); + _vbuf[5] = (_uz = uz); + AL10.alListenerfv(AL10.AL_ORIENTATION, _vbuf); } } @@ -115,5 +116,5 @@ public class Listener protected float _ax, _ay, _az = -1f, _ux, _uy = 1f, _uz; /** A buffer for floating point values. */ - protected FloatBuffer _vbuf = BufferUtils.createFloatBuffer(6); + protected float[] _vbuf = new float[6]; } diff --git a/core/src/main/java/com/threerings/openal/OpenALSoundPlayer.java b/core/src/main/java/com/threerings/openal/OpenALSoundPlayer.java index fa0a64b3..aaa4523a 100644 --- a/core/src/main/java/com/threerings/openal/OpenALSoundPlayer.java +++ b/core/src/main/java/com/threerings/openal/OpenALSoundPlayer.java @@ -34,7 +34,7 @@ import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import org.lwjgl.openal.AL10; -import org.lwjgl.util.WaveData; +import com.threerings.openal.util.WaveData; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; diff --git a/core/src/main/java/com/threerings/openal/SoundManager.java b/core/src/main/java/com/threerings/openal/SoundManager.java index e66a20a8..6f78e394 100644 --- a/core/src/main/java/com/threerings/openal/SoundManager.java +++ b/core/src/main/java/com/threerings/openal/SoundManager.java @@ -23,11 +23,15 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.nio.ByteBuffer; import java.nio.IntBuffer; import org.lwjgl.BufferUtils; -import org.lwjgl.openal.AL; import org.lwjgl.openal.AL10; +import org.lwjgl.openal.AL; +import org.lwjgl.openal.ALC10; +import org.lwjgl.openal.ALC; +import org.lwjgl.openal.ALCCapabilities; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -77,8 +81,13 @@ public class SoundManager */ public void shutdown () { - if (isInitialized() && !_sharingAL) { - AL.destroy(); + if (_alcContext != 0L) { + ALC10.alcDestroyContext(_alcContext); + _alcContext = 0L; + } + if (_alcDevice != 0L) { + ALC10.alcCloseDevice(_alcDevice); + _alcDevice = 0L; } } @@ -201,23 +210,31 @@ public class SoundManager _rqueue = rqueue; // initialize the OpenAL sound system - if (AL.isCreated()) { - _sharingAL = true; - } else { - try { - AL.create("", 44100, 15, false); - } catch (Exception e) { - log.warning("Failed to initialize sound system.", e); + try { + _alcDevice = ALC10.alcOpenDevice((ByteBuffer)null); + if (_alcDevice == 0) { + log.warning("Failed to create AL device."); // don't start the background loading thread return; } + ALCCapabilities deviceCaps = ALC.createCapabilities(_alcDevice); - 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; - } + _alcContext = ALC10.alcCreateContext(_alcDevice, (IntBuffer)null); + ALC10.alcMakeContextCurrent(_alcContext); + AL.createCapabilities(deviceCaps); + + // 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 @@ -415,8 +432,8 @@ public class SoundManager } }; - /** Whether or not we're sharing an AL context that someone else created. */ - protected boolean _sharingAL; + protected long _alcDevice; + protected long _alcContext; /** Used to get back from the background thread to our "main" thread. */ protected RunQueue _rqueue; diff --git a/core/src/main/java/com/threerings/openal/WaveDataClipProvider.java b/core/src/main/java/com/threerings/openal/WaveDataClipProvider.java index de18233d..c91b5ea5 100644 --- a/core/src/main/java/com/threerings/openal/WaveDataClipProvider.java +++ b/core/src/main/java/com/threerings/openal/WaveDataClipProvider.java @@ -21,7 +21,7 @@ package com.threerings.openal; import java.io.IOException; -import org.lwjgl.util.WaveData; +import com.threerings.openal.util.WaveData; /** * Loads clips via the class loader using LWJGL's {@link WaveData} utility diff --git a/core/src/main/java/com/threerings/openal/util/SunFileReader.java b/core/src/main/java/com/threerings/openal/util/SunFileReader.java new file mode 100644 index 00000000..3a32a769 --- /dev/null +++ b/core/src/main/java/com/threerings/openal/util/SunFileReader.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.threerings.openal.util; + +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.DataInputStream; +import java.net.URL; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.UnsupportedAudioFileException; +import javax.sound.sampled.spi.AudioFileReader; + + + +/** + * Abstract File Reader class. + * + * @author Jan Borgersen + */ +abstract class SunFileReader extends AudioFileReader { + + // buffer size for temporary input streams + protected static final int bisBufferSize = 4096; + + /** + * Constructs a new SunFileReader object. + */ + SunFileReader() { + } + + + // METHODS TO IMPLEMENT AudioFileReader + + /** + * Obtains the audio file format of the input stream provided. The stream must + * point to valid audio file data. In general, audio file providers may + * need to read some data from the stream before determining whether they + * support it. These parsers must + * be able to mark the stream, read enough data to determine whether they + * support the stream, and, if not, reset the stream's read pointer to its original + * position. If the input stream does not support this, this method may fail + * with an IOException. + * @param stream the input stream from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the stream does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + * @see InputStream#markSupported + * @see InputStream#mark + */ + abstract public AudioFileFormat getAudioFileFormat(InputStream stream) throws UnsupportedAudioFileException, IOException; + + + /** + * Obtains the audio file format of the URL provided. The URL must + * point to valid audio file data. + * @param url the URL from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the URL does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + abstract public AudioFileFormat getAudioFileFormat(URL url) throws UnsupportedAudioFileException, IOException; + + + /** + * Obtains the audio file format of the File provided. The File must + * point to valid audio file data. + * @param file the File from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the File does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + abstract public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException; + + + /** + * Obtains an audio stream from the input stream provided. The stream must + * point to valid audio file data. In general, audio file providers may + * need to read some data from the stream before determining whether they + * support it. These parsers must + * be able to mark the stream, read enough data to determine whether they + * support the stream, and, if not, reset the stream's read pointer to its original + * position. If the input stream does not support this, this method may fail + * with an IOException. + * @param stream the input stream from which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data contained + * in the input stream. + * @throws UnsupportedAudioFileException if the stream does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + * @see InputStream#markSupported + * @see InputStream#mark + */ + abstract public AudioInputStream getAudioInputStream(InputStream stream) throws UnsupportedAudioFileException, IOException; + + + /** + * Obtains an audio stream from the URL provided. The URL must + * point to valid audio file data. + * @param url the URL for which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data pointed + * to by the URL + * @throws UnsupportedAudioFileException if the URL does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + abstract public AudioInputStream getAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException; + + + /** + * Obtains an audio stream from the File provided. The File must + * point to valid audio file data. + * @param file the File for which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data pointed + * to by the File + * @throws UnsupportedAudioFileException if the File does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + abstract public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException; + + + // HELPER METHODS + + + + /** + * rllong + * Protected helper method to read 64 bits and changing the order of + * each bytes. + * @param DataInputStream + * @return 32 bits swapped value. + * @exception IOException + */ + final int rllong(DataInputStream dis) throws IOException { + + int b1, b2, b3, b4 ; + int i = 0; + + i = dis.readInt(); + + b1 = ( i & 0xFF ) << 24 ; + b2 = ( i & 0xFF00 ) << 8; + b3 = ( i & 0xFF0000 ) >> 8; + b4 = ( i & 0xFF000000 ) >>> 24; + + i = ( b1 | b2 | b3 | b4 ); + + return i; + } + + /** + * big2little + * Protected helper method to swap the order of bytes in a 32 bit int + * @param int + * @return 32 bits swapped value + */ + final int big2little(int i) { + + int b1, b2, b3, b4 ; + + b1 = ( i & 0xFF ) << 24 ; + b2 = ( i & 0xFF00 ) << 8; + b3 = ( i & 0xFF0000 ) >> 8; + b4 = ( i & 0xFF000000 ) >>> 24; + + i = ( b1 | b2 | b3 | b4 ); + + return i; + } + + /** + * rlshort + * Protected helper method to read 16 bits value. Swap high with low byte. + * @param DataInputStream + * @return the swapped value. + * @exception IOException + */ + final short rlshort(DataInputStream dis) throws IOException { + + short s=0; + short high, low; + + s = dis.readShort(); + + high = (short)(( s & 0xFF ) << 8) ; + low = (short)(( s & 0xFF00 ) >>> 8); + + s = (short)( high | low ); + + return s; + } + + /** + * big2little + * Protected helper method to swap the order of bytes in a 16 bit short + * @param int + * @return 16 bits swapped value + */ + final short big2littleShort(short i) { + + short high, low; + + high = (short)(( i & 0xFF ) << 8) ; + low = (short)(( i & 0xFF00 ) >>> 8); + + i = (short)( high | low ); + + return i; + } + + /** Calculates the frame size for PCM frames. + * Note that this method is appropriate for non-packed samples. + * For instance, 12 bit, 2 channels will return 4 bytes, not 3. + * @param sampleSizeInBits the size of a single sample in bits + * @param channels the number of channels + * @return the size of a PCM frame in bytes. + */ + static final int calculatePCMFrameSize(int sampleSizeInBits, int channels) { + return ((sampleSizeInBits + 7) / 8) * channels; + } +} diff --git a/core/src/main/java/com/threerings/openal/util/WaveData.java b/core/src/main/java/com/threerings/openal/util/WaveData.java new file mode 100644 index 00000000..38f43468 --- /dev/null +++ b/core/src/main/java/com/threerings/openal/util/WaveData.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2002-2008 LWJGL Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'LWJGL' nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.threerings.openal.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; + +import org.lwjgl.openal.AL10; + +import static com.threerings.openal.Log.log; + +/** + * + * Utitlity class for loading wavefiles. + * + * @author Brian Matzon + * @version $Revision$ + * $Id$ + */ +public class WaveData { + /** actual wave data */ + public final ByteBuffer data; + + /** format type of data */ + public final int format; + + /** sample rate of data */ + public final int samplerate; + + /** + * Creates a new WaveData + * + * @param data actual wavedata + * @param format format of wave data + * @param samplerate sample rate of data + */ + private WaveData(ByteBuffer data, int format, int samplerate) { + this.data = data; + this.format = format; + this.samplerate = samplerate; + } + + /** + * Disposes the wavedata + */ + public void dispose() { + data.clear(); + } + + /** + * Creates a WaveData container from the specified url + * + * @param path URL to file + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(URL path) { + try { + // due to an issue with AudioSystem.getAudioInputStream + // and mixing unsigned and signed code + // we will use the reader directly + WaveFileReader wfr = new WaveFileReader(); + return create(wfr.getAudioInputStream(new BufferedInputStream(path.openStream()))); + } catch (Exception e) { + log.warning("Unable to create from: " + path + ", " + e.getMessage()); + return null; + } + } + + /** + * Creates a WaveData container from the specified in the classpath + * + * @param path path to file (relative, and in classpath) + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(String path) { + return create(Thread.currentThread().getContextClassLoader().getResource(path)); + } + + /** + * Creates a WaveData container from the specified inputstream + * + * @param is InputStream to read from + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(InputStream is) { + try { + return create( + AudioSystem.getAudioInputStream(is)); + } catch (Exception e) { + log.warning("Unable to create from inputstream, " + e.getMessage()); + return null; + } + } + + /** + * Creates a WaveData container from the specified bytes + * + * @param buffer array of bytes containing the complete wave file + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(byte[] buffer) { + try { + return create( + AudioSystem.getAudioInputStream( + new BufferedInputStream(new ByteArrayInputStream(buffer)))); + } catch (Exception e) { + log.warning("Unable to create from byte array, " + e.getMessage()); + return null; + } + } + + /** + * Creates a WaveData container from the specified ByetBuffer. + * If the buffer is backed by an array, it will be used directly, + * else the contents of the buffer will be copied using get(byte[]). + * + * @param buffer ByteBuffer containing sound file + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(ByteBuffer buffer) { + try { + byte[] bytes = null; + + if(buffer.hasArray()) { + bytes = buffer.array(); + } else { + bytes = new byte[buffer.capacity()]; + buffer.get(bytes); + } + return create(bytes); + } catch (Exception e) { + log.warning("Unable to create from ByteBuffer, " + e.getMessage()); + return null; + } + } + + /** + * Creates a WaveData container from the specified stream + * + * @param ais AudioInputStream to read from + * @return WaveData containing data, or null if a failure occured + */ + public static WaveData create(AudioInputStream ais) { + //get format of data + AudioFormat audioformat = ais.getFormat(); + + // get channels + int channels = 0; + if (audioformat.getChannels() == 1) { + if (audioformat.getSampleSizeInBits() == 8) { + channels = AL10.AL_FORMAT_MONO8; + } else if (audioformat.getSampleSizeInBits() == 16) { + channels = AL10.AL_FORMAT_MONO16; + } else { + assert false : "Illegal sample size"; + } + } else if (audioformat.getChannels() == 2) { + if (audioformat.getSampleSizeInBits() == 8) { + channels = AL10.AL_FORMAT_STEREO8; + } else if (audioformat.getSampleSizeInBits() == 16) { + channels = AL10.AL_FORMAT_STEREO16; + } else { + assert false : "Illegal sample size"; + } + } else { + assert false : "Only mono or stereo is supported"; + } + + //read data into buffer + ByteBuffer buffer = null; + try { + int available = ais.available(); + if(available <= 0) { + available = ais.getFormat().getChannels() * (int) ais.getFrameLength() * ais.getFormat().getSampleSizeInBits() / 8; + } + byte[] buf = new byte[ais.available()]; + int read = 0, total = 0; + while ((read = ais.read(buf, total, buf.length - total)) != -1 + && total < buf.length) { + total += read; + } + buffer = convertAudioBytes(buf, audioformat.getSampleSizeInBits() == 16, audioformat.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + } catch (IOException ioe) { + return null; + } + + + //create our result + WaveData wavedata = + new WaveData(buffer, channels, (int) audioformat.getSampleRate()); + + //close stream + try { + ais.close(); + } catch (IOException ioe) { + } + + return wavedata; + } + + private static ByteBuffer convertAudioBytes(byte[] audio_bytes, boolean two_bytes_data, ByteOrder order) { + ByteBuffer dest = ByteBuffer.allocateDirect(audio_bytes.length); + dest.order(ByteOrder.nativeOrder()); + ByteBuffer src = ByteBuffer.wrap(audio_bytes); + src.order(order); + if (two_bytes_data) { + ShortBuffer dest_short = dest.asShortBuffer(); + ShortBuffer src_short = src.asShortBuffer(); + while (src_short.hasRemaining()) + dest_short.put(src_short.get()); + } else { + while (src.hasRemaining()) + dest.put(src.get()); + } + dest.rewind(); + return dest; + } +} diff --git a/core/src/main/java/com/threerings/openal/util/WaveFileFormat.java b/core/src/main/java/com/threerings/openal/util/WaveFileFormat.java new file mode 100644 index 00000000..f3ee7ba8 --- /dev/null +++ b/core/src/main/java/com/threerings/openal/util/WaveFileFormat.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.threerings.openal.util; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; + + +/** + * WAVE file format class. + * + * @author Jan Borgersen + */ + +final class WaveFileFormat extends AudioFileFormat { + + /** + * Wave format type. + */ + private final int waveType; + + //$$fb 2001-07-13: added management of header size in this class + //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant + private static final int STANDARD_HEADER_SIZE = 28; + + //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant + /** + * fmt_ chunk size in bytes + */ + private static final int STANDARD_FMT_CHUNK_SIZE = 16; + + // magic numbers + static final int RIFF_MAGIC = 1380533830; + static final int WAVE_MAGIC = 1463899717; + static final int FMT_MAGIC = 0x666d7420; // "fmt " + static final int DATA_MAGIC = 0x64617461; // "data" + + // encodings + static final int WAVE_FORMAT_UNKNOWN = 0x0000; + static final int WAVE_FORMAT_PCM = 0x0001; + static final int WAVE_FORMAT_ADPCM = 0x0002; + static final int WAVE_FORMAT_ALAW = 0x0006; + static final int WAVE_FORMAT_MULAW = 0x0007; + static final int WAVE_FORMAT_OKI_ADPCM = 0x0010; + static final int WAVE_FORMAT_DIGISTD = 0x0015; + static final int WAVE_FORMAT_DIGIFIX = 0x0016; + static final int WAVE_IBM_FORMAT_MULAW = 0x0101; + static final int WAVE_IBM_FORMAT_ALAW = 0x0102; + static final int WAVE_IBM_FORMAT_ADPCM = 0x0103; + static final int WAVE_FORMAT_DVI_ADPCM = 0x0011; + static final int WAVE_FORMAT_SX7383 = 0x1C07; + + + WaveFileFormat( AudioFileFormat aff ) { + + this( aff.getType(), aff.getByteLength(), aff.getFormat(), aff.getFrameLength() ); + } + + WaveFileFormat(AudioFileFormat.Type type, int lengthInBytes, AudioFormat format, int lengthInFrames) { + + super(type,lengthInBytes,format,lengthInFrames); + + AudioFormat.Encoding encoding = format.getEncoding(); + + if( encoding.equals(AudioFormat.Encoding.ALAW) ) { + waveType = WAVE_FORMAT_ALAW; + } else if( encoding.equals(AudioFormat.Encoding.ULAW) ) { + waveType = WAVE_FORMAT_MULAW; + } else if( encoding.equals(AudioFormat.Encoding.PCM_SIGNED) || + encoding.equals(AudioFormat.Encoding.PCM_UNSIGNED) ) { + waveType = WAVE_FORMAT_PCM; + } else { + waveType = WAVE_FORMAT_UNKNOWN; + } + } + + int getWaveType() { + + return waveType; + } + + int getHeaderSize() { + return getHeaderSize(getWaveType()); + } + + static int getHeaderSize(int waveType) { + //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant + // use dynamic format chunk size + return STANDARD_HEADER_SIZE + getFmtChunkSize(waveType); + } + + static int getFmtChunkSize(int waveType) { + //$$fb 2002-04-16: Fix for 4636355: RIFF audio headers could be _more_ spec compliant + // add 2 bytes for "codec specific data length" for non-PCM codecs + int result = STANDARD_FMT_CHUNK_SIZE; + if (waveType != WAVE_FORMAT_PCM) { + result += 2; // WORD for "codec specific data length" + } + return result; + } +} diff --git a/core/src/main/java/com/threerings/openal/util/WaveFileReader.java b/core/src/main/java/com/threerings/openal/util/WaveFileReader.java new file mode 100644 index 00000000..b79deca5 --- /dev/null +++ b/core/src/main/java/com/threerings/openal/util/WaveFileReader.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.threerings.openal.util; + +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + + + +/** + * WAVE file reader. + * + * @author Kara Kytle + * @author Jan Borgersen + * @author Florian Bomers + */ +public final class WaveFileReader extends SunFileReader { + + private static final int MAX_READ_LENGTH = 12; + + /** + * Obtains the audio file format of the input stream provided. The stream must + * point to valid audio file data. In general, audio file providers may + * need to read some data from the stream before determining whether they + * support it. These parsers must + * be able to mark the stream, read enough data to determine whether they + * support the stream, and, if not, reset the stream's read pointer to its original + * position. If the input stream does not support this, this method may fail + * with an IOException. + * @param stream the input stream from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the stream does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + * @see InputStream#markSupported + * @see InputStream#mark + */ + public AudioFileFormat getAudioFileFormat(InputStream stream) throws UnsupportedAudioFileException, IOException { + // fix for 4489272: AudioSystem.getAudioFileFormat() fails for InputStream, but works for URL + AudioFileFormat aff = getFMT(stream, true); + // the following is not strictly necessary - but was implemented like that in 1.3.0 - 1.4.1 + // so I leave it as it was. May remove this for 1.5.0 + stream.reset(); + return aff; + } + + + /** + * Obtains the audio file format of the URL provided. The URL must + * point to valid audio file data. + * @param url the URL from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the URL does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + public AudioFileFormat getAudioFileFormat(URL url) throws UnsupportedAudioFileException, IOException { + InputStream urlStream = url.openStream(); // throws IOException + AudioFileFormat fileFormat = null; + try { + fileFormat = getFMT(urlStream, false); + } finally { + urlStream.close(); + } + return fileFormat; + } + + + /** + * Obtains the audio file format of the File provided. The File must + * point to valid audio file data. + * @param file the File from which file format information should be + * extracted + * @return an AudioFileFormat object describing the audio file format + * @throws UnsupportedAudioFileException if the File does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException { + AudioFileFormat fileFormat = null; + FileInputStream fis = new FileInputStream(file); // throws IOException + // part of fix for 4325421 + try { + fileFormat = getFMT(fis, false); + } finally { + fis.close(); + } + + return fileFormat; + } + + + /** + * Obtains an audio stream from the input stream provided. The stream must + * point to valid audio file data. In general, audio file providers may + * need to read some data from the stream before determining whether they + * support it. These parsers must + * be able to mark the stream, read enough data to determine whether they + * support the stream, and, if not, reset the stream's read pointer to its original + * position. If the input stream does not support this, this method may fail + * with an IOException. + * @param stream the input stream from which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data contained + * in the input stream. + * @throws UnsupportedAudioFileException if the stream does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + * @see InputStream#markSupported + * @see InputStream#mark + */ + public AudioInputStream getAudioInputStream(InputStream stream) throws UnsupportedAudioFileException, IOException { + // getFMT leaves the input stream at the beginning of the audio data + AudioFileFormat fileFormat = getFMT(stream, true); // throws UnsupportedAudioFileException, IOException + + // we've got everything, and the stream is at the + // beginning of the audio data, so return an AudioInputStream. + return new AudioInputStream(stream, fileFormat.getFormat(), fileFormat.getFrameLength()); + } + + + /** + * Obtains an audio stream from the URL provided. The URL must + * point to valid audio file data. + * @param url the URL for which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data pointed + * to by the URL + * @throws UnsupportedAudioFileException if the URL does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + public AudioInputStream getAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException { + InputStream urlStream = url.openStream(); // throws IOException + AudioFileFormat fileFormat = null; + try { + fileFormat = getFMT(urlStream, false); + } finally { + if (fileFormat == null) { + urlStream.close(); + } + } + return new AudioInputStream(urlStream, fileFormat.getFormat(), fileFormat.getFrameLength()); + } + + + /** + * Obtains an audio stream from the File provided. The File must + * point to valid audio file data. + * @param file the File for which the AudioInputStream should be + * constructed + * @return an AudioInputStream object based on the audio file data pointed + * to by the File + * @throws UnsupportedAudioFileException if the File does not point to valid audio + * file data recognized by the system + * @throws IOException if an I/O exception occurs + */ + public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException { + FileInputStream fis = new FileInputStream(file); // throws IOException + AudioFileFormat fileFormat = null; + // part of fix for 4325421 + try { + fileFormat = getFMT(fis, false); + } finally { + if (fileFormat == null) { + fis.close(); + } + } + return new AudioInputStream(fis, fileFormat.getFormat(), fileFormat.getFrameLength()); + } + + + //-------------------------------------------------------------------- + + + private AudioFileFormat getFMT(InputStream stream, boolean doReset) throws UnsupportedAudioFileException, IOException { + + // assumes sream is rewound + + int bytesRead; + int nread = 0; + int fmt; + int length = 0; + int wav_type = 0; + short channels; + long sampleRate; + long avgBytesPerSec; + short blockAlign; + int sampleSizeInBits; + AudioFormat.Encoding encoding = null; + + DataInputStream dis = new DataInputStream( stream ); + + if (doReset) { + dis.mark(MAX_READ_LENGTH); + } + + int magic = dis.readInt(); + int fileLength = rllong(dis); + int waveMagic = dis.readInt(); + int totallength; + if (fileLength <= 0) { + fileLength = AudioSystem.NOT_SPECIFIED; + totallength = AudioSystem.NOT_SPECIFIED; + } else { + totallength = fileLength + 8; + } + + if ((magic != WaveFileFormat.RIFF_MAGIC) || (waveMagic != WaveFileFormat.WAVE_MAGIC)) { + // not WAVE, throw UnsupportedAudioFileException + if (doReset) { + dis.reset(); + } + throw new UnsupportedAudioFileException("not a WAVE file"); + } + + // find and read the "fmt" chunk + // we break out of this loop either by hitting EOF or finding "fmt " + while(true) { + + try { + fmt = dis.readInt(); + nread += 4; + if( fmt==WaveFileFormat.FMT_MAGIC ) { + // we've found the 'fmt' chunk + break; + } else { + // else not 'fmt', skip this chunk + length = rllong(dis); + nread += 4; + if (length % 2 > 0) length++; + nread += dis.skipBytes(length); + } + } catch (EOFException eof) { + // we've reached the end of the file without finding the 'fmt' chunk + throw new UnsupportedAudioFileException("Not a valid WAV file"); + } + } + + // Read the format chunk size. + length = rllong(dis); + nread += 4; + + // This is the nread position at the end of the format chunk + int endLength = nread + length; + + // Read the wave format data out of the format chunk. + + // encoding. + wav_type = rlshort(dis); nread += 2; + + if (wav_type == WaveFileFormat.WAVE_FORMAT_PCM) + encoding = AudioFormat.Encoding.PCM_SIGNED; // if 8-bit, we need PCM_UNSIGNED, below... + else if ( wav_type == WaveFileFormat.WAVE_FORMAT_ALAW ) + encoding = AudioFormat.Encoding.ALAW; + else if ( wav_type == WaveFileFormat.WAVE_FORMAT_MULAW ) + encoding = AudioFormat.Encoding.ULAW; + else { + // we don't support any other WAVE formats.... + throw new UnsupportedAudioFileException("Not a supported WAV file"); + } + // channels + channels = rlshort(dis); nread += 2; + if (channels <= 0) { + throw new UnsupportedAudioFileException("Invalid number of channels"); + } + + // sample rate. + sampleRate = rllong(dis); nread += 4; + + // this is the avgBytesPerSec + avgBytesPerSec = rllong(dis); nread += 4; + + // this is blockAlign value + blockAlign = rlshort(dis); nread += 2; + + // this is the PCM-specific value bitsPerSample + sampleSizeInBits = (int)rlshort(dis); nread += 2; + if (sampleSizeInBits <= 0) { + throw new UnsupportedAudioFileException("Invalid bitsPerSample"); + } + + // if sampleSizeInBits==8, we need to use PCM_UNSIGNED + if ((sampleSizeInBits==8) && encoding.equals(AudioFormat.Encoding.PCM_SIGNED)) + encoding = AudioFormat.Encoding.PCM_UNSIGNED; + + // skip any difference between the length of the format chunk + // and what we read + + // if the length of the chunk is odd, there's an extra pad byte + // at the end. i've never seen this in the fmt chunk, but we + // should check to make sure. + + if (length % 2 != 0) length += 1; + + // $$jb: 07.28.99: endLength>nread, not length>nread. + // This fixes #4257986 + if (endLength > nread) + nread += dis.skipBytes(endLength - nread); + + // we have a format now, so find the "data" chunk + // we break out of this loop either by hitting EOF or finding "data" + // $$kk: if "data" chunk precedes "fmt" chunk we are hosed -- can this legally happen? + nread = 0; + while(true) { + try{ + int datahdr = dis.readInt(); + nread+=4; + if (datahdr == WaveFileFormat.DATA_MAGIC) { + // we've found the 'data' chunk + break; + } else { + // else not 'data', skip this chunk + int thisLength = rllong(dis); nread += 4; + if (thisLength % 2 > 0) thisLength++; + nread += dis.skipBytes(thisLength); + } + } catch (EOFException eof) { + // we've reached the end of the file without finding the 'data' chunk + throw new UnsupportedAudioFileException("Not a valid WAV file"); + } + } + // this is the length of the data chunk + int dataLength = rllong(dis); nread += 4; + + // now build the new AudioFileFormat and return + + AudioFormat format = new AudioFormat(encoding, + (float)sampleRate, + sampleSizeInBits, channels, + calculatePCMFrameSize(sampleSizeInBits, channels), + (float)sampleRate, false); + + return new WaveFileFormat(AudioFileFormat.Type.WAVE, + totallength, + format, + dataLength / format.getFrameSize()); + } +}