OggVorbisExtractor (WIP - Seeking not yet enabled)

This commit is contained in:
Oliver Woodman 2016-01-22 11:45:04 +00:00
parent 88fa1495c4
commit 89ce1ccedf
13 changed files with 3366 additions and 0 deletions

View file

@ -242,6 +242,8 @@ import java.util.Locale;
+ "&key=ik0", Util.TYPE_OTHER),
new Sample("Google Play (MP3 Audio)",
"http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", Util.TYPE_OTHER),
new Sample("Google Play (Ogg/Vorbis Audio)",
"https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg", Util.TYPE_OTHER),
new Sample("Google Glass (WebM Video with Vorbis Audio)",
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", Util.TYPE_OTHER),
new Sample("Big Buck Bunny (FLV Video)",

View file

@ -0,0 +1,346 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggReader}
*/
public final class OggReaderTest extends TestCase {
private static final String TAG = "OggReaderTest";
private OggReader oggReader;
private RecordableOggExtractorInput extractorInput;
@Override
public void setUp() throws Exception {
super.setUp();
extractorInput = new RecordableOggExtractorInput(1024 * 64);
// we want the mocked ExtractorInput to throw errors often
extractorInput.doThrowExceptionsAtPeek(true);
extractorInput.doThrowExceptionsAtRead(true);
// create reader
oggReader = new OggReader();
oggReader.reset();
}
public void testReadPacketUntilEOFIncludingAnEmptyPage() throws Exception {
// record first page with a single packet
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{0x08});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(8));
// record intermediate page with two packets
extractorInput.recordOggHeader((byte) 0x00, 16, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF, 0x11});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(255 + 17));
// empty page
extractorInput.recordOggHeader((byte) 0x00, 16, (byte) 0x00);
// record last page with two packets (256 and 271 bytes)
extractorInput.recordOggHeader((byte) 0x04, 128, (byte) 0x04);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF, 0x01, (byte) 0xff, 0x10});
extractorInput.recordOggPacket(RecordableExtractorInput
.getBytesGrowingValues(255 + 1 + 255 + 16));
// read first packet
final ParsableByteArray packetArray = new ParsableByteArray(new byte[255 * 255], 0);
readPacketUntilSuccess(packetArray);
// verify
assertEquals(8, packetArray.limit());
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(0x02, oggReader.getPageHeader().type);
assertEquals(27 + 1, oggReader.getPageHeader().headerSize);
assertEquals(8, oggReader.getPageHeader().bodySize);
assertEquals(RecordableExtractorInput.STREAM_REVISION, oggReader.getPageHeader().revision);
assertEquals(1, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(4096, oggReader.getPageHeader().streamSerialNumber);
assertEquals(0, oggReader.getPageHeader().granulePosition);
for (int i = 0; i < 8; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
packetArray.reset();
// read second packet
readPacketUntilSuccess(packetArray);
// verify
assertEquals(255 + 17, packetArray.limit());
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(0, oggReader.getPageHeader().type);
assertEquals(27 + 2, oggReader.getPageHeader().headerSize);
assertEquals(255 + 17, oggReader.getPageHeader().bodySize);
assertEquals(2, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(16, oggReader.getPageHeader().granulePosition);
packetArray.reset();
// read next packet and skip empty page
readPacketUntilSuccess(packetArray);
// verify
assertEquals(255 + 1, packetArray.limit());
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(4, oggReader.getPageHeader().type);
assertEquals(27 + 4, oggReader.getPageHeader().headerSize);
assertEquals(255 + 1 + 255 + 16, oggReader.getPageHeader().bodySize);
assertEquals(4, oggReader.getPageHeader().pageSegmentCount);
// page 1002 is empty, so current is 1003
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(128, oggReader.getPageHeader().granulePosition);
packetArray.reset();
// read last packet
readPacketUntilSuccess(packetArray);
assertEquals(255 + 16, packetArray.limit());
// EOF!
readEOFUntilSuccess(packetArray, 10);
}
public void testReadPacketWithZeroSizeTerminator() throws Exception {
// record first page with a single packet
extractorInput.recordOggHeader((byte) 0x06, 0, (byte) 0x04);
extractorInput.recordOggLaces(new byte[]{(byte) 0xff, 0x00, 0x00, 0x08});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(255 + 8));
ParsableByteArray packetArray = new ParsableByteArray(new byte[255 * 255], 0);
readPacketUntilSuccess(packetArray);
assertEquals(255, packetArray.limit());
packetArray.reset();
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
readEOFUntilSuccess(packetArray, 10);
}
public void testReadContinuedPacket() throws Exception {
// record first page with a packet continuing on the second page
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF, (byte) 0xFF});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(510));
// record the continuing page
extractorInput.recordOggHeader((byte) 0x05, 10, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{0x08});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(8, (byte) 0x22));
// there is only one single packet across two pages
ParsableByteArray packetArray = new ParsableByteArray(new byte[255 * 255], 0);
readPacketUntilSuccess(packetArray);
assertEquals(255 + 255 + 8, packetArray.limit());
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
// we must be on the second page already
assertEquals(1001, oggReader.getPageHeader().pageSequenceNumber);
// verify packet data
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(255, packetArray.getPosition());
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(510, packetArray.getPosition());
for (int i = 0; i < 8; i++) {
assertEquals(i + 0x22, packetArray.readUnsignedByte());
}
assertEquals(0, packetArray.bytesLeft());
// EOF!
readEOFUntilSuccess(packetArray, 10);
}
// no one does this with vorbis buts it's supported
public void testReadContinuedPacketOverMoreThan2Pages() throws Exception {
// record first page with a packet continuing on the second page
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF, (byte) 0xFF});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(510));
// record the first continuing page
extractorInput.recordOggHeader((byte) 0x01, 10, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(255));
// record the second continuing page
extractorInput.recordOggHeader((byte) 0x01, 10, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{(byte) 0xFF});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(255));
// record the third continuing page
extractorInput.recordOggHeader((byte) 0x05, 10, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{(byte) 0x08});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(8, (byte) 0x22));
// there is only one single packet across two pages
ParsableByteArray packetArray = new ParsableByteArray(new byte[255 * 255], 0);
readPacketUntilSuccess(packetArray);
assertEquals(255 + 255 + 255 + 255 + 8, packetArray.limit());
assertTrue((oggReader.getPageHeader().type & 0x04) == 0x04);
assertFalse((oggReader.getPageHeader().type & 0x02) == 0x02);
// we must be on the fourth page already
assertEquals(1003, oggReader.getPageHeader().pageSequenceNumber);
// verify packet data
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(255, packetArray.getPosition());
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(510, packetArray.getPosition());
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(765, packetArray.getPosition());
for (int i = 0; i < 255; i++) {
assertEquals(i, packetArray.readUnsignedByte());
}
assertEquals(1020, packetArray.getPosition());
for (int i = 0; i < 8; i++) {
assertEquals(i + 0x22, packetArray.readUnsignedByte());
}
assertEquals(0, packetArray.bytesLeft());
// EOF!
readEOFUntilSuccess(packetArray, 10);
}
public void testReadExceptionThrownWhilePeekingHeader() throws Exception {
// record first page with two packets packet
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{(byte) 0x01, (byte) 0x08});
extractorInput.recordOggPacket(new byte[]{0x10});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(8));
// record next page
extractorInput.recordOggHeader((byte) 0x05, 10, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{0x08});
extractorInput.recordOggPacket(RecordableExtractorInput.getBytesGrowingValues(8, (byte) 0x22));
ParsableByteArray packetArray = new ParsableByteArray(new byte[255 * 255], 0);
readPacketUntilSuccess(packetArray);
// verify packet data
assertEquals(1, packetArray.limit());
assertEquals(0x10, packetArray.data[0]);
// verify header
assertTrue((oggReader.getPageHeader().type & 0x02) == 0x02);
assertFalse((oggReader.getPageHeader().type & 0x04) == 0x04);
assertEquals(27 + 2, oggReader.getPageHeader().headerSize);
assertEquals(9, oggReader.getPageHeader().bodySize);
assertEquals(2, oggReader.getPageHeader().pageSegmentCount);
assertEquals(1000, oggReader.getPageHeader().pageSequenceNumber);
assertEquals(0, oggReader.getPageHeader().granulePosition);
packetArray.reset();
readPacketUntilSuccess(packetArray);
}
public void testReadNoZeroSizedPacketsAreReturned() throws Exception {
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x04);
extractorInput.recordOggLaces(new byte[]{(byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x08});
extractorInput.recordOggPacket(new byte[]{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10});
extractorInput.recordOggPacket(new byte[]{0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20});
ParsableByteArray packetArray = new ParsableByteArray(new byte[1024], 0);
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
assertEquals(0x10, packetArray.data[0]);
assertEquals(0x10, packetArray.data[7]);
packetArray.reset();
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
assertEquals(0x20, packetArray.data[0]);
assertEquals(0x20, packetArray.data[7]);
readEOFUntilSuccess(packetArray, 10);
}
public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x01);
extractorInput.recordOggLaces(new byte[]{(byte) 0x08});
extractorInput.recordOggPacket(new byte[]{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10});
extractorInput.recordOggHeader((byte) 0x04, 0, (byte) 0x03);
extractorInput.recordOggLaces(new byte[]{(byte) 0x08, (byte) 0x00, (byte) 0x00});
extractorInput.recordOggPacket(new byte[]{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10});
extractorInput.recordOggHeader((byte) 0x04, 0, (byte) 0x03);
extractorInput.recordOggLaces(new byte[]{(byte) 0x08, 0x00, 0x00});
extractorInput.recordOggPacket(new byte[]{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10});
ParsableByteArray packetArray = new ParsableByteArray(new byte[1024], 0);
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
packetArray.reset();
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
packetArray.reset();
readPacketUntilSuccess(packetArray);
assertEquals(8, packetArray.limit());
packetArray.reset();
readEOFUntilSuccess(packetArray, 10);
assertEquals(0, packetArray.limit());
}
private void readPacketUntilSuccess(ParsableByteArray packetArray) {
int exceptionCount = 0;
while (exceptionCount < 10) {
try {
assertTrue(oggReader.readPacket(extractorInput, packetArray));
break;
} catch (IOException | InterruptedException e) {
exceptionCount++;
extractorInput.resetPeekPosition();
}
}
if (exceptionCount == 10) {
fail("maxException threshold reached");
}
}
private void readEOFUntilSuccess(ParsableByteArray packetArray, int maxExceptions) {
int exceptionCount = 0;
while (exceptionCount < maxExceptions) {
try {
assertFalse(oggReader.readPacket(extractorInput, packetArray));
break;
} catch (IOException | InterruptedException e) {
exceptionCount++;
Log.e(TAG, e.getMessage(), e);
}
}
if (exceptionCount == maxExceptions) {
fail("maxException threshold reached");
}
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import junit.framework.TestCase;
import java.io.IOException;
/**
* Unit test for {@link OggVorbisExtractor}.
*/
public final class OggVorbisExtractorTest extends TestCase {
private static final String TAG = "OggVorbisExtractorTest";
private OggVorbisExtractor extractor;
private RecordableOggExtractorInput extractorInput;
@Override
public void setUp() throws Exception {
super.setUp();
extractorInput = new RecordableOggExtractorInput(1024 * 64);
extractor = new OggVorbisExtractor();
}
public void testSniff() throws Exception {
extractorInput.recordOggHeader((byte) 0x02, 0, (byte) 0x02);
extractorInput.recordOggLaces(new byte[]{120, 120});
assertTrue(extractor.sniff(extractorInput));
}
public void testSniffFails() throws Exception {
extractorInput.recordOggHeader((byte) 0x00, 0, (byte) 0);
assertFalse(extractor.sniff(extractorInput));
}
public void testAppendNumberOfSamples() throws Exception {
ParsableByteArray buffer = new ParsableByteArray(4);
buffer.setLimit(0);
OggVorbisExtractor.appendNumberOfSamples(buffer, 0x01234567);
assertEquals(4, buffer.limit());
assertEquals(0x67, buffer.data[0]);
assertEquals(0x45, buffer.data[1]);
assertEquals(0x23, buffer.data[2]);
assertEquals(0x01, buffer.data[3]);
}
public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {
extractorInput.doThrowExceptionsAtRead(true);
extractorInput.doThrowExceptionsAtPeek(true);
byte[] data = TestData.getVorbisHeaderPages();
extractorInput.record(data);
int exceptionCount = 0;
int maxExceptions = 20;
OggVorbisExtractor.VorbisSetup vorbisSetup;
while (exceptionCount < maxExceptions) {
try {
vorbisSetup = extractor.readSetupHeaders(extractorInput,
new ParsableByteArray(new byte[255 * 255], 0));
assertNotNull(vorbisSetup.idHeader);
assertNotNull(vorbisSetup.commentHeader);
assertNotNull(vorbisSetup.setupHeaderData);
assertNotNull(vorbisSetup.modes);
assertEquals(45, vorbisSetup.commentHeader.length);
assertEquals(30, vorbisSetup.idHeader.data.length);
assertEquals(3597, vorbisSetup.setupHeaderData.length);
assertEquals(-1, vorbisSetup.idHeader.bitrateMax);
assertEquals(-1, vorbisSetup.idHeader.bitrateMin);
assertEquals(66666, vorbisSetup.idHeader.bitrateNominal);
assertEquals(512, vorbisSetup.idHeader.blockSize0);
assertEquals(1024, vorbisSetup.idHeader.blockSize1);
assertEquals(2, vorbisSetup.idHeader.channels);
assertTrue(vorbisSetup.idHeader.framingFlag);
assertEquals(22050, vorbisSetup.idHeader.sampleRate);
assertEquals(0, vorbisSetup.idHeader.version);
assertEquals("Xiph.Org libVorbis I 20030909", vorbisSetup.commentHeader.vendor);
assertEquals(1, vorbisSetup.iLogModes);
assertEquals(data[data.length - 1],
vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]);
assertFalse(vorbisSetup.modes[0].blockFlag);
assertTrue(vorbisSetup.modes[1].blockFlag);
break;
} catch (Throwable e) {
Log.e(TAG, e.getMessage(), e);
extractorInput.resetPeekPosition();
exceptionCount++;
}
}
if (exceptionCount >= maxExceptions) {
fail("more than " + maxExceptions + " exceptions thrown");
}
}
}

