/*
* $ProjectName$
* $ProjectRevision$
* -----------------------------------------------------------
* $Id: OggPage.java,v 1.3 2003/04/10 19:48:22 jarnbjo Exp $
* -----------------------------------------------------------
*
* $Author: jarnbjo $
*
* Description:
*
* Copyright 2002-2003 Tor-Einar Jarnbjo
* -----------------------------------------------------------
*
* Change History
* -----------------------------------------------------------
* $Log: OggPage.java,v $
* Revision 1.3 2003/04/10 19:48:22 jarnbjo
* no message
*
* Revision 1.2 2003/03/31 00:23:04 jarnbjo
* no message
*
* Revision 1.1 2003/03/03 21:02:20 jarnbjo
* no message
*
*/
package de.jarnbjo.ogg;
import java.io.*;
import de.jarnbjo.util.io.*;
/**
* <p>An instance of this class represents an ogg page read from an ogg file
* or network stream. It has no public constructor, but instances can be
* created by the <code>create</code> methods, supplying a JMF stream or
* a <code>RandomAccessFile</code>
* which is positioned at the beginning of an Ogg page.</p>
*
* <p>Furtheron, the class provides methods for accessing the raw page data,
* as well as data attributes like segmenting information, sequence number,
* stream serial number, chechsum and wether this page is the beginning or
* end of a logical bitstream (BOS, EOS) and if the page data starts with a
* continued packet or a fresh data packet.</p>
*/
public class OggPage {
private int version;
private boolean continued, bos, eos;
private long absoluteGranulePosition;
private int streamSerialNumber, pageSequenceNumber, pageCheckSum;
private int[] segmentOffsets;
private int[] segmentLengths;
private int totalLength;
private byte[] header, segmentTable, data;
protected OggPage() {
}
private OggPage(
int version,
boolean continued,
boolean bos,
boolean eos,
long absoluteGranulePosition,
int streamSerialNumber,
int pageSequenceNumber,
int pageCheckSum,
int[] segmentOffsets,
int[] segmentLengths,
int totalLength,
byte[] header,
byte[] segmentTable,
byte[] data) {
this.version=version;
this.continued=continued;
this.bos=bos;
this.eos=eos;
this.absoluteGranulePosition=absoluteGranulePosition;
this.streamSerialNumber=streamSerialNumber;
this.pageSequenceNumber=pageSequenceNumber;
this.pageCheckSum=pageCheckSum;
this.segmentOffsets=segmentOffsets;
this.segmentLengths=segmentLengths;
this.totalLength=totalLength;
this.header=header;
this.segmentTable=segmentTable;
this.data=data;
}
/**
* this method equals to create(RandomAccessFile source, false)
*
* @see #create(RandomAccessFile, boolean)
*/
public static OggPage create(RandomAccessFile source) throws IOException, EndOfOggStreamException, OggFormatException {
return create(source, false);
}
/**
* This method is called to read data from the current position in the
* specified RandomAccessFile and create a new OggPage instance based on the data
* read. If the parameter <code>skipData</code> is set to <code>true</code>,
* the actual page segments (page data) is skipped and not read into
* memory. This mode is useful when scanning through an ogg file to build
* a seek table.
*
* @param source the source from which the ogg page is generated
* @param skipData if set to <code>true</code>, the actual page data is not read into memory
* @return an ogg page created by reading data from the specified source, starting at the current position
* @throws FormatException if the data read from the specified source is not matching the specification for an ogg page
* @throws EndOfStreamException if it is not possible to read an entire ogg page from the specified source
* @throws IOException if some other I/O error is detected when reading from the source
*
* @see #create(RandomAccessFile)
*/
public static OggPage create(RandomAccessFile source, boolean skipData) throws IOException, EndOfOggStreamException, OggFormatException {
return create((Object)source, skipData);
}
/**
* this method equals to create(InputStream source, false)
*
* @see #create(InputStream, boolean)
*/
public static OggPage create(InputStream source) throws IOException, EndOfOggStreamException, OggFormatException {
return create(source, false);
}
/**
* This method is called to read data from the current position in the
* specified InpuStream and create a new OggPage instance based on the data
* read. If the parameter <code>skipData</code> is set to <code>true</code>,
* the actual page segments (page data) is skipped and not read into
* memory. This mode is useful when scanning through an ogg file to build
* a seek table.
*
* @param source the source from which the ogg page is generated
* @param skipData if set to <code>true</code>, the actual page data is not read into memory
* @return an ogg page created by reading data from the specified source, starting at the current position
* @throws FormatException if the data read from the specified source is not matching the specification for an ogg page
* @throws EndOfStreamException if it is not possible to read an entire ogg page from the specified source
* @throws IOException if some other I/O error is detected when reading from the source
*
* @see #create(InputStream)
*/
public static OggPage create(InputStream source, boolean skipData) throws IOException, EndOfOggStreamException, OggFormatException {
return create((Object)source, skipData);
}
/**
* this method equals to create(byte[] source, false)
*
* @see #create(byte[], boolean)
*/
public static OggPage create(byte[] source) throws IOException, EndOfOggStreamException, OggFormatException {
return create(source, false);
}
/**
* This method is called to
* create a new OggPage instance based on the specified byte array.
*
* @param source the source from which the ogg page is generated
* @param skipData if set to <code>true</code>, the actual page data is not read into memory
* @return an ogg page created by reading data from the specified source, starting at the current position
* @throws FormatException if the data read from the specified source is not matching the specification for an ogg page
* @throws EndOfStreamException if it is not possible to read an entire ogg page from the specified source
* @throws IOException if some other I/O error is detected when reading from the source
*
* @see #create(byte[])
*/
public static OggPage create(byte[] source, boolean skipData) throws IOException, EndOfOggStreamException, OggFormatException {
return create((Object)source, skipData);
}
private static OggPage create(Object source, boolean skipData) throws IOException, EndOfOggStreamException, OggFormatException {
try {
int sourceOffset=27;
byte[] header=new byte[27];
if(source instanceof RandomAccessFile) {
RandomAccessFile raf=(RandomAccessFile)source;
if(raf.getFilePointer()==raf.length()) {
return null;
}
raf.readFully(header);
}
else if(source instanceof InputStream) {
readFully((InputStream)source, header);
}
else if(source instanceof byte[]) {
System.arraycopy((byte[])source, 0, header, 0, 27);
}
BitInputStream bdSource=new ByteArrayBitInputStream(header);
int capture=bdSource.getInt(32);
if(capture!=0x5367674f) {
//throw new FormatException("Ogg page does not start with 'OggS' (0x4f676753)");
/*
** This condition is IMHO an error, but older Ogg files often contain
** pages with a different capture than OggS. I am not sure how to
** manage these pages, but the decoder seems to work properly, if
** the incorrect capture is simply ignored.
*/
String cs=Integer.toHexString(capture);
while(cs.length()<8) {
cs="0"+cs;
}
cs=cs.substring(6, 8)+cs.substring(4, 6)+cs.substring(2, 4)+cs.substring(0, 2);
char c1=(char)(Integer.valueOf(cs.substring(0, 2), 16).intValue());
char c2=(char)(Integer.valueOf(cs.substring(2, 4), 16).intValue());
char c3=(char)(Integer.valueOf(cs.substring(4, 6), 16).intValue());
char c4=(char)(Integer.valueOf(cs.substring(6, 8), 16).intValue());
System.out.println("Ogg packet header is 0x"+cs+" ("+c1+c2+c3+c4+"), should be 0x4f676753 (OggS)");
}
int version=bdSource.getInt(8);
byte tmp=(byte)bdSource.getInt(8);
boolean bf1=(tmp&1)!=0;
boolean bos=(tmp&2)!=0;
boolean eos=(tmp&4)!=0;
long absoluteGranulePosition=bdSource.getLong(64);
int streamSerialNumber=bdSource.getInt(32);
int pageSequenceNumber=bdSource.getInt(32);
int pageCheckSum=bdSource.getInt(32);
int pageSegments=bdSource.getInt(8);
//System.out.println("OggPage: "+streamSerialNumber+" / "+absoluteGranulePosition+" / "+pageSequenceNumber);
int[] segmentOffsets=new int[pageSegments];
int[] segmentLengths=new int[pageSegments];
int totalLength=0;
byte[] segmentTable=new byte[pageSegments];
byte[] tmpBuf=new byte[1];
for(int i=0; i<pageSegments; i++) {
int l=0;
if(source instanceof RandomAccessFile) {
l=((int)((RandomAccessFile)source).readByte()&0xff);
}
else if(source instanceof InputStream) {
l=(int)((InputStream)source).read();
}
else if(source instanceof byte[]) {
l=(int)((byte[])source)[sourceOffset++];
l&=255;
}
segmentTable[i]=(byte)l;
segmentLengths[i]=l;
segmentOffsets[i]=totalLength;
totalLength+=l;
}
byte[] data=null;
if(!skipData) {
//System.out.println("createPage: "+absoluteGranulePosition*1000/44100);
data=new byte[totalLength];
//source.read(data, 0, totalLength);
if(source instanceof RandomAccessFile) {
((RandomAccessFile)source).readFully(data);
}
else if(source instanceof InputStream) {
readFully((InputStream)source, data);
}
else if(source instanceof byte[]) {
System.arraycopy(source, sourceOffset, data, 0, totalLength);
}
}
return new OggPage(version, bf1, bos, eos, absoluteGranulePosition, streamSerialNumber, pageSequenceNumber, pageCheckSum, segmentOffsets, segmentLengths, totalLength, header, segmentTable, data);
}
catch(EOFException e) {
throw new EndOfOggStreamException();
}
}
private static void readFully(InputStream source, byte[] buffer) throws IOException {
int total=0;
while(total<buffer.length) {
int read=source.read(buffer, total, buffer.length-total);
if(read==-1) {
throw new EndOfOggStreamException();
}
total+=read;
}
}
/**
* Returns the absolute granule position of the last complete
* packet contained in this Ogg page, or -1 if the page contains a single
* packet, which is not completed on this page. For pages containing Vorbis
* data, this value is the sample index within the Vorbis stream. The Vorbis
* stream does not necessarily start with sample index 0.
*
* @return the absolute granule position of the last packet completed on
* this page
*/
public long getAbsoluteGranulePosition() {
return absoluteGranulePosition;
}
/**
* Returns the stream serial number of this ogg page.
*
* @return this page's serial number
*/
public int getStreamSerialNumber() {
return streamSerialNumber;
}
/**
* Return the sequnce number of this ogg page.
*
* @return this page's sequence number
*/
public int getPageSequenceNumber() {
return pageSequenceNumber;
}
/**
* Return the check sum of this ogg page.
*
* @return this page's check sum
*/
public int getPageCheckSum() {
return pageCheckSum;
}
/**
* @return the total number of bytes in the page data
*/
public int getTotalLength() {
if(data!=null) {
return 27+segmentTable.length+data.length;
}
else {
return totalLength;
}
}
/**
* @return a ByteBuffer containing the page data
*/
public byte[] getData() {
return data;
}
public byte[] getHeader() {
return header;
}
public byte[] getSegmentTable() {
return segmentTable;
}
public int[] getSegmentOffsets() {
return segmentOffsets;
}
public int[] getSegmentLengths() {
return segmentLengths;
}
/**
* @return <code>true</code> if this page begins with a continued packet
*/
public boolean isContinued() {
return continued;
}
/**
* @return <code>true</code> if this page begins with a fresh packet
*/
public boolean isFresh() {
return !continued;
}
/**
* @return <code>true</code> if this page is the beginning of a logical stream
*/
public boolean isBos() {
return bos;
}
/**
* @return <code>true</code> if this page is the end of a logical stream
*/
public boolean isEos() {
return eos;
}
}