/****************************************************************************** * * * Copyright (c) 1999-2003 Wimba S.A., All Rights Reserved. * * * * COPYRIGHT: * * This software is the property of Wimba S.A. * * This software is redistributed under the Xiph.org variant of * * the BSD license. * * 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 Wimba, the Xiph.org Foundation nor the names of * * its contributors may be used to endorse or promote products derived * * from this software without specific prior written permission. * * * * WARRANTIES: * * This software is made available by the authors in the hope * * that it will be useful, but without any warranty. * * Wimba S.A. is not liable for any consequence related to the * * use of the provided software. * * * * Class: Speex2PcmAudioInputStream.java * * * * Author: Marc GIMPEL * * * * Date: 12th July 2003 * * * ******************************************************************************/ /* $Id: Speex2PcmAudioInputStream.java,v 1.5 2005/05/27 15:57:55 mgimpel Exp $ */ package org.xiph.speex.spi; import java.io.InputStream; import java.io.IOException; import java.io.StreamCorruptedException; import javax.sound.sampled.AudioFormat; import org.xiph.speex.Bits; import org.xiph.speex.Decoder; import org.xiph.speex.NbDecoder; import org.xiph.speex.SbDecoder; /** * Converts an Ogg Speex bitstream into a PCM 16bits/sample audio stream. * * @author Marc Gimpel, Wimba S.A. (mgimpel@horizonwimba.com) * @version $Revision: 1.5 $ */ public class Speex2PcmAudioInputStream extends FilteredAudioInputStream { // InputStream variables /** Flag to indicate if this Stream has been initialised. */ private boolean initialised; // audio parameters /** The sample rate of the audio, in samples per seconds (Hz). */ private int sampleRate; /** The number of audio channels (1=mono, 2=stereo). */ private int channelCount; // Speex variables /** Array containing the decoded audio samples. */ private float[] decodedData; /** Array containing the decoded audio samples converted into bytes. */ private byte[] outputData; /** Speex bit packing and unpacking class. */ private Bits bits; /** Speex Decoder. */ private Decoder decoder; /** The frame size, in samples. */ private int frameSize; /** The number of Speex frames that will be put in each Ogg packet. */ private int framesPerPacket; // Ogg variables /** A unique serial number that identifies the Ogg stream. */ private int streamSerialNumber; /** The number of Ogg packets that are in each Ogg page. */ private int packetsPerOggPage; /** The number of Ogg packets that have been decoded in the current page. */ private int packetCount; /** Array containing the sizes of Ogg packets in the current page.*/ private byte[] packetSizes; /** * Constructor * @param in the underlying input stream. * @param format the target format of this stream's audio data. * @param length the length in sample frames of the data in this stream. */ public Speex2PcmAudioInputStream(final InputStream in, final AudioFormat format, final long length) { this(in, format, length, DEFAULT_BUFFER_SIZE); } /** * Constructor * @param in the underlying input stream. * @param format the target format of this stream's audio data. * @param length the length in sample frames of the data in this stream. * @param size the buffer size. * @exception IllegalArgumentException if size <= 0. */ public Speex2PcmAudioInputStream(final InputStream in, final AudioFormat format, final long length, final int size) { super(in, format, length, size); bits = new Bits(); packetSizes = new byte[256]; initialised = false; } /** * Initialises the Ogg Speex to PCM InputStream. * Read the Ogg Speex header and extract the speex decoder parameters to * initialise the decoder. Then read the Comment header. * Ogg Header description: * <pre> * 0 - 3: capture_pattern * 4: stream_structure_version * 5: header_type_flag (2=bos: beginning of sream) * 6 - 13: absolute granule position * 14 - 17: stream serial number * 18 - 21: page sequence no * 22 - 25: page checksum * 26: page_segments * 27 -...: segment_table * </pre> * Speex Header description * <pre> * 0 - 7: speex_string * 8 - 27: speex_version * 28 - 31: speex_version_id * 32 - 35: header_size * 36 - 39: rate * 40 - 43: mode (0=narrowband, 1=wb, 2=uwb) * 44 - 47: mode_bitstream_version * 48 - 51: nb_channels * 52 - 55: bitrate * 56 - 59: frame_size * 60 - 63: vbr * 64 - 67: frames_per_packet * 68 - 71: extra_headers * 72 - 75: reserved1 * 76 - 79: reserved2 * </pre> * @param blocking whether the method should block until initialisation is * successfully completed or not. * @exception IOException */ protected void initialise(final boolean blocking) throws IOException { while (!initialised) { int readsize = prebuf.length - precount - 1; int avail = in.available(); if (!blocking && avail <= 0) { return; } readsize = (avail > 0 ? Math.min(avail, readsize) : readsize); int n = in.read(prebuf, precount, readsize); if (n < 0) { throw new StreamCorruptedException("Incomplete Ogg Headers"); } if (n == 0) { // This should never happen. //assert false : "Read 0 bytes from stream - possible infinate loop"; } precount += n; if (decoder==null && precount>=108) { // we can process the speex header if (!(new String(prebuf, 0, 4).equals("OggS"))) { throw new StreamCorruptedException("The given stream does not appear to be Ogg."); } streamSerialNumber = readInt(prebuf, 14); if (!(new String(prebuf, 28, 8).equals("Speex "))) { throw new StreamCorruptedException("The given stream does not appear to be Ogg Speex."); } sampleRate = readInt(prebuf, 28+36); channelCount = readInt(prebuf, 28+48); framesPerPacket = readInt(prebuf, 28+64); int mode = readInt(prebuf, 28+40); switch (mode) { case 0: decoder = new NbDecoder(); ((NbDecoder)decoder).nbinit(); break; case 1: decoder = new SbDecoder(); ((SbDecoder)decoder).wbinit(); break; case 2: decoder = new SbDecoder(); ((SbDecoder)decoder).uwbinit(); break; default: } /* initialize the speex decoder */ decoder.setPerceptualEnhancement(true); /* set decoder format and properties */ frameSize = decoder.getFrameSize(); decodedData = new float[frameSize*channelCount]; outputData = new byte[2*frameSize*channelCount*framesPerPacket]; bits.init(); } if (decoder!=null && precount>=108+27) { // we can process the comment (skip them) packetsPerOggPage = 0xff & prebuf[108+26]; if (precount>=108+27+packetsPerOggPage) { int size = 0; for (int i=0; i<packetsPerOggPage; i++) { size += 0xff & prebuf[108+27+i]; } if (precount>=108+27+packetsPerOggPage+size) { // we have read the complete comment page prepos = 108+27+packetsPerOggPage+size; packetsPerOggPage = 0; packetCount = 255; initialised = true; } } } } } /** * Fills the buffer with more data, taking into account shuffling and other * tricks for dealing with marks. * Assumes that it is being called by a synchronized method. * This method also assumes that all data has already been read in, hence * pos > count. * @exception IOException */ protected void fill() throws IOException { makeSpace(); while (!initialised) { initialise(true); } while (true) { int read = in.read(prebuf, precount, prebuf.length - precount); if (read < 0) { // inputstream has ended while (prepos < precount) { // still data to decode if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } if (packetCount < packetsPerOggPage) { // Ogg Page might be empty (0 packets) int n = packetSizes[packetCount++]; if ((precount-prepos) < n) { // we don't have enough data for a complete speex frame throw new StreamCorruptedException("Incompleted last Speex packet"); } // do last stuff here decode(prebuf, prepos, n); prepos += n; while ((buf.length - count) < outputData.length) { // grow buffer int nsz = buf.length * 2; byte[] nbuf = new byte[nsz]; System.arraycopy(buf, 0, nbuf, 0, count); buf = nbuf; } System.arraycopy(outputData, 0, buf, count, outputData.length); count += outputData.length; } } return; } // if read=0 but the prebuffer contains data, it is decoded and returned. // if read=0 but the prebuffer is almost empty, it loops back to read. else if (read >= 0) { precount += read; // do stuff here if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } if (packetCount < packetsPerOggPage) { // read the next packet if ((precount-prepos) >= packetSizes[packetCount]) { // we have enough data, lets start decoding while (((precount-prepos) >= packetSizes[packetCount]) && (packetCount < packetsPerOggPage)) { // lets decode all we can int n = packetSizes[packetCount++]; decode(prebuf, prepos, n); prepos += n; while ((buf.length - count) < outputData.length) { // grow buffer int nsz = buf.length * 2; byte[] nbuf = new byte[nsz]; System.arraycopy(buf, 0, nbuf, 0, count); buf = nbuf; } System.arraycopy(outputData, 0, buf, count, outputData.length); count += outputData.length; if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } } System.arraycopy(prebuf, prepos, prebuf, 0, precount-prepos); precount -= prepos; prepos = 0; return; // we have decoded some data (all that we could), so we can leave now, otherwise we return to a potentially blocking read of the underlying inputstream. } } } } } /** * This is where the actual decoding takes place. * @param data the array of data to decode. * @param offset the offset from which to start reading the data. * @param len the length of data to read from the array. * @throws StreamCorruptedException If the input stream not valid Ogg Speex * data. */ protected void decode(final byte[] data, final int offset, final int len) throws StreamCorruptedException { int i; short val; int outputSize = 0; /* read packet bytes into bitstream */ bits.read_from(data, offset, len); for (int frame=0; frame<framesPerPacket; frame++) { /* decode the bitstream */ decoder.decode(bits, decodedData); if (channelCount == 2) decoder.decodeStereo(decodedData, frameSize); /* PCM saturation */ for (i=0; i<frameSize*channelCount; i++) { if (decodedData[i] > 32767.0f) decodedData[i] = 32767.0f; else if (decodedData[i] < -32768.0f) decodedData[i] = -32768.0f; } /* convert to short and save to buffer */ for (i=0; i<frameSize*channelCount; i++) { val = (decodedData[i]>0) ? (short)(decodedData[i]+.5) : (short)(decodedData[i]-.5); outputData[outputSize++] = (byte) (val & 0xff); outputData[outputSize++] = (byte) ((val >> 8) & 0xff ); } } } /** * See the general contract of the <code>skip</code> method of * <code>InputStream</code>. * * @param n the number of bytes to be skipped. * @return the actual number of bytes skipped. * @exception IOException if an I/O error occurs. */ public synchronized long skip(long n) throws IOException { while (!initialised) { initialise(true); } checkIfStillOpen(); // Sanity check if (n <= 0) { return 0; } // Skip buffered data if there is any if (pos < count) { return super.skip(n); } // Nothing in the buffers to skip else { int decodedPacketSize = 2*framesPerPacket*frameSize*channelCount; if (markpos < 0 && n >= decodedPacketSize) { // We aren't buffering and skipping more than a complete Speex packet: // Lets try to skip complete Speex packets without decoding if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } if (packetCount < packetsPerOggPage) { // read the next packet int skipped = 0; if ((precount-prepos) < packetSizes[packetCount]) { // we don't have enough data int avail = in.available(); if (avail > 0) { int size = Math.min(prebuf.length - precount, avail); int read = in.read(prebuf, precount, size); if (read < 0) { // inputstream has ended throw new IOException("End of stream but there are still supposed to be packets to decode"); } precount += read; } } while (((precount-prepos) >= packetSizes[packetCount]) && (packetCount < packetsPerOggPage) && (n >= decodedPacketSize)) { // lets skip all we can prepos += packetSizes[packetCount++]; skipped += decodedPacketSize; n -= decodedPacketSize; if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } } System.arraycopy(prebuf, prepos, prebuf, 0, precount-prepos); precount -= prepos; prepos = 0; return skipped; // we have skipped some data (all that we could), so we can leave now, otherwise we return to a potentially blocking read of the underlying inputstream. } } // We are buffering, or couldn't skip a complete Speex packet: // Read (decode) into buffers and skip (this is potentially blocking) return super.skip(n); } } /** * Returns the number of bytes that can be read from this inputstream without * blocking. * <p> * The <code>available</code> method of <code>FilteredAudioInputStream</code> * returns the sum of the the number of bytes remaining to be read in the * buffer (<code>count - pos</code>). * The result of calling the <code>available</code> method of the underlying * inputstream is not used, as this data will have to be filtered, and thus * may not be the same size after processing (although subclasses that do the * filtering should override this method and use the amount of data available * in the underlying inputstream). * * @return the number of bytes that can be read from this inputstream without * blocking. * @exception IOException if an I/O error occurs. * @see #in */ public synchronized int available() throws IOException { if (!initialised) { initialise(false); if (!initialised) { return 0; } } int avail = super.available(); if (packetCount >= packetsPerOggPage) { // read new Ogg Page header readOggPageHeader(); } // See how much we could decode from the underlying stream. if (packetCount < packetsPerOggPage) { int undecoded = precount - prepos + in.available(); int size = packetSizes[packetCount]; int tempCount = 0; while (size < undecoded && packetCount + tempCount < packetsPerOggPage) { undecoded -= size; avail += 2 * frameSize * framesPerPacket; tempCount++; size = packetSizes[packetCount + tempCount]; } } return avail; } //--------------------------------------------------------------------------- // Ogg Specific code //--------------------------------------------------------------------------- /** * Read the Ogg Page header and extract the speex packet sizes. * Note: the checksum is ignores. * Note: the method should no block on a read because it will not read more * then is available */ private void readOggPageHeader() throws IOException { int packets = 0; if (precount-prepos<27) { int avail = in.available(); if (avail > 0) { int size = Math.min(prebuf.length - precount, avail); int read = in.read(prebuf, precount, size); if (read < 0) { // inputstream has ended throw new IOException("End of stream but available was positive"); } precount += read; } } if (precount-prepos>=27) { // can read beginning of Page header if (!(new String(prebuf, prepos, 4).equals("OggS"))) { throw new StreamCorruptedException("Lost Ogg Sync"); } if (streamSerialNumber != readInt(prebuf, prepos+14)) { throw new StreamCorruptedException("Ogg Stream Serial Number mismatch"); } packets = 0xff & prebuf[prepos+26]; } if (precount-prepos<27+packets) { int avail = in.available(); if (avail > 0) { int size = Math.min(prebuf.length - precount, avail); int read = in.read(prebuf, precount, size); if (read < 0) { // inputstream has ended throw new IOException("End of stream but available was positive"); } precount += read; } } if (precount-prepos>=27+packets) { // can read entire Page header System.arraycopy(prebuf, prepos+27, packetSizes, 0, packets); packetCount = 0; prepos += 27+packets; packetsPerOggPage = packets; } } /** * Converts Little Endian (Windows) bytes to an int (Java uses Big Endian). * @param data the data to read. * @param offset the offset from which to start reading. * @return the integer value of the reassembled bytes. */ private static int readInt(final byte[] data, final int offset) { return (data[offset] & 0xff) | ((data[offset+1] & 0xff) << 8) | ((data[offset+2] & 0xff) << 16) | (data[offset+3] << 24); // no & 0xff at the end to keep the sign } }