View file

@ -0,0 +1,273 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import java.io.EOFException;
import java.io.IOException;
/**
* Implementation of {@link ExtractorInput} for testing purpose.
*/
/* package */ class RecordableExtractorInput implements ExtractorInput {
protected static final byte STREAM_REVISION = 0x00;
private byte[] data;
private int readOffset;
private int writeOffset;
private int peekOffset;
private boolean throwExceptionsAtRead = false;
private boolean throwExceptionsAtPeek = false;
private int numberOfReadsUntilException = 1;
private int numberOfPeeksUntilException = 1;
private int readCounter;
private int peekCounter;
private int maxReadExceptions = Integer.MAX_VALUE;
private int maxPeekExceptions = Integer.MAX_VALUE;
private int readExceptionCounter;
private int peekExceptionCounter;
/**
* Constructs an instance with a initial array of bytes.
*
* @param data the initial data.
* @param writeOffset the {@code writeOffset} from where to start recording.
*/
public RecordableExtractorInput(byte[] data, int writeOffset) {
this.data = data;
this.writeOffset = writeOffset;
}
/**
* Constructs an instance with an empty data array with length {@code maxBytes}.
*
* @param maxBytes the maximal number of bytes this {@code ExtractorInput} can store.
*/
public RecordableExtractorInput(int maxBytes) {
this(new byte[maxBytes], 0);
}
@Override
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
readFully(target, offset, length);
return isEOF() ? C.RESULT_END_OF_INPUT : length;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
readCounter++;
if (throwExceptionsAtRead
&& readExceptionCounter < maxReadExceptions
&& readCounter % numberOfReadsUntilException == 0) {
readCounter = 0;
numberOfReadsUntilException++;
readExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing");
}
if (readOffset + length > writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
System.arraycopy(data, readOffset, target, offset, length);
readOffset += length;
peekOffset = readOffset;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
readFully(target, offset, length, false);
}
@Override
public int skip(int length) throws IOException, InterruptedException {
skipFully(length);
return isEOF() ? C.RESULT_END_OF_INPUT : length;
}
private boolean isEOF() {
return readOffset == writeOffset;
}
@Override
public boolean skipFully(int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
if (readOffset + length >= writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
readOffset += length;
peekOffset = readOffset;
return true;
}
@Override
public void skipFully(int length) throws IOException, InterruptedException {
skipFully(length, false);
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
peekCounter++;
if (throwExceptionsAtPeek
&& peekExceptionCounter < maxPeekExceptions
&& peekCounter % numberOfPeeksUntilException == 0) {
peekCounter = 0;
numberOfPeeksUntilException++;
peekExceptionCounter++;
throw new IOException("deliberately thrown an exception for testing");
}
if (peekOffset + length > writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
System.arraycopy(data, peekOffset, target, offset, length);
peekOffset += length;
return true;
}
@Override
public void peekFully(byte[] target, int offset, int length)
throws IOException, InterruptedException {
peekFully(target, offset, length, false);
}
@Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput)
throws IOException, InterruptedException {
if (peekOffset + length >= writeOffset) {
if (!allowEndOfInput) {
throw new EOFException();
}
return false;
}
peekOffset += length;
return true;
}
@Override
public void advancePeekPosition(int length) throws IOException, InterruptedException {
advancePeekPosition(length, false);
}
@Override
public void resetPeekPosition() {
peekOffset = readOffset;
}
@Override
public long getPosition() {
return readOffset;
}
@Override
public long getLength() {
return writeOffset;
}
/**
* Records the {@code bytes}.
*
* @param bytes the bytes to record.
*/
public void record(final byte[] bytes) {
System.arraycopy(bytes, 0, data, writeOffset, bytes.length);
writeOffset += bytes.length;
}
/** Records a single byte. **/
public void record(byte b) {
record(new byte[]{b});
}
/**
* Gets a byte array with length {@code length} with ascending values starting from 0 (zero).
*
* @param length the length of the array.
* @return an array of bytes with ascending values.
*/
public static byte[] getBytesGrowingValues(int length) {
return fillBytesGrowingValues(new byte[length], length, (byte) 0);
}
/**
* Gets a byte array with length {@code length} with ascending values starting
* from {@code startValue}.
*
* @param length the length of the array.
* @param startValue the value from which to start.
* @return an array of bytes with ascending values starting from {@code startValue}.
*/
public static byte[] getBytesGrowingValues(int length, byte startValue) {
return fillBytesGrowingValues(new byte[length], length, startValue);
}
/**
* Fills the byte array passed as argument with ascending values.
*
* @param bytes the byte array to fill with values.
* @param limit the number of bytes to set in the array.
* @param startValue the startValue from which the values in the array have to start.
*/
public static byte[] fillBytesGrowingValues(byte[] bytes, int limit, byte startValue) {
for (int i = 0; i < bytes.length; i++) {
if (i < limit) {
bytes[i] = (byte) ((i + startValue) % 255);
} else {
bytes[i] = 0;
}
}
return bytes;
}
public void setMaxReadExceptions(int maxReadExceptions) {
this.maxReadExceptions = maxReadExceptions;
}
public void setMaxPeekExceptions(int maxPeekExceptions) {
this.maxPeekExceptions = maxPeekExceptions;
}
public void setNumberOfReadsUntilException(int numberOfReadsUntilException) {
this.numberOfReadsUntilException = numberOfReadsUntilException;
}
public void setNumberOfPeeksUntilException(int numberOfPeeksUntilException) {
this.numberOfPeeksUntilException = numberOfPeeksUntilException;
}
public void doThrowExceptionsAtRead(boolean throwExceptionsAtRead) {
this.throwExceptionsAtRead = throwExceptionsAtRead;
}
public void doThrowExceptionsAtPeek(boolean throwExceptionsAtPeek) {
this.throwExceptionsAtPeek = throwExceptionsAtPeek;
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
/**
* A {@link RecordableOggExtractorInput} with convenient methods to record an OGG byte stream.
*/
/* package */ final class RecordableOggExtractorInput extends RecordableExtractorInput {
private long pageSequenceCounter;
public RecordableOggExtractorInput(byte[] data, int writeOffset) {
super(data, writeOffset);
pageSequenceCounter = 1000;
}
public RecordableOggExtractorInput(int maxBytes) {
this(new byte[maxBytes], 0);
}
/**
* Syntax sugar to make tests more readable.
*
* @param laces the laces to record to the data.
*/
protected void recordOggLaces(final byte[] laces) {
record(laces);
}
/**
* Syntax sugar to make tests more readable.
*
* @param packet the packet bytes to record to the data.
*/
protected void recordOggPacket(final byte[] packet) {
record(packet);
}
protected void recordOggHeader(final byte headerType, final long granule,
final byte pageSegmentCount) {
record((byte) 0x4F); // O
record((byte) 0x67); // g
record((byte) 0x67); // g
record((byte) 0x53); // S
record(STREAM_REVISION);
record(headerType);
recordGranulePosition(granule);
record((byte) 0x00); // LSB of data serial number
record((byte) 0x10);
record((byte) 0x00);
record((byte) 0x00); // MSB of data serial number
recordPageSequenceCounter();
record((byte) 0x00); // LSB of page checksum
record((byte) 0x00);
record((byte) 0x00);
record((byte) 0x00); // MSB of page checksum
record(pageSegmentCount); // 0 - 255
}
protected void recordGranulePosition(long granule) {
record((byte) (granule & 0xFF));
record((byte) ((granule >> 8) & 0xFF));
record((byte) ((granule >> 16) & 0xFF));
record((byte) ((granule >> 24) & 0xFF));
record((byte) ((granule >> 32) & 0xFF));
record((byte) ((granule >> 40) & 0xFF));
record((byte) ((granule >> 48) & 0xFF));
record((byte) ((granule >> 56) & 0xFF));
}
protected void recordPageSequenceCounter() {
record((byte) (pageSequenceCounter & 0xFF));
record((byte) ((pageSequenceCounter >> 8) & 0xFF));
record((byte) ((pageSequenceCounter >> 16) & 0xFF));
record((byte) ((pageSequenceCounter++ >> 24) & 0xFF));
}
}

View file

@ -0,0 +1,337 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ParsableBitArray;
import junit.framework.TestCase;
/**
* Unit test for {@link VorbisBitArray}.
*/
public final class VorbisBitArrayTest extends TestCase {
public void testReadBit() throws ParserException {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0x5c, 0x50
});
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
assertTrue(bitArray.readBit());
assertTrue(bitArray.readBit());
assertTrue(bitArray.readBit());
assertFalse(bitArray.readBit());
assertTrue(bitArray.readBit());
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
assertTrue(bitArray.readBit());
assertFalse(bitArray.readBit());
assertTrue(bitArray.readBit());
assertFalse(bitArray.readBit());
try {
assertFalse(bitArray.readBit());
fail();
} catch (IllegalStateException e) {/* ignored */}
}
public void testSkipBits() throws ParserException {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xF0, 0x0F
});
bitArray.skipBits(10);
assertEquals(10, bitArray.getPosition());
assertTrue(bitArray.readBit());
assertTrue(bitArray.readBit());
assertFalse(bitArray.readBit());
bitArray.skipBits(1);
assertEquals(14, bitArray.getPosition());
assertFalse(bitArray.readBit());
assertFalse(bitArray.readBit());
try {
bitArray.readBit();
fail();
} catch (IllegalStateException e) {
// ignored
}
}
public void testSkipBitsThrowsErrorIfEOB() throws ParserException {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xF0, 0x0F
});
try {
bitArray.skipBits(17);
fail();
} catch (IllegalStateException e) {/* ignored */}
}
public void testGetPosition() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xF0, 0x0F
});
assertEquals(0, bitArray.getPosition());
bitArray.readBit();
assertEquals(1, bitArray.getPosition());
bitArray.readBit();
bitArray.readBit();
bitArray.skipBits(4);
assertEquals(7, bitArray.getPosition());
}
public void testSetPosition() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xF0, 0x0F
});
assertEquals(0, bitArray.getPosition());
bitArray.setPosition(4);
assertEquals(4, bitArray.getPosition());
bitArray.setPosition(15);
assertFalse(bitArray.readBit());
try {
bitArray.readBit();
fail();
} catch (IllegalStateException e) {/* ignored */}
}
public void testSetPositionIllegalPositions() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xF0, 0x0F
});
try {
bitArray.setPosition(16);
fail();
} catch (IllegalArgumentException e) {
assertEquals(0, bitArray.getPosition());
}
try {
bitArray.setPosition(-1);
fail();
} catch (IllegalArgumentException e) {
assertEquals(0, bitArray.getPosition());
}
}
public void testReadInt32() throws ParserException {
byte[] data = {(byte) 0xF0, 0x0F, (byte) 0xF0, 0x0F};
VorbisBitArray lsb = new VorbisBitArray(data);
assertEquals(0x0FF00FF0, lsb.readBits(32));
data = new byte[]{0x0F, (byte) 0xF0, 0x0F, (byte) 0xF0};
lsb = new VorbisBitArray(data);
assertEquals(0xF00FF00F, lsb.readBits(32));
}
public void testReadBits() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0x03, 0x22
});
assertEquals(3, bitArray.readBits(2));
bitArray.skipBits(6);
assertEquals(2, bitArray.readBits(2));
bitArray.skipBits(2);
assertEquals(2, bitArray.readBits(2));
bitArray.reset();
assertEquals(0x2203, bitArray.readBits(16));
}
public void testRead4BitsBeyondBoundary() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
0x2e, 0x10
});
assertEquals(0x2e, bitArray.readBits(7));
assertEquals(7, bitArray.getPosition());
assertEquals(0x0, bitArray.readBits(4));
}
public void testReadBitsBeyondByteBoundaries() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xFF, (byte) 0x0F, (byte) 0xFF, (byte) 0x0F
});
assertEquals(0x0FFF0FFF, bitArray.readBits(32));
bitArray.reset();
bitArray.skipBits(4);
assertEquals(0xF0FF, bitArray.readBits(16));
bitArray.reset();
bitArray.skipBits(6);
assertEquals(0xc3F, bitArray.readBits(12));
bitArray.reset();
bitArray.skipBits(6);
assertTrue(bitArray.readBit());
assertTrue(bitArray.readBit());
assertEquals(24, bitArray.bitsLeft());
bitArray.reset();
bitArray.skipBits(10);
assertEquals(3, bitArray.readBits(5));
assertEquals(15, bitArray.getPosition());
}
public void testReadBitsIllegalLengths() throws Exception {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0x03, 0x22, 0x30
});
// reading zero bits gets 0 without advancing position
// (like a zero-bit read is defined to yield zer0)
assertEquals(0, bitArray.readBits(0));
assertEquals(0, bitArray.getPosition());
bitArray.readBit();
assertEquals(1, bitArray.getPosition());
try {
bitArray.readBits(24);
fail();
} catch (IllegalStateException e) {
assertEquals(1, bitArray.getPosition());
}
}
public void testLimit() throws ParserException {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xc0, 0x02
}, 1);
try {
bitArray.skipBits(9);
fail();
} catch (IllegalStateException e) {
assertEquals(0, bitArray.getPosition());
}
try {
bitArray.readBits(9);
fail();
} catch (IllegalStateException e) {
assertEquals(0, bitArray.getPosition());
}
bitArray.readBits(8);
assertEquals(8, bitArray.getPosition());
try {
bitArray.readBit();
fail();
} catch (IllegalStateException e) {
assertEquals(8, bitArray.getPosition());
}
}
public void testBitsLeft() throws ParserException {
VorbisBitArray bitArray = new VorbisBitArray(new byte[]{
(byte) 0xc0, 0x02
});
assertEquals(16, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.skipBits(1);
assertEquals(15, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.skipBits(3);
assertEquals(12, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.setPosition(6);
assertEquals(10, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.readBit();
assertEquals(9, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.readBits(1);
assertEquals(8, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.readBits(4);
assertEquals(4, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
bitArray.readBits(4);
assertEquals(0, bitArray.bitsLeft());
assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft());
try {
bitArray.readBit();
fail();
} catch (IllegalStateException e) {
assertEquals(0, bitArray.bitsLeft());
}
}
public void testReadBitCompareWithMSb() throws ParserException {
byte[] data = {0x0F};
VorbisBitArray lsb = new VorbisBitArray(data);
ParsableBitArray msb = new ParsableBitArray(data);
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
assertEquals(lsb.readBit(), !msb.readBit());
}
public void testReadBitsCompareWithMSb() throws ParserException {
byte[] data = {0x0F};
VorbisBitArray lsb = new VorbisBitArray(data);
ParsableBitArray msb = new ParsableBitArray(data);
assertEquals(15, lsb.readBits(4));
assertEquals(lsb.readBits(4), msb.readBits(4));
assertEquals(15, msb.readBits(4));
}
public void testReadBitsCompareWithMSbBeyondByteBoundary() throws ParserException {
byte[] data = {(byte) 0xF0, 0x0F};
VorbisBitArray lsb = new VorbisBitArray(data);
ParsableBitArray msb = new ParsableBitArray(data);
assertEquals(0x00, lsb.readBits(4));
assertEquals(0x0F, msb.readBits(4));
assertEquals(0xFF, lsb.readBits(8));
assertEquals(0x00, msb.readBits(8));
assertEquals(0x00, lsb.readBits(4));
assertEquals(0x0F, msb.readBits(4));
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase;
/**
* Unit test for {@link VorbisUtil}.
*/
public final class VorbisUtilTest extends TestCase {
public void testILog() throws Exception {
assertEquals(0, VorbisUtil.iLog(0));
assertEquals(1, VorbisUtil.iLog(1));
assertEquals(2, VorbisUtil.iLog(2));
assertEquals(2, VorbisUtil.iLog(3));
assertEquals(3, VorbisUtil.iLog(4));
assertEquals(3, VorbisUtil.iLog(5));
assertEquals(4, VorbisUtil.iLog(8));
assertEquals(0, VorbisUtil.iLog(-1));
assertEquals(0, VorbisUtil.iLog(-122));
}
public void testReadBits() throws Exception {
assertEquals(0, VorbisUtil.readBits((byte) 0x00, 2, 2));
assertEquals(1, VorbisUtil.readBits((byte) 0x02, 1, 1));
assertEquals(15, VorbisUtil.readBits((byte) 0xF0, 4, 4));
assertEquals(1, VorbisUtil.readBits((byte) 0x80, 1, 7));
}
public void testReadIdHeader() throws Exception {
byte[] data = TestData.getIdentificationHeaderData();
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.VorbisIdHeader vorbisIdHeader =
VorbisUtil.readVorbisIdentificationHeader(headerData);
assertEquals(22050, vorbisIdHeader.sampleRate);
assertEquals(0, vorbisIdHeader.version);
assertTrue(vorbisIdHeader.framingFlag);
assertEquals(2, vorbisIdHeader.channels);
assertEquals(512, vorbisIdHeader.blockSize0);
assertEquals(1024, vorbisIdHeader.blockSize1);
assertEquals(-1, vorbisIdHeader.bitrateMax);
assertEquals(-1, vorbisIdHeader.bitrateMin);
assertEquals(66666, vorbisIdHeader.bitrateNominal);
assertEquals(66666, vorbisIdHeader.getApproximateBitrate());
}
public void testReadCommentHeader() throws ParserException {
byte[] data = TestData.getCommentHeaderDataUTF8();
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData);
assertEquals("Xiph.Org libVorbis I 20120203 (Omnipresent)", commentHeader.vendor);
assertEquals(3, commentHeader.comments.length);
assertEquals("ALBUM=äö", commentHeader.comments[0]);
assertEquals("TITLE=A sample song", commentHeader.comments[1]);
assertEquals("ARTIST=Google", commentHeader.comments[2]);
}
public void testReadVorbisModes() throws ParserException {
byte[] data = TestData.getSetupHeaderData();
ParsableByteArray headerData = new ParsableByteArray(data, data.length);
VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2);
assertEquals(2, modes.length);
assertEquals(false, modes[0].blockFlag);
assertEquals(0, modes[0].mapping);
assertEquals(0, modes[0].transformType);
assertEquals(0, modes[0].windowType);
assertEquals(true, modes[1].blockFlag);
assertEquals(1, modes[1].mapping);
assertEquals(0, modes[1].transformType);
assertEquals(0, modes[1].windowType);
}
}

View file

@ -154,6 +154,13 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.ogg.OggVorbisExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
}
private final ExtractorHolder extractorHolder;

View file

@ -0,0 +1,196 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
/**
* Reads OGG packets from an {@link ExtractorInput}.
*/
/* package */ final class OggReader {
private static final String CAPTURE_PATTERN_PAGE = "OggS";
private final PageHeader pageHeader = new PageHeader();
private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255);
private int currentSegmentIndex = -1;
/**
* Resets this reader.
*/
public void reset() {
pageHeader.reset();
headerArray.reset();
currentSegmentIndex = -1;
}
/**
* Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
* sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
* can resume properly from an error while reading a continued packet spanned across multiple
* pages.
*
* @param input the {@link ExtractorInput} to read data from.
* @param packetArray the {@link ParsableByteArray} to write the packet data into.
* @return {@code true} if the read was successful. {@code false} if the end of the input was
* encountered having read no data.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from input.
*/
public boolean readPacket(ExtractorInput input, ParsableByteArray packetArray)
throws IOException, InterruptedException {
Assertions.checkState(input != null && packetArray != null);
boolean packetComplete = false;
while (!packetComplete) {
if (currentSegmentIndex < 0) {
// We're at the start of a page.
if (!populatePageHeader(input, pageHeader, headerArray, false)) {
return false;
}
currentSegmentIndex = 0;
}
int packetSize = 0;
int segmentIndex = currentSegmentIndex;
// add up packetSize from laces
while (segmentIndex < pageHeader.pageSegmentCount) {
int segmentLength = pageHeader.laces[segmentIndex++];
packetSize += segmentLength;
if (segmentLength != 255) {
// packets end at first lace < 255
break;
}
}
if (packetSize > 0) {
input.readFully(packetArray.data, packetArray.limit(), packetSize);
packetArray.setLimit(packetArray.limit() + packetSize);
packetComplete = pageHeader.laces[segmentIndex - 1] != 255;
}
// advance now since we are sure reading didn't throw an exception
currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 : segmentIndex;
}
return true;
}
/**
* Returns the {@link OggReader.PageHeader} of the current page. The header might not have been
* populated if the first packet has yet to be read.
* <p>
* Note that there is only a single instance of {@code OggReader.PageHeader} which is mutable.
* The value of the fields might be changed by the reader when reading the stream advances and
* the next page is read (which implies reading and populating the next header).
*
* @return the {@code PageHeader} of the current page or {@code null}.
*/
public PageHeader getPageHeader() {
return pageHeader;
}
/**
* Reads/peeks an Ogg page header and stores the data in the {@code header} object passed
* as argument.
*
* @param input the {@link ExtractorInput} to read from.
* @param header the {@link PageHeader} to read from.
* @param scratch a scratch array temporary use.
* @param peek pass {@code true} if data should only be peeked from current peek position.
* @return {@code true} if the read was successful. {@code false} if the end of the
* input was encountered having read no data.
* @throws IOException thrown if reading data fails or the stream is invalid.
* @throws InterruptedException thrown if thread is interrupted when reading/peeking.
*/
public static boolean populatePageHeader(ExtractorInput input, PageHeader header,
ParsableByteArray scratch, boolean peek) throws IOException, InterruptedException {
scratch.reset();
header.reset();
if (!input.peekFully(scratch.data, 0, 27, true)) {
return false;
}
if (scratch.readUnsignedInt() != Util.getIntegerCodeForString(CAPTURE_PATTERN_PAGE)) {
throw new ParserException("expected OggS capture pattern at begin of page");
}
header.revision = scratch.readUnsignedByte();
if (header.revision != 0x00) {
throw new ParserException("unsupported bit stream revision");
}
header.type = scratch.readUnsignedByte();
header.granulePosition = scratch.readLittleEndianLong();
header.streamSerialNumber = scratch.readLittleEndianUnsignedInt();
header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
header.pageChecksum = scratch.readLittleEndianUnsignedInt();
header.pageSegmentCount = scratch.readUnsignedByte();
scratch.reset();
// calculate total size of header including laces
header.headerSize = 27 + header.pageSegmentCount;
input.peekFully(scratch.data, 0, header.pageSegmentCount);
for (int i = 0; i < header.pageSegmentCount; i++) {
header.laces[i] = scratch.readUnsignedByte();
header.bodySize += header.laces[i];
}
if (!peek) {
input.skipFully(header.headerSize);
}
return true;
}
/**
* Data object to store header information. Be aware that {@code laces.length} is always 255.
* Instead use {@code pageSegmentCount} to iterate.
*/
public static final class PageHeader {
public int revision;
public int type;
public long granulePosition;
public long streamSerialNumber;
public long pageSequenceNumber;
public long pageChecksum;
public int pageSegmentCount;
public int headerSize;
public int bodySize;
public int[] laces = new int[255];
/**
* Resets all primitive member fields to zero.
*/
public void reset() {
revision = 0;
type = 0;
granulePosition = 0;
streamSerialNumber = 0;
pageSequenceNumber = 0;
pageChecksum = 0;
pageSegmentCount = 0;
headerSize = 0;
bodySize = 0;
}
}
}

View file

@ -0,0 +1,213 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.ogg.VorbisUtil.Mode;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* {@link Extractor} to extract Vorbis data out of Ogg byte stream.
*/
public final class OggVorbisExtractor implements Extractor {
private static final String TAG = "OggVorbisExtractor";
private static final int OGG_MAX_SEGMENT_SIZE = 255;
private final ParsableByteArray scratch = new ParsableByteArray(
new byte[OGG_MAX_SEGMENT_SIZE * 255], 0);
private final OggReader oggReader = new OggReader();
private TrackOutput trackOutput;
private VorbisSetup vorbisSetup;
private int previousPacketBlockSize;
private long elapsedSamples;
private boolean seenFirstAudioPacket;
private VorbisUtil.VorbisIdHeader vorbisIdHeader;
private VorbisUtil.CommentHeader commentHeader;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
try {
OggReader.PageHeader header = new OggReader.PageHeader();
OggReader.populatePageHeader(input, header, scratch, true);
if ((header.type & 0x02) != 0x02) {
throw new ParserException("expected page to be first page of a logical stream");
}
input.resetPeekPosition();
} catch (ParserException e) {
Log.e(TAG, e.getMessage());
return false;
}
return true;
}
@Override
public void init(ExtractorOutput output) {
trackOutput = output.track(0);
output.endTracks();
output.seekMap(SeekMap.UNSEEKABLE);
}
@Override
public void seek() {
oggReader.reset();
previousPacketBlockSize = -1;
elapsedSamples = 0;
seenFirstAudioPacket = false;
scratch.reset();
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (vorbisSetup == null) {
vorbisSetup = readSetupHeaders(input, scratch);
ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
codecInitialisationData.clear();
codecInitialisationData.add(vorbisSetup.idHeader.data);
codecInitialisationData.add(vorbisSetup.setupHeaderData);
long duration = input.getLength() == C.LENGTH_UNBOUNDED ? C.UNKNOWN_TIME_US
: input.getLength() * 8000000 / vorbisSetup.idHeader.getApproximateBitrate();
trackOutput.format(MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_VORBIS,
this.vorbisSetup.idHeader.bitrateNominal, OGG_MAX_SEGMENT_SIZE * 255, duration,
this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
codecInitialisationData, null));
}
if (oggReader.readPacket(input, scratch)) {
// if this is an audio packet...
if ((scratch.data[0] & 0x01) != 1) {
// ... we need to decode the block size
int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup);
// a packet contains samples produced from overlapping the previous and current frame data
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
: 0;
// codec expects the number of samples appended to audio data
appendNumberOfSamples(scratch, samplesInPacket);
// calculate time and send audio data to codec
long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate;
trackOutput.sampleData(scratch, scratch.limit());
trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null);
// update state in members for next iteration
seenFirstAudioPacket = true;
elapsedSamples += samplesInPacket;
previousPacketBlockSize = packetBlockSize;
}
scratch.reset();
return RESULT_CONTINUE;
}
return RESULT_END_OF_INPUT;
}
//@VisibleForTesting
/* package */ VorbisSetup readSetupHeaders(ExtractorInput input, ParsableByteArray scratch)
throws IOException, InterruptedException {
if (vorbisIdHeader == null) {
oggReader.readPacket(input, scratch);
vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
scratch.reset();
}
if (commentHeader == null) {
oggReader.readPacket(input, scratch);
commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
scratch.reset();
}
oggReader.readPacket(input, scratch);
// the third packet contains the setup header
byte[] setupHeaderData = new byte[scratch.limit()];
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
// partially decode setup header to get the modes
Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
// we need the ilog of modes all the time when extracting, so we compute it once
int iLogModes = VorbisUtil.iLog(modes.length - 1);
scratch.reset();
return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
}
//@VisibleForTesting
/* package */ static void appendNumberOfSamples(ParsableByteArray buffer,
long packetSampleCount) {
buffer.setLimit(buffer.limit() + 4);
// The vorbis decoder expects the number of samples in the packet
// to be appended to the audio data as an int32
buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF);
buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
}
private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
// read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
int modeNumber = VorbisUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
int currentBlockSize;
if (!vorbisSetup.modes[modeNumber].blockFlag) {
currentBlockSize = vorbisSetup.idHeader.blockSize0;
} else {
currentBlockSize = vorbisSetup.idHeader.blockSize1;
}
return currentBlockSize;
}
/**
* Class to hold all data read from Vorbis setup headers.
*/
/* package */ static final class VorbisSetup {
public final VorbisUtil.VorbisIdHeader idHeader;
public final VorbisUtil.CommentHeader commentHeader;
public final byte[] setupHeaderData;
public final Mode[] modes;
public final int iLogModes;
public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
this.idHeader = idHeader;
this.commentHeader = commentHeader;
this.setupHeaderData = setupHeaderData;
this.modes = modes;
this.iLogModes = iLogModes;
}
}
}

