Add WavExtractor for extracting samples from WAV files.

This version only supports 16-bit uncompressed PCM. A follow-up CL will
add support for other sample bit depths.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117809475
This commit is contained in:
olly 2016-03-22 04:07:23 -07:00 committed by Oliver Woodman
parent a49c8dc86d
commit 733f2ccd1c
5 changed files with 432 additions and 3 deletions

View file

@ -57,9 +57,10 @@ import java.util.List;
* <li>Ogg Vorbis ({@link com.google.android.exoplayer.extractor.ogg.OggVorbisExtractor}</li>
* <li>MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})</li>
* <li>AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})</li>
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}</li>
* <li>MPEG PS ({@link com.google.android.exoplayer.extractor.ts.PsExtractor}</li>
* <li>FLV ({@link com.google.android.exoplayer.extractor.flv.FlvExtractor}</li>
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor})</li>
* <li>MPEG PS ({@link com.google.android.exoplayer.extractor.ts.PsExtractor})</li>
* <li>FLV ({@link com.google.android.exoplayer.extractor.flv.FlvExtractor})</li>
* <li>WAV ({@link com.google.android.exoplayer.extractor.wav.WavExtractor})</li>
* </ul>
*
* <p>Seeking in AAC, MPEG TS and FLV streams is not supported.
@ -183,6 +184,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
} catch (ClassNotFoundException e) {
// Extractor not found.
}
try {
DEFAULT_EXTRACTOR_CLASSES.add(
Class.forName("com.google.android.exoplayer.extractor.wav.WavExtractor")
.asSubclass(Extractor.class));
} catch (ClassNotFoundException e) {
// Extractor not found.
}
}
private final ExtractorHolder extractorHolder;

