mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
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:
parent
a49c8dc86d
commit
733f2ccd1c
5 changed files with 432 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue