/*
* 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;
}
}