View file

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 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.wav;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.Format;
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.util.MimeTypes;
import java.io.IOException;
/** {@link Extractor} to extract samples from a WAV byte stream. */
public final class WavExtractor implements Extractor, SeekMap {
/** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
private static final int MAX_INPUT_SIZE = 32 * 1024;
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private WavHeader wavHeader;
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
return WavHeaderReader.peek(input) != null;
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(0);
wavHeader = null;
output.endTracks();
}
@Override
public void seek() {
// Do nothing.
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (wavHeader == null) {
wavHeader = WavHeaderReader.peek(input);
if (wavHeader == null) {
// Should only happen if the media wasn't sniffed.
throw new ParserException("Unsupported or unrecognized wav header.");
}
Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW,
wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(),
wavHeader.getSampleRateHz(), null, null);
trackOutput.format(format);
}
if (!wavHeader.hasDataBounds()) {
WavHeaderReader.skipToData(input, wavHeader);
extractorOutput.seekMap(this);
}
long inputPosition = input.getPosition();
int bytesRead = trackOutput.sampleData(input, MAX_INPUT_SIZE, true);
if (bytesRead == RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT;
}
trackOutput.sampleMetadata(
wavHeader.getTimeUs(inputPosition), C.SAMPLE_FLAG_SYNC, bytesRead, 0, null);
return RESULT_CONTINUE;
}
// SeekMap implementation.
@Override
public long getDurationUs() {
return wavHeader.getDurationUs();
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getPosition(long timeUs) {
return wavHeader.getPosition(timeUs);
}
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 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.wav;
import com.google.android.exoplayer.C;
/** Header for a WAV file. */
/*package*/ final class WavHeader {
/** Number of audio chanels. */
private final int numChannels;
/** Sample rate in Hertz. */
private final int sampleRateHz;
/** Average bytes per second for the sample data. */
private final int averageBytesPerSecond;
/** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */
private final int blockAlignment;
/** Bits per sample for the audio data. */
private final int bitsPerSample;
/** Offset to the start of sample data. */
private long dataStartPosition;
/** Total size in bytes of the sample data. */
private long dataSize;
public WavHeader(
int numChannels,
int sampleRateHz,
int averageBytesPerSecond,
int blockAlignment,
int bitsPerSample) {
this.numChannels = numChannels;
this.sampleRateHz = sampleRateHz;
this.averageBytesPerSecond = averageBytesPerSecond;
this.blockAlignment = blockAlignment;
this.bitsPerSample = bitsPerSample;
}
/** Returns the duration in microseconds of this WAV. */
public long getDurationUs() {
return (getNumFrames() * C.MICROS_PER_SECOND) / sampleRateHz;
}
/** Returns the number of samples in this WAV. */
public long getNumSamples() {
return dataSize / getBytesPerSample();
}
/** Returns the number of frames in this WAV. */
public long getNumFrames() {
return getNumSamples() / getNumChannels();
}
/** Returns the bytes per sample of this WAV. */
public int getBytesPerSample() {
return blockAlignment / numChannels;
}
/** Returns the bitrate of this WAV. */
public int getBitrate() {
return sampleRateHz * bitsPerSample * numChannels;
}
/** Returns the sample rate in Hertz of this WAV. */
public int getSampleRateHz() {
return sampleRateHz;
}
/** Returns the number of audio channels in this WAV. */
public int getNumChannels() {
return numChannels;
}
/** Returns the position in bytes in this WAV for the given time in microseconds. */
public long getPosition(long timeUs) {
long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Round down to nearest frame.
return (unroundedPosition / numChannels) * numChannels + dataStartPosition;
}
/** Returns the time in microseconds for the given position in bytes in this WAV. */
public long getTimeUs(long position) {
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
}
/** Returns true if the data start position and size have been set. */
public boolean hasDataBounds() {
return dataStartPosition != 0 && dataSize != 0;
}
/** Sets the start position and size in bytes of sample data in this WAV. */
public void setDataBounds(long dataStartPosition, long dataSize) {
this.dataStartPosition = dataStartPosition;
this.dataSize = dataSize;
}
}

View file

@ -0,0 +1,186 @@
/*
* Copyright (C) 2016 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.wav;
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 android.util.Log;
import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
/*package*/ final class WavHeaderReader {
private static final String TAG = "WavHeaderReader";
/** Integer PCM audio data. */
private static final int TYPE_PCM = 0x0001;
/** Extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
/**
* Peeks and returns a {@code WavHeader}.
*
* @param input Input stream to peek the WAV header from.
* @throws IOException If peeking from the input fails.
* @throws InterruptedException If interrupted while peeking from input.
* @throws ParserException If the input file is an incorrect RIFF WAV.
* @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a
* supported WAV format.
*/
public static WavHeader peek(ExtractorInput input)
throws IOException, InterruptedException, ParserException {
Assertions.checkNotNull(input);
// Allocate a scratch buffer large enough to store the format chunk.
ParsableByteArray scratch = new ParsableByteArray(16);
// Attempt to read the RIFF chunk.
ChunkHeader riffChunkHeader = ChunkHeader.peek(input, scratch);
if (riffChunkHeader.id != Util.getIntegerCodeForString("RIFF")) {
return null;
}
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
int riffFormat = scratch.readInt();
if (riffFormat != Util.getIntegerCodeForString("WAVE")) {
Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
return null;
}
// Attempt to read the format chunk.
ChunkHeader formatChunkHeader = ChunkHeader.peek(input, scratch);
if (formatChunkHeader.id != Util.getIntegerCodeForString("fmt ")) {
throw new ParserException(
"Second chunk in RIFF WAV should be format; got: " + formatChunkHeader.id);
}
input.peekFully(scratch.data, 0, 16);
scratch.setPosition(0);
int type = scratch.readLittleEndianUnsignedShort();
int numChannels = scratch.readLittleEndianUnsignedShort();
int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();
int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
int blockAlignment = scratch.readLittleEndianUnsignedShort();
int bitsPerSample = scratch.readLittleEndianUnsignedShort();
int expectedBlockAlignment = numChannels * bitsPerSample / 8;
if (blockAlignment != expectedBlockAlignment) {
throw new ParserException(
"Expected WAV block alignment of: "
+ expectedBlockAlignment
+ "; got: "
+ blockAlignment);
}
if (bitsPerSample != 16) {
Log.e(TAG, "Only 16-bit WAVs are supported; got: " + bitsPerSample);
return null;
}
if (type == TYPE_PCM) {
Assertions.checkState(formatChunkHeader.size == 16);
// No more data to read.
} else if (type == TYPE_WAVE_FORMAT_EXTENSIBLE) {
Assertions.checkState(formatChunkHeader.size == 40);
// Skip extensionSize, validBitsPerSample, channelMask, subFormatGuid.
input.advancePeekPosition(2 + 2 + 4 + 16);
} else {
Log.e(TAG, "Unsupported WAV format type: " + type);
return null;
}
return new WavHeader(
numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample);
}
/**
* Skips to the data in the given WAV input stream and returns its data size. After calling, the
* input stream's position will point to the start of sample data in the WAV.
* <p>
* If an exception is thrown, the input position will be left pointing to a chunk header.
*
* @param input Input stream to skip to the data chunk in. Its peek position must be pointing to
* a valid chunk header that is not the RIFF chunk.
* @param wavHeader WAV header to populate with data bounds.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from input.
* @throws ParserException If an error occurs parsing chunks.
*/
public static void skipToData(ExtractorInput input, WavHeader wavHeader)
throws IOException, InterruptedException, ParserException {
Assertions.checkNotNull(input);
Assertions.checkNotNull(wavHeader);
ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
// Skip all chunks until we hit the data header.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != Util.getIntegerCodeForString("data")) {
Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
if (bytesToSkip > Integer.MAX_VALUE) {
throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id);
}
input.skipFully((int) bytesToSkip);
chunkHeader = ChunkHeader.peek(input, scratch);
}
// Skip past the "data" header.
input.skipFully(ChunkHeader.SIZE_IN_BYTES);
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
}
/** Container for a WAV chunk header. */
private static final class ChunkHeader {
/** Size in bytes of a WAV chunk header. */
public static final int SIZE_IN_BYTES = 8;
/** 4-character identifier, stored as an integer, for this chunk. */
public final int id;
/** Size of this chunk in bytes. */
public final long size;
private ChunkHeader(int id, long size) {
this.id = id;
this.size = size;
}
/**
* Peeks and returns a {@link ChunkHeader}.
*
* @param input Input stream to peek the chunk header from.
* @param scratch Buffer for temporary use.
* @throws IOException If peeking from the input fails.
* @throws InterruptedException If interrupted while peeking from input.
* @return A new {@code ChunkHeader} peeked from {@code input}.
*/
public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch)
throws IOException, InterruptedException {
input.peekFully(scratch.data, 0, SIZE_IN_BYTES);
scratch.setPosition(0);
int id = scratch.readInt();
long size = scratch.readLittleEndianUnsignedInt();
return new ChunkHeader(id, size);
}
}
}

View file

@ -194,6 +194,13 @@ public final class ParsableByteArray {
| (data[position++] & 0xFF);
}
/** Reads the next three bytes as a signed value in little endian order. */
public int readLittleEndianInt24() {
return (data[position++] & 0xFF)
| (data[position++] & 0xFF) << 8
| (data[position++] & 0xFF) << 16;
}
/** Reads the next three bytes as an unsigned value in little endian order. */
public int readLittleEndianUnsignedInt24() {
return (data[position++] & 0xFF)
@ -294,6 +301,20 @@ public final class ParsableByteArray {
return result;
}
/**
* Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit
* is a zero.
*
* @throws IllegalStateException Thrown if the top bit of the input data is set.
*/
public int readLittleEndianUnsignedIntToInt() {
int result = readLittleEndianInt();
if (result < 0) {
throw new IllegalStateException("Top bit not zero: " + result);
}
return result;
}
/**
* Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero.
*