/* * Copyright 2013 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.audio.formats; import com.jcraft.jogg.Packet; import com.jcraft.jogg.Page; import com.jcraft.jogg.StreamState; import com.jcraft.jogg.SyncState; import com.jcraft.jorbis.Block; import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Info; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Decompresses an Ogg file. * <br><br> * How to use:<br> * 1. Create OggInputStream passing in the input stream of the packed ogg file<br> * 2. Fetch format and sampling rate using getFormat() and getRate(). Use it to * initialize the sound player.<br> * 3. Read the PCM data using one of the read functions, and feed it to your player. * <br><br> * OggInputStream provides a read(ByteBuffer, int, int) that can be used to read * data directly into a native buffer. */ public class OggReader extends FilterInputStream { private static final Logger logger = LoggerFactory.getLogger(OggReader.class); /** * The mono 16 bit format */ private static final int FORMAT_MONO16 = 1; /** * The stereo 16 bit format */ private static final int FORMAT_STEREO16 = 2; /// Conversion buffer size private static int convsize = 4096 * 2; // Conversion buffer private static byte[] convbuffer = new byte[convsize]; // temp vars private float[][][] pcm = new float[1][][]; private int[] index; // end of stream private boolean eos; // sync and verify incoming physical bitstream private SyncState syncState = new SyncState(); // take physical pages, weld into a logical stream of packets private StreamState streamState = new StreamState(); // one Ogg bitstream page. Vorbis packets are inside private Page page = new Page(); // one raw packet of data for decode private Packet packet = new Packet(); // struct that stores all the static vorbis bitstream settings private Info info = new Info(); // struct that stores all the bitstream user comments private Comment comment = new Comment(); // central working state for the packet->PCM decoder private DspState dspState = new DspState(); // local working space for packet->PCM decode private Block block = new Block(dspState); // where we are in the convbuffer private int convbufferOff; // bytes ready in convbuffer. private int convbufferSize; // a dummy used by read() to read 1 byte. private byte[] readDummy = new byte[1]; /** * Creates an OggInputStream that decompressed the specified ogg file. */ public OggReader(InputStream input) { super(input); try { initVorbis(); index = new int[info.channels]; } catch (Exception e) { logger.error("Failed to read ogg file", e); eos = true; } } /** * Gets the format of the ogg file. Is either FORMAT_MONO16 or FORMAT_STEREO16 */ public int getChannels() { return info.channels; } /** * Gets the rate of the pcm audio. */ public int getRate() { return info.rate; } /** * Reads the next byte of data from this input stream. The value byte is * returned as an int in the range 0 to 255. If no byte is available because * the end of the stream has been reached, the value -1 is returned. This * method blocks until input data is available, the end of the stream is * detected, or an exception is thrown. * * @return the next byte of data, or -1 if the end of the stream is reached. */ @Override public int read() throws IOException { int retVal = read(readDummy, 0, 1); return (retVal == -1 ? -1 : readDummy[0]); } /** * Reads up to len bytes of data from the input stream into an array of bytes. * * @param b the buffer into which the data is read. * @param off the start offset of the data. * @param len the maximum number of bytes read. * @return the total number of bytes read into the buffer, or -1 if there is * no more data because the end of the stream has been reached. */ @Override public int read(byte[] b, int off, int len) throws IOException { if (eos) { return -1; } int bytesRead = 0; int bytesRemaining = len; int offset = off; while (!eos && (bytesRemaining > 0)) { fillConvbuffer(); if (!eos) { int bytesToCopy = Math.min(bytesRemaining, convbufferSize - convbufferOff); System.arraycopy(convbuffer, convbufferOff, b, offset, bytesToCopy); convbufferOff += bytesToCopy; bytesRead += bytesToCopy; bytesRemaining -= bytesToCopy; offset += bytesToCopy; } } return bytesRead; } /** * Reads up to len bytes of data from the input stream into a ByteBuffer. * * @param b the buffer into which the data is read. * @param off the start offset of the data. * @param len the maximum number of bytes read. * @return the total number of bytes read into the buffer, or -1 if there is * no more data because the end of the stream has been reached. */ public int read(ByteBuffer b, int off, int len) throws IOException { if (eos) { return -1; } b.position(off); int bytesRead = 0; int bytesRemaining = len; while (!eos && (bytesRemaining > 0)) { fillConvbuffer(); if (!eos) { int bytesToCopy = Math.min(bytesRemaining, convbufferSize - convbufferOff); b.put(convbuffer, convbufferOff, bytesToCopy); convbufferOff += bytesToCopy; bytesRead += bytesToCopy; bytesRemaining -= bytesToCopy; } } return bytesRead; } /** * Helper function. Decodes a packet to the convbuffer if it is empty. * Updates convbufferSize, convbufferOff, and eos. */ private void fillConvbuffer() throws IOException { if (convbufferOff >= convbufferSize) { convbufferSize = lazyDecodePacket(); convbufferOff = 0; if (convbufferSize == -1) { eos = true; } } } /** * Returns 0 after EOF is reached, otherwise always return 1. * <br><br> * Programs should not count on this method to return the actual number of * bytes that could be read without blocking. * * @return 1 before EOF and 0 after EOF is reached. */ @Override public int available() throws IOException { return (eos ? 0 : 1); } /** * OggInputStream does not support mark and reset. This function does nothing. */ @Override public synchronized void reset() throws IOException { } /** * OggInputStream does not support mark and reset. * * @return false. */ @Override public boolean markSupported() { return false; } /** * Skips over and discards n bytes of data from the input stream. The skip * method may, for a variety of reasons, end up skipping over some smaller * number of bytes, possibly 0. The actual number of bytes skipped is returned. * * @param n the number of bytes to be skipped. * @return the actual number of bytes skipped. */ @Override public long skip(long n) throws IOException { int bytesRead = 0; while (bytesRead < n) { int res = read(); if (res == -1) { break; } bytesRead++; } return bytesRead; } /** * Initalizes the vorbis stream. Reads the stream until info and comment are read. */ private void initVorbis() throws Exception { // Now we can read pages syncState.init(); // grab some data at the head of the stream. We want the first page // (which is guaranteed to be small and only contain the Vorbis // stream initial header) We need the first page to get the stream // serialno. // submit a 4k block to libvorbis' Ogg layer int bufferIndex = syncState.buffer(4096); byte[] buffer = syncState.data; int bytes = in.read(buffer, bufferIndex, 4096); syncState.wrote(bytes); // Get the first page. if (syncState.pageout(page) != 1) { // have we simply run out of data? If so, we're done. if (bytes < 4096) { return; //break; } // error case. Must not be Vorbis data throw new Exception("Input does not appear to be an Ogg bitstream."); } // Get the serial number and set up the rest of decode. // serialno first; use it to set up a logical stream streamState.init(page.serialno()); // extract the initial header from the first page and verify that the // Ogg bitstream is in fact Vorbis data // I handle the initial header first instead of just having the code // read all three Vorbis headers at once because reading the initial // header is an easy way to identify a Vorbis bitstream and it's // useful to see that functionality seperated out. info.init(); comment.init(); if (streamState.pagein(page) < 0) { // error; stream version mismatch perhaps throw new Exception("Error reading first page of Ogg bitstream data."); } if (streamState.packetout(packet) != 1) { // no page? must not be vorbis throw new Exception("Error reading initial header packet."); } if (info.synthesis_headerin(comment, packet) < 0) { // error case; not a vorbis header throw new Exception("This Ogg bitstream does not contain Vorbis audio data."); } // At this point, we're sure we're Vorbis. We've set up the logical // (Ogg) bitstream decoder. Get the comment and codebook headers and // set up the Vorbis decoder // The next two packets in order are the comment and codebook headers. // They're likely large and may span multiple pages. Thus we read // and submit data until we get our two packets, watching that no // pages are missing. If a page is missing, error out; losing a // header page is the only place where missing data is fatal. int i = 0; while (i < 2) { while (i < 2) { int result = syncState.pageout(page); if (result == 0) { break; // Need more data } // Don't complain about missing or corrupt data yet. We'll // catch it at the packet output phase if (result == 1) { streamState.pagein(page); // we can ignore any errors here // as they'll also become apparent // at packet out while (i < 2) { result = streamState.packetout(packet); if (result == 0) { break; } if (result == -1) { // Uh oh; data at some point was corrupted or missing! // We can't tolerate that in a header. Die. throw new Exception("Corrupt secondary header. Exiting."); } info.synthesis_headerin(comment, packet); i++; } } } // no harm in not checking before adding more bufferIndex = syncState.buffer(4096); buffer = syncState.data; bytes = in.read(buffer, bufferIndex, 4096); // NOTE: This is a bugfix. read will return -1 which will mess up syncState. if (bytes < 0) { bytes = 0; } if (bytes == 0 && i < 2) { throw new Exception("End of file before finding all Vorbis headers!"); } syncState.wrote(bytes); } convsize = 4096 / info.channels; // OK, got and parsed all three headers. Initialize the Vorbis // packet->PCM decoder. dspState.synthesis_init(info); // central decode state block.init(dspState); // local state for most of the decode // so multiple block decodes can // proceed in parallel. We could init // multiple vorbis_block structures // for vd here } /** * Decodes a packet. */ private int decodePacket() { // check the endianes of the computer. final boolean bigEndian = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; if (block.synthesis(packet) == 0) { dspState.synthesis_blockin(block); } // **pcm is a multichannel float vector. In stereo, for // example, pcm[0] is left, and pcm[1] is right. samples is // the size of each channel. Convert the float values // (-1.<=range<=1.) to whatever PCM format and write it out int convOff = 0; int samples; while ((samples = dspState.synthesis_pcmout(pcm, index)) > 0) { float[][] localPcm = this.pcm[0]; int bout = (samples < convsize ? samples : convsize); // convert floats to 16 bit signed ints (host order) and interleave for (int i = 0; i < info.channels; i++) { int ptr = (i << 1) + convOff; int mono = index[i]; for (int j = 0; j < bout; j++) { int val = (int) (localPcm[i][mono + j] * 32767); // might as well guard against clipping val = Math.max(-32768, Math.min(32767, val)); val |= (val < 0 ? 0x8000 : 0); convbuffer[ptr + 0] = (byte) (bigEndian ? val >>> 8 : val); convbuffer[ptr + 1] = (byte) (bigEndian ? val : val >>> 8); ptr += (info.channels) << 1; } } convOff += 2 * info.channels * bout; // Tell orbis how many samples were consumed dspState.synthesis_read(bout); } return convOff; } /** * Decodes the next packet. * * @return bytes read into convbuffer of -1 if end of file */ private int lazyDecodePacket() throws IOException { int result = getNextPacket(); if (result == -1) { return -1; } // we have a packet. Decode it return decodePacket(); } /** * @return * @throws IOException */ private int getNextPacket() throws IOException { // get next packet. boolean fetchedPacket = false; while (!eos && !fetchedPacket) { int result1 = streamState.packetout(packet); if (result1 == 0) { // no more packets in page. Fetch new page. int result2 = 0; while (!eos && result2 == 0) { result2 = syncState.pageout(page); if (result2 == 0) { fetchData(); } } // return if we have reaced end of file. if ((result2 == 0) && (page.eos() != 0)) { return -1; } if (result2 == 0) { // need more data fetching page.. fetchData(); } else if (result2 == -1) { logger.warn("syncState.pageout(page) result == -1"); return -1; } else { streamState.pagein(page); } } else if (result1 == -1) { logger.warn("streamState.packetout(packet) result == -1"); return -1; } else { fetchedPacket = true; } } return 0; } /** * Copys data from input stream to syncState. */ private void fetchData() throws IOException { if (!eos) { // copy 4096 bytes from compressed stream to syncState. int bufferIndex = syncState.buffer(4096); if (bufferIndex < 0) { eos = true; return; } int bytes = in.read(syncState.data, bufferIndex, 4096); syncState.wrote(bytes); if (bytes == 0) { eos = true; } } } /** * Gets information on the ogg. */ @Override public String toString() { String s = ""; s = s + "version " + info.version + "\n"; s = s + "channels " + info.channels + "\n"; s = s + "rate (hz) " + info.rate; return s; } }