View file

@ -0,0 +1,162 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.util.Assertions;
/**
* Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
* specification</a>
*/
/* package */ final class VorbisBitArray {
public final byte[] data;
private int limit;
private int byteOffset;
private int bitOffset;
/**
* Creates a new instance that wraps an existing array.
*
* @param data the array to wrap.
*/
public VorbisBitArray(byte[] data) {
this(data, data.length);
}
/**
* Creates a new instance that wraps an existing array.
*
* @param data the array to wrap.
* @param limit the limit in bytes.
*/
public VorbisBitArray(byte[] data, int limit) {
this.data = data;
this.limit = limit * 8;
}
/** Resets the reading position to zero. */
public void reset() {
byteOffset = 0;
bitOffset = 0;
}
/**
* Reads a single bit.
*
* @return {@code true} if the bit is set, {@code false} otherwise.
*/
public boolean readBit() {
return readBits(1) == 1;
}
/**
* Reads up to 32 bits.
*
* @param numBits The number of bits to read.
* @return An int whose bottom {@code numBits} bits hold the read data.
*/
public int readBits(int numBits) {
Assertions.checkState(getPosition() + numBits <= limit);
if (numBits == 0) {
return 0;
}
int result = 0;
int bitCount = 0;
if (bitOffset != 0) {
bitCount = Math.min(numBits, 8 - bitOffset);
int mask = 0xFF >>> (8 - bitCount);
result = (data[byteOffset] >>> bitOffset) & mask;
bitOffset += bitCount;
if (bitOffset == 8) {
byteOffset++;
bitOffset = 0;
}
}
if (numBits - bitCount > 7) {
int numBytes = (numBits - bitCount) / 8;
for (int i = 0; i < numBytes; i++) {
result |= (data[byteOffset++] & 0xFFL) << bitCount;
bitCount += 8;
}
}
if (numBits > bitCount) {
int bitsOnNextByte = numBits - bitCount;
int mask = 0xFF >>> (8 - bitsOnNextByte);
result |= (data[byteOffset] & mask) << bitCount;
bitOffset += bitsOnNextByte;
}
return result;
}
/**
* Skips {@code numberOfBits} bits.
*
* @param numberOfBits the number of bits to skip.
*/
public void skipBits(int numberOfBits) {
Assertions.checkState(getPosition() + numberOfBits <= limit);
byteOffset += numberOfBits / 8;
bitOffset += numberOfBits % 8;
if (bitOffset > 7) {
byteOffset++;
bitOffset -= 8;
}
}
/**
* Gets the current reading position in bits.
*
* @return the current reading position in bits.
*/
public int getPosition() {
return byteOffset * 8 + bitOffset;
}
/**
* Sets the index of the current reading position in bits.
*
* @param position the new reading position in bits.
*/
public void setPosition(int position) {
Assertions.checkArgument(position < limit && position >= 0);
byteOffset = position / 8;
bitOffset = position - (byteOffset * 8);
}
/**
* Gets the number of remaining bits.
*
* @return number of remaining bits.
*/
public int bitsLeft() {
return limit - getPosition();
}
/**
* Returns the limit in bits.
*
* @return the limit in bits.
**/
public int limit() {
return limit;
}
}

