mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +00:00
OggVorbisExtractor (WIP - Seeking not yet enabled)
This commit is contained in:
parent
88fa1495c4
commit
89ce1ccedf
13 changed files with 3366 additions and 0 deletions
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue