/**
* Copyright 2012 Jason Sorensen (sorensenj@smert.net)
*
* 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 net.smert.frameworkgl.openal.codecs.ogg;
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 java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Jason Sorensen <sorensenj@smert.net>
*/
public class OggInputStream extends InputStream {
private final static int BUFFER_SIZE = 2 * 1024;
private final static int CONV_BUFFER_SIZE = 4 * 1024;
private final static int PCM_BUFFER_SIZE = 256 * 1024; // Hopefully large enought to decode a page
private final static Logger log = LoggerFactory.getLogger(OGGCodec.class);
private boolean bigEndian;
private boolean endOfStream;
private boolean initialized;
private byte[] convertedBuffer;
private float[][][] pcmInfo;
private int count;
private int index;
private int packet;
private int pcmBufferIndex;
private int[] pcmIndexes;
private final Block jorbisBlock;
private ByteBuffer pcmBuffer;
private final Comment jorbisComment;
private final DspState jorbisDspState;
private HeaderPacketParser headerPacketParser;
private final Info jorbisInfo;
private final InputStream in;
private final Packet joggPacket;
private final Page joggPage;
private PCMPacketParser pcmPacketParser;
private final StreamState joggStreamState;
private final SyncState joggSyncState;
public OggInputStream(InputStream in) {
jorbisComment = new Comment();
jorbisDspState = new DspState();
jorbisInfo = new Info();
jorbisBlock = new Block(jorbisDspState); // Noobs
this.in = in;
joggPacket = new Packet();
joggPage = new Page();
joggStreamState = new StreamState();
joggSyncState = new SyncState();
init();
}
private void init() {
bigEndian = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
endOfStream = false;
initialized = false;
headerPacketParser = new HeaderPacketParser(jorbisComment, jorbisInfo, joggPacket, joggPage, joggStreamState,
joggSyncState);
joggSyncState.init();
count = 0;
packet = 1;
}
private void initPCM() {
convertedBuffer = new byte[CONV_BUFFER_SIZE];
pcmBufferIndex = 0;
jorbisDspState.synthesis_init(jorbisInfo);
jorbisBlock.init(jorbisDspState); // Noobs
pcmBuffer = ByteBuffer.allocateDirect(PCM_BUFFER_SIZE).order(ByteOrder.nativeOrder());
pcmBuffer.limit(0);
pcmPacketParser = new PCMPacketParser(joggPacket, joggPage, joggStreamState, joggSyncState);
pcmInfo = new float[1][][];
pcmIndexes = new int[getChannels()];
}
private void readHeader() throws IOException {
boolean finished = false;
boolean needsData = true;
while (!finished) {
if (needsData) {
// This will clear out any data that we already processed
index = joggSyncState.buffer(BUFFER_SIZE);
// Read new data
count = in.read(joggSyncState.data, index, BUFFER_SIZE);
if (count == -1) {
throw new IOException("OGG stream ended prematurely");
}
// Must tell the sync state how many bytes were read
joggSyncState.wrote(count);
needsData = false;
}
// Read page and packet
if (!headerPacketParser.read(packet)) {
needsData = true;
continue;
}
// Increase packet count and reset parser state
packet++;
headerPacketParser.reset();
// If we read 3 packets then the header is complete
if (packet == 4) {
finished = true;
}
}
}
private void readPCM() throws IOException {
// Return if we are at the end of the stream
if (endOfStream) {
return;
}
boolean finished = false;
boolean needsData = false;
while (!finished) {
if (needsData) {
// This will clear out any data that we already processed
index = joggSyncState.buffer(BUFFER_SIZE);
// Read new data
count = in.read(joggSyncState.data, index, BUFFER_SIZE);
if (count == -1) {
throw new IOException("OGG stream ended prematurely");
}
// Must tell the sync state how many bytes were read
joggSyncState.wrote(count);
needsData = false;
}
// Read page and packet
if (!pcmPacketParser.read(packet)) {
needsData = true;
continue;
}
// Increase packet count
packet++;
// If there was an error get new data
if (pcmPacketParser.isError()) {
pcmPacketParser.reset(); // Reset parser state for new data
continue;
}
boolean hasPacket = true;
boolean savedData = false;
while (hasPacket) {
// Check packet for errors
if (jorbisBlock.synthesis(joggPacket) == 0) {
jorbisDspState.synthesis_blockin(jorbisBlock);
}
// Process samples
int samples;
while ((samples = jorbisDspState.synthesis_pcmout(pcmInfo, pcmIndexes)) > 0) {
int maxSamples = (samples < CONV_BUFFER_SIZE ? samples : CONV_BUFFER_SIZE);
for (int channel = 0; channel < getChannels(); channel++) {
float[][] pcm = pcmInfo[0]; // Channel, Index
int convertedIndex = channel * 2;
int pcmIndex = pcmIndexes[channel];
for (int i = 0; i < maxSamples; i++) {
int value = (int) (pcm[channel][pcmIndex + i] * 32767f);
// Clamp values
if (value > 32767) {
value = 32767;
} else if (value < -32768) {
value = -32768;
}
if (value < 0) {
value = value | 0x8000;
}
// Convert data
if (bigEndian) {
convertedBuffer[convertedIndex] = (byte) (value >>> 8);
convertedBuffer[convertedIndex + 1] = (byte) (value);
} else {
convertedBuffer[convertedIndex] = (byte) (value);
convertedBuffer[convertedIndex + 1] = (byte) (value >>> 8);
}
// Advance by stride
convertedIndex += 2 * getChannels();
}
}
// Tell jOrbis how many samples we processed
jorbisDspState.synthesis_read(maxSamples);
// Save converted data in PCM buffer
int bytesToWrite = 2 * getChannels() * maxSamples;
if (bytesToWrite > pcmBuffer.remaining()) {
throw new RuntimeException("PCM buffer too full: Bytes to write: " + bytesToWrite
+ " Remaining: " + pcmBuffer.remaining());
}
pcmBuffer.put(convertedBuffer, 0, bytesToWrite);
savedData = true;
}
// Read another packet
if (!pcmPacketParser.read(packet)) {
// Flip buffer once only if we saved data
if (savedData) {
pcmBuffer.flip();
}
pcmPacketParser.reset(); // Reset parser state for new data
hasPacket = false;
}
// Increase packet count
packet++;
}
// Have we reached the end of the stream?
if (joggPage.eos() != 0) {
endOfStream = true;
}
// If we are at the end of the stream or we saved data
// to the PCM buffer then we can break. Read will continue
// to pull bytes out of the PCM buffer until we hit the
// limit again.
if (endOfStream || savedData) {
break;
}
}
}
public int getChannels() {
return jorbisInfo.channels;
}
public int getSampleRate() {
return jorbisInfo.rate;
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
joggStreamState.clear();
joggSyncState.clear();
jorbisBlock.clear();
jorbisDspState.clear();
jorbisInfo.clear();
in.close();
}
@Override
public int read() throws IOException {
// Initialize once
if (!initialized) {
readHeader();
initPCM();
initialized = true;
}
// Have we read past the limit of the buffer?
if (pcmBufferIndex >= pcmBuffer.limit()) {
// End of stream when we try to read past the limit
// since there maybe data in PCM buffer
if (endOfStream) {
return -1;
}
pcmBuffer.clear();
pcmBufferIndex = 0;
readPCM();
}
// Get the value from the PCM buffer
int value = pcmBuffer.get(pcmBufferIndex++);
if (value < 0) {
value = 256 + value; // Must be in the range 0 to 255
}
return value;
}
private static class HeaderPacketParser {
private int state;
private final Comment jorbisComment;
private final Info jorbisInfo;
private final Packet joggPacket;
private final Page joggPage;
private final StreamState joggStreamState;
private final SyncState joggSyncState;
public HeaderPacketParser(Comment jorbisComment, Info jorbisInfo, Packet joggPacket, Page joggPage,
StreamState joggStreamState, SyncState joggSyncState) {
this.jorbisComment = jorbisComment;
this.jorbisInfo = jorbisInfo;
this.joggPacket = joggPacket;
this.joggPage = joggPage;
this.joggStreamState = joggStreamState;
this.joggSyncState = joggSyncState;
}
public boolean read(int packet) throws IOException {
switch (state) {
// Read page
case 0:
switch (joggSyncState.pageout(joggPage)) {
case 0:
return false;
case 1:
break;
default:
throw new IOException("There was a hole in the data for packet: " + packet);
}
if (packet == 1) {
joggStreamState.init(joggPage.serialno());
joggStreamState.reset();
jorbisComment.init();
jorbisInfo.init();
}
if (joggStreamState.pagein(joggPage) == -1) {
throw new IOException("Unable read the header page for packet: " + packet);
}
state = 1;
// Read packet
case 1:
switch (joggStreamState.packetout(joggPacket)) {
case 0:
return false;
case 1:
break;
default:
throw new IOException("There was a hole in the data for packet: " + packet);
}
if (jorbisInfo.synthesis_headerin(jorbisComment, joggPacket) < 0) {
throw new IOException("This is not an OGG stream. Unable to read header info for packet: "
+ packet);
}
state = 2;
}
if (state != 2) {
throw new IllegalStateException("Unknown state: " + state);
}
return true;
}
public final void reset() {
state = 0;
}
}
private static class PCMPacketParser {
private boolean error;
private int state;
private final Packet joggPacket;
private final Page joggPage;
private final StreamState joggStreamState;
private final SyncState joggSyncState;
public PCMPacketParser(Packet joggPacket, Page joggPage, StreamState joggStreamState, SyncState joggSyncState) {
this.joggPacket = joggPacket;
this.joggPage = joggPage;
this.joggStreamState = joggStreamState;
this.joggSyncState = joggSyncState;
}
public boolean isError() {
return error;
}
public boolean read(int packet) throws IOException {
switch (state) {
// Read page
case 0:
switch (joggSyncState.pageout(joggPage)) {
case 0:
return false;
case 1:
break;
default:
log.error("Error in OGG stream: Missing data in page for packet: {}", packet);
error = true;
}
joggStreamState.pagein(joggPage);
state = 1;
// Read packet
case 1:
switch (joggStreamState.packetout(joggPacket)) {
case 0:
return false;
case 1:
break;
default:
log.error("Error in OGG stream: Missing data in packet for packet: {}", packet);
}
}
// State in the state so all new calls to read will
// handle a packet.
if (state != 1) {
throw new IllegalStateException("Unknown state: " + state);
}
return true;
}
public final void reset() {
error = false;
state = 0;
}
}
}