View file

@ -0,0 +1,483 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.util.Arrays;
/**
* Utility methods for parsing vorbis streams.
*/
/* package */ final class VorbisUtil {
private static final String TAG = "VorbisUtil";
/**
* Returns ilog(x), which is the index of the highest set bit in {@code x}.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1">
* Vorbis spec</a>
* @param x the value of which the ilog should be calculated.
* @return ilog(x)
*/
public static int iLog(int x) {
int val = 0;
while (x > 0) {
val++;
x >>>= 1;
}
return val;
}
/**
* Reads an int of {@code length} bits from {@code src} starting at
* {@code leastSignificantBitIndex}.
*
* @param src the {@code byte} to read from.
* @param length the length in bits of the int to read.
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
* @return the int value read.
*/
public static int readBits(byte src, int length, int leastSignificantBitIndex) {
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
}
/**
* Reads a vorbis identification header from {@code headerData}.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
* spec/Identification header</a>
* @param headerData a {@link ParsableByteArray} wrapping the header data.
* @return a {@link VorbisUtil.VorbisIdHeader} with meta data.
* @throws ParserException thrown if invalid capture pattern is detected.
*/
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
throws ParserException {
captureVorbisHeader(0x01, headerData);
long version = headerData.readLittleEndianUnsignedInt();
int channels = headerData.readUnsignedByte();
long sampleRate = headerData.readLittleEndianUnsignedInt();
int bitrateMax = headerData.readLittleEndianInt();
int bitrateNominal = headerData.readLittleEndianInt();
int bitrateMin = headerData.readLittleEndianInt();
int blockSize = headerData.readUnsignedByte();
int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);
int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);
boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;
// raw data of vorbis setup header has to be passed to decoder as CSD buffer #1
byte[] data = Arrays.copyOf(headerData.data, headerData.limit());
return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin,
blockSize0, blockSize1, framingFlag, data);
}
/**
* Reads a vorbis comment header.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">
* Vorbis spec/Comment header</a>
* @param headerData a {@link ParsableByteArray} wrapping the header data.
* @return a {@link VorbisUtil.CommentHeader} with all the comments.
* @throws ParserException thrown if invalid capture pattern is detected.
*/
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
throws ParserException {
int length = captureVorbisHeader(0x03, headerData);
int len = (int) headerData.readLittleEndianUnsignedInt();
length += 4;
String vendor = headerData.readString(len);
length += vendor.length();
long commentListLen = headerData.readLittleEndianUnsignedInt();
String[] comments = new String[(int) commentListLen];
length += 4;
for (int i = 0; i < commentListLen; i++) {
len = (int) headerData.readLittleEndianUnsignedInt();
length += 4;
comments[i] = headerData.readString(len);
length += comments[i].length();
}
if ((headerData.readUnsignedByte() & 0x01) == 0) {
throw new ParserException("framing bit expected to be set");
}
length += 1;
return new CommentHeader(vendor, comments, length);
}
private static int captureVorbisHeader(int headerType, ParsableByteArray idHeader)
throws ParserException {
if (idHeader.readUnsignedByte() != headerType) {
throw new ParserException("expected header type " + Integer.toHexString(headerType));
}
if (!(idHeader.readUnsignedByte() == 'v'
&& idHeader.readUnsignedByte() == 'o'
&& idHeader.readUnsignedByte() == 'r'
&& idHeader.readUnsignedByte() == 'b'
&& idHeader.readUnsignedByte() == 'i'
&& idHeader.readUnsignedByte() == 's')) {
throw new ParserException("expected characters 'vorbis'");
}
return 7; // bytes read
}
/**
* This method reads the modes which are located at the very end of the vorbis setup header.
* That's why we need to partially decode or at least read the entire setup header to know
* where to start reading the modes.
*
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">
* Vorbis spec/Setup header</a>
* @param headerData a {@link ParsableByteArray} containing setup header data.
* @param channels the number of channels.
* @return an array of {@link Mode}s.
* @throws ParserException thrown if bit stream is invalid.
*/
public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
throws ParserException {
captureVorbisHeader(0x05, headerData);
int numberOfBooks = headerData.readUnsignedByte() + 1;
VorbisBitArray bitArray = new VorbisBitArray(headerData.data);
bitArray.skipBits(headerData.getPosition() * 8);
for (int i = 0; i < numberOfBooks; i++) {
readBook(bitArray);
}
int timeCount = bitArray.readBits(6) + 1;
for (int i = 0; i < timeCount; i++) {
if (bitArray.readBits(16) != 0x00) {
throw new ParserException("placeholder of time domain transforms not zeroed out");
}
}
readFloors(bitArray);
readResidues(bitArray);
readMappings(channels, bitArray);
Mode[] modes = readModes(bitArray);
if (!bitArray.readBit()) {
throw new ParserException("framing bit after modes not set as expected");
}
return modes;
}
private static Mode[] readModes(VorbisBitArray bitArray) throws ParserException {
int modeCount = bitArray.readBits(6) + 1;
Mode[] modes = new Mode[modeCount];
for (int i = 0; i < modeCount; i++) {
boolean blockFlag = bitArray.readBit();
int windowType = bitArray.readBits(16);
int transformType = bitArray.readBits(16);
int mapping = bitArray.readBits(8);
modes[i] = new Mode(blockFlag, windowType, transformType, mapping);
}
return modes;
}
private static void readMappings(int channels, VorbisBitArray bitArray)
throws ParserException {
int mappingsCount = bitArray.readBits(6) + 1;
for (int i = 0; i < mappingsCount; i++) {
int mappingType = bitArray.readBits(16);
switch (mappingType) {
case 0:
int submaps;
if (bitArray.readBit()) {
submaps = bitArray.readBits(4) + 1;
} else {
submaps = 1;
}
int couplingSteps;
if (bitArray.readBit()) {
couplingSteps = bitArray.readBits(8) + 1;
for (int j = 0; j < couplingSteps; j++) {
bitArray.skipBits(iLog(channels - 1)); // magnitude
bitArray.skipBits(iLog(channels - 1)); // angle
}
} /*else {
couplingSteps = 0;
}*/
if (bitArray.readBits(2) != 0x00) {
throw new ParserException("to reserved bits must be zero after mapping coupling steps");
}
if (submaps > 1) {
for (int j = 0; j < channels; j++) {
bitArray.skipBits(4); // mappingMux
}
}
for (int j = 0; j < submaps; j++) {
bitArray.skipBits(8); // discard
bitArray.skipBits(8); // submapFloor
bitArray.skipBits(8); // submapResidue
}
break;
default:
Log.e(TAG, "mapping type other than 0 not supported: " + mappingType);
}
}
}
private static void readResidues(VorbisBitArray bitArray) throws ParserException {
int residueCount = bitArray.readBits(6) + 1;
for (int i = 0; i < residueCount; i++) {
int residueType = bitArray.readBits(16);
if (residueType > 2) {
throw new ParserException("residueType greater than 2 is not decodable");
} else {
bitArray.skipBits(24); // begin
bitArray.skipBits(24); // end
bitArray.skipBits(24); // partitionSize (add one)
int classifications = bitArray.readBits(6) + 1;
bitArray.skipBits(8); // classbook
int[] cascade = new int[classifications];
for (int j = 0; j < classifications; j++) {
int highBits = 0;
int lowBits = bitArray.readBits(3);
if (bitArray.readBit()) {
highBits = bitArray.readBits(5);
}
cascade[j] = highBits * 8 + lowBits;
}
for (int j = 0; j < classifications; j++) {
for (int k = 0; k < 8; k++) {
if ((cascade[j] & (0x01 << k)) != 0) {
bitArray.skipBits(8); // discard
}
}
}
}
}
}
private static void readFloors(VorbisBitArray bitArray) throws ParserException {
int floorCount = bitArray.readBits(6) + 1;
for (int i = 0; i < floorCount; i++) {
int floorType = bitArray.readBits(16);
switch (floorType) {
case 0:
bitArray.skipBits(8); //order
bitArray.skipBits(16); // rate
bitArray.skipBits(16); // barkMapSize
bitArray.skipBits(6); // amplitudeBits
bitArray.skipBits(8); // amplitudeOffset
int floorNumberOfBooks = bitArray.readBits(4) + 1;
for (int j = 0; j < floorNumberOfBooks; j++) {
bitArray.skipBits(8);
}
break;
case 1:
int partitions = bitArray.readBits(5);
int maximumClass = -1;
int[] partitionClassList = new int[partitions];
for (int j = 0; j < partitions; j++) {
partitionClassList[j] = bitArray.readBits(4);
if (partitionClassList[j] > maximumClass) {
maximumClass = partitionClassList[j];
}
}
int[] classDimensions = new int[maximumClass + 1];
for (int j = 0; j < classDimensions.length; j++) {
classDimensions[j] = bitArray.readBits(3) + 1;
int classSubclasses = bitArray.readBits(2);
if (classSubclasses > 0) {
bitArray.skipBits(8); // classMasterbooks
}
for (int k = 0; k < (1 << classSubclasses); k++) {
bitArray.skipBits(8); // subclassBook (subtract 1)
}
}
bitArray.skipBits(2); // multiplier (add one)
int rangeBits = bitArray.readBits(4);
int count = 0;
for (int j = 0, k = 0; j < partitions; j++) {
int idx = partitionClassList[j];
count += classDimensions[idx];
for (; k < count; k++) {
bitArray.skipBits(rangeBits); // floorValue
}
}
break;
default:
throw new ParserException("floor type greater than 1 not decodable: " + floorType);
}
}
}
private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException {
if (bitArray.readBits(24) != 0x564342) {
throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at "
+ bitArray.getPosition());
}
int dimensions = bitArray.readBits(16);
int entries = bitArray.readBits(24);
long[] lengthMap = new long[entries];
boolean isOrdered = bitArray.readBit();
if (!isOrdered) {
boolean isSparse = bitArray.readBit();
for (int i = 0; i < lengthMap.length; i++) {
if (isSparse) {
if (bitArray.readBit()) {
lengthMap[i] = bitArray.readBits(5) + 1;
} else { // entry unused
lengthMap[i] = 0;
}
} else { // not sparse
lengthMap[i] = bitArray.readBits(5) + 1;
}
}
} else {
int length = bitArray.readBits(5) + 1;
for (int i = 0; i < lengthMap.length;) {
int num = bitArray.readBits(iLog(entries - i));
for (int j = 0; j < num && i < lengthMap.length; i++, j++) {
lengthMap[i] = length;
}
length++;
}
}
int lookupType = bitArray.readBits(4);
if (lookupType > 2) {
throw new ParserException("lookup type greater than 2 not decodable: " + lookupType);
} else if (lookupType == 1 || lookupType == 2) {
bitArray.skipBits(32); // minimumValue
bitArray.skipBits(32); // deltaValue
int valueBits = bitArray.readBits(4) + 1;
bitArray.skipBits(1); // sequenceP
long lookupValuesCount;
if (lookupType == 1) {
if (dimensions != 0) {
lookupValuesCount = mapType1QuantValues(entries, dimensions);
} else {
// TODO no sample file found yet
lookupValuesCount = 0;
}
} else {
// TODO no sample file found yet
lookupValuesCount = entries * dimensions;
}
// discard (no decoding required yet)
bitArray.skipBits((int) (lookupValuesCount * valueBits));
}
return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered);
}
/**
* @see <a href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">function
* _book_maptype1_quantvals</a> of libvorbis.
*/
private static long mapType1QuantValues(long entries, long dimension) {
return (long) Math.floor(Math.pow((double) entries, 1.d / dimension));
}
public static final class CodeBook {
public final int dimensions;
public final int entries;
public final long[] lengthMap;
public final int lookupType;
public final boolean isOrdered;
public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType,
boolean isOrdered) {
this.dimensions = dimensions;
this.entries = entries;
this.lengthMap = lengthMap;
this.lookupType = lookupType;
this.isOrdered = isOrdered;
}
}
public static final class CommentHeader {
public final String vendor;
public final String[] comments;
public final int length;
public CommentHeader(String vendor, String[] comments, int length) {
this.vendor = vendor;
this.comments = comments;
this.length = length;
}
}
public static final class VorbisIdHeader {
public final long version;
public final int channels;
public final long sampleRate;
public final int bitrateMax;
public final int bitrateNominal;
public final int bitrateMin;
public final int blockSize0;
public final int blockSize1;
public final boolean framingFlag;
public final byte[] data;
public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
byte[] data) {
this.version = version;
this.channels = channels;
this.sampleRate = sampleRate;
this.bitrateMax = bitrateMax;
this.bitrateNominal = bitrateNominal;
this.bitrateMin = bitrateMin;
this.blockSize0 = blockSize0;
this.blockSize1 = blockSize1;
this.framingFlag = framingFlag;
this.data = data;
}
public int getApproximateBitrate() {
return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
}
}
public static final class Mode {
public final boolean blockFlag;
public final int windowType;
public final int transformType;
public final int mapping;
public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
this.blockFlag = blockFlag;
this.windowType = windowType;
this.transformType = transformType;
this.mapping = mapping;
}
}
}