WavExtractor: split header reading state into 2 states

This refactoring is the basis to support RF64 (see
Issue: google/ExoPlayer#9543).

#minor-release

PiperOrigin-RevId: 407301056
This commit is contained in:
kimvde 2021-11-03 11:01:07 +00:00 committed by Ian Baker
parent 043a80a5ba
commit 7168f93829
4 changed files with 121 additions and 95 deletions

View File

@ -63,12 +63,18 @@ public final class WavExtractor implements Extractor {
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_USE}) @Target({ElementType.TYPE_USE})
@IntDef({STATE_READING_HEADER, STATE_SKIPPING_TO_SAMPLE_DATA, STATE_READING_SAMPLE_DATA}) @IntDef({
STATE_READING_FILE_TYPE,
STATE_READING_FORMAT,
STATE_SKIPPING_TO_SAMPLE_DATA,
STATE_READING_SAMPLE_DATA
})
private @interface State {} private @interface State {}
private static final int STATE_READING_HEADER = 0; private static final int STATE_READING_FILE_TYPE = 0;
private static final int STATE_SKIPPING_TO_SAMPLE_DATA = 1; private static final int STATE_READING_FORMAT = 1;
private static final int STATE_READING_SAMPLE_DATA = 2; private static final int STATE_SKIPPING_TO_SAMPLE_DATA = 2;
private static final int STATE_READING_SAMPLE_DATA = 3;
private @MonotonicNonNull ExtractorOutput extractorOutput; private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput; private @MonotonicNonNull TrackOutput trackOutput;
@ -78,14 +84,14 @@ public final class WavExtractor implements Extractor {
private long dataEndPosition; private long dataEndPosition;
public WavExtractor() { public WavExtractor() {
state = STATE_READING_HEADER; state = STATE_READING_FILE_TYPE;
dataStartPosition = C.POSITION_UNSET; dataStartPosition = C.POSITION_UNSET;
dataEndPosition = C.POSITION_UNSET; dataEndPosition = C.POSITION_UNSET;
} }
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException { public boolean sniff(ExtractorInput input) throws IOException {
return WavHeaderReader.peek(input) != null; return WavHeaderReader.checkFileType(input);
} }
@Override @Override
@ -97,7 +103,7 @@ public final class WavExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
state = position == 0 ? STATE_READING_HEADER : STATE_READING_SAMPLE_DATA; state = position == 0 ? STATE_READING_FILE_TYPE : STATE_READING_SAMPLE_DATA;
if (outputWriter != null) { if (outputWriter != null) {
outputWriter.reset(timeUs); outputWriter.reset(timeUs);
} }
@ -113,8 +119,11 @@ public final class WavExtractor implements Extractor {
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
assertInitialized(); assertInitialized();
switch (state) { switch (state) {
case STATE_READING_HEADER: case STATE_READING_FILE_TYPE:
readHeader(input); readFileType(input);
return Extractor.RESULT_CONTINUE;
case STATE_READING_FORMAT:
readFormat(input);
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
case STATE_SKIPPING_TO_SAMPLE_DATA: case STATE_SKIPPING_TO_SAMPLE_DATA:
skipToSampleData(input); skipToSampleData(input);
@ -132,50 +141,54 @@ public final class WavExtractor implements Extractor {
Util.castNonNull(extractorOutput); Util.castNonNull(extractorOutput);
} }
@RequiresNonNull({"extractorOutput", "trackOutput"}) private void readFileType(ExtractorInput input) throws IOException {
private void readHeader(ExtractorInput input) throws IOException {
Assertions.checkState(input.getPosition() == 0); Assertions.checkState(input.getPosition() == 0);
if (dataStartPosition != C.POSITION_UNSET) { if (dataStartPosition != C.POSITION_UNSET) {
input.skipFully(dataStartPosition); input.skipFully(dataStartPosition);
state = STATE_READING_SAMPLE_DATA; state = STATE_READING_SAMPLE_DATA;
return; return;
} }
WavHeader header = WavHeaderReader.peek(input); if (!WavHeaderReader.checkFileType(input)) {
if (header == null) {
// Should only happen if the media wasn't sniffed. // Should only happen if the media wasn't sniffed.
throw ParserException.createForMalformedContainer( throw ParserException.createForMalformedContainer(
"Unsupported or unrecognized wav header.", /* cause= */ null); "Unsupported or unrecognized wav file type.", /* cause= */ null);
} }
input.skipFully((int) (input.getPeekPosition() - input.getPosition())); input.skipFully((int) (input.getPeekPosition() - input.getPosition()));
state = STATE_READING_FORMAT;
}
if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { @RequiresNonNull({"extractorOutput", "trackOutput"})
outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); private void readFormat(ExtractorInput input) throws IOException {
} else if (header.formatType == WavUtil.TYPE_ALAW) { WavFormat wavFormat = WavHeaderReader.readFormat(input);
if (wavFormat.formatType == WavUtil.TYPE_IMA_ADPCM) {
outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, wavFormat);
} else if (wavFormat.formatType == WavUtil.TYPE_ALAW) {
outputWriter = outputWriter =
new PassthroughOutputWriter( new PassthroughOutputWriter(
extractorOutput, extractorOutput,
trackOutput, trackOutput,
header, wavFormat,
MimeTypes.AUDIO_ALAW, MimeTypes.AUDIO_ALAW,
/* pcmEncoding= */ Format.NO_VALUE); /* pcmEncoding= */ Format.NO_VALUE);
} else if (header.formatType == WavUtil.TYPE_MLAW) { } else if (wavFormat.formatType == WavUtil.TYPE_MLAW) {
outputWriter = outputWriter =
new PassthroughOutputWriter( new PassthroughOutputWriter(
extractorOutput, extractorOutput,
trackOutput, trackOutput,
header, wavFormat,
MimeTypes.AUDIO_MLAW, MimeTypes.AUDIO_MLAW,
/* pcmEncoding= */ Format.NO_VALUE); /* pcmEncoding= */ Format.NO_VALUE);
} else { } else {
@C.PcmEncoding @C.PcmEncoding
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); int pcmEncoding =
WavUtil.getPcmEncodingForType(wavFormat.formatType, wavFormat.bitsPerSample);
if (pcmEncoding == C.ENCODING_INVALID) { if (pcmEncoding == C.ENCODING_INVALID) {
throw ParserException.createForUnsupportedContainerFeature( throw ParserException.createForUnsupportedContainerFeature(
"Unsupported WAV format type: " + header.formatType); "Unsupported WAV format type: " + wavFormat.formatType);
} }
outputWriter = outputWriter =
new PassthroughOutputWriter( new PassthroughOutputWriter(
extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); extractorOutput, trackOutput, wavFormat, MimeTypes.AUDIO_RAW, pcmEncoding);
} }
state = STATE_SKIPPING_TO_SAMPLE_DATA; state = STATE_SKIPPING_TO_SAMPLE_DATA;
} }
@ -236,7 +249,7 @@ public final class WavExtractor implements Extractor {
private final ExtractorOutput extractorOutput; private final ExtractorOutput extractorOutput;
private final TrackOutput trackOutput; private final TrackOutput trackOutput;
private final WavHeader header; private final WavFormat wavFormat;
private final Format format; private final Format format;
/** The target size of each output sample, in bytes. */ /** The target size of each output sample, in bytes. */
private final int targetSampleSizeBytes; private final int targetSampleSizeBytes;
@ -258,33 +271,33 @@ public final class WavExtractor implements Extractor {
public PassthroughOutputWriter( public PassthroughOutputWriter(
ExtractorOutput extractorOutput, ExtractorOutput extractorOutput,
TrackOutput trackOutput, TrackOutput trackOutput,
WavHeader header, WavFormat wavFormat,
String mimeType, String mimeType,
@C.PcmEncoding int pcmEncoding) @C.PcmEncoding int pcmEncoding)
throws ParserException { throws ParserException {
this.extractorOutput = extractorOutput; this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
this.header = header; this.wavFormat = wavFormat;
int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; int bytesPerFrame = wavFormat.numChannels * wavFormat.bitsPerSample / 8;
// Validate the header. Blocks are expected to correspond to single frames. // Validate the WAV format. Blocks are expected to correspond to single frames.
if (header.blockSize != bytesPerFrame) { if (wavFormat.blockSize != bytesPerFrame) {
throw ParserException.createForMalformedContainer( throw ParserException.createForMalformedContainer(
"Expected block size: " + bytesPerFrame + "; got: " + header.blockSize, "Expected block size: " + bytesPerFrame + "; got: " + wavFormat.blockSize,
/* cause= */ null); /* cause= */ null);
} }
int constantBitrate = header.frameRateHz * bytesPerFrame * 8; int constantBitrate = wavFormat.frameRateHz * bytesPerFrame * 8;
targetSampleSizeBytes = targetSampleSizeBytes =
max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); max(bytesPerFrame, wavFormat.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
format = format =
new Format.Builder() new Format.Builder()
.setSampleMimeType(mimeType) .setSampleMimeType(mimeType)
.setAverageBitrate(constantBitrate) .setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate) .setPeakBitrate(constantBitrate)
.setMaxInputSize(targetSampleSizeBytes) .setMaxInputSize(targetSampleSizeBytes)
.setChannelCount(header.numChannels) .setChannelCount(wavFormat.numChannels)
.setSampleRate(header.frameRateHz) .setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(pcmEncoding) .setPcmEncoding(pcmEncoding)
.build(); .build();
} }
@ -299,7 +312,7 @@ public final class WavExtractor implements Extractor {
@Override @Override
public void init(int dataStartPosition, long dataEndPosition) { public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap( extractorOutput.seekMap(
new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); new WavSeekMap(wavFormat, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
trackOutput.format(format); trackOutput.format(format);
} }
@ -320,13 +333,13 @@ public final class WavExtractor implements Extractor {
// Write the corresponding sample metadata. Samples must be a whole number of frames. It's // Write the corresponding sample metadata. Samples must be a whole number of frames. It's
// possible that the number of pending output bytes is not a whole number of frames if the // possible that the number of pending output bytes is not a whole number of frames if the
// stream ended unexpectedly. // stream ended unexpectedly.
int bytesPerFrame = header.blockSize; int bytesPerFrame = wavFormat.blockSize;
int pendingFrames = pendingOutputBytes / bytesPerFrame; int pendingFrames = pendingOutputBytes / bytesPerFrame;
if (pendingFrames > 0) { if (pendingFrames > 0) {
long timeUs = long timeUs =
startTimeUs startTimeUs
+ Util.scaleLargeTimestamp( + Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); outputFrameCount, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
int size = pendingFrames * bytesPerFrame; int size = pendingFrames * bytesPerFrame;
int offset = pendingOutputBytes - size; int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata( trackOutput.sampleMetadata(
@ -356,7 +369,7 @@ public final class WavExtractor implements Extractor {
private final ExtractorOutput extractorOutput; private final ExtractorOutput extractorOutput;
private final TrackOutput trackOutput; private final TrackOutput trackOutput;
private final WavHeader header; private final WavFormat wavFormat;
/** Number of frames per block of the input (yet to be decoded) data. */ /** Number of frames per block of the input (yet to be decoded) data. */
private final int framesPerBlock; private final int framesPerBlock;
@ -386,23 +399,26 @@ public final class WavExtractor implements Extractor {
private long outputFrameCount; private long outputFrameCount;
public ImaAdPcmOutputWriter( public ImaAdPcmOutputWriter(
ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) ExtractorOutput extractorOutput, TrackOutput trackOutput, WavFormat wavFormat)
throws ParserException { throws ParserException {
this.extractorOutput = extractorOutput; this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
this.header = header; this.wavFormat = wavFormat;
targetSampleSizeFrames = max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); targetSampleSizeFrames = max(1, wavFormat.frameRateHz / TARGET_SAMPLES_PER_SECOND);
ParsableByteArray scratch = new ParsableByteArray(header.extraData); ParsableByteArray scratch = new ParsableByteArray(wavFormat.extraData);
scratch.readLittleEndianUnsignedShort(); scratch.readLittleEndianUnsignedShort();
framesPerBlock = scratch.readLittleEndianUnsignedShort(); framesPerBlock = scratch.readLittleEndianUnsignedShort();
int numChannels = header.numChannels; int numChannels = wavFormat.numChannels;
// Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update // Validate the WAV format. This calculation is defined in "Microsoft Multimedia Standards
// Update
// - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI
// ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter.
int expectedFramesPerBlock = int expectedFramesPerBlock =
(((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; (((wavFormat.blockSize - (4 * numChannels)) * 8)
/ (wavFormat.bitsPerSample * numChannels))
+ 1;
if (framesPerBlock != expectedFramesPerBlock) { if (framesPerBlock != expectedFramesPerBlock) {
throw ParserException.createForMalformedContainer( throw ParserException.createForMalformedContainer(
"Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock, "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock,
@ -412,22 +428,22 @@ public final class WavExtractor implements Extractor {
// Calculate the number of blocks we'll need to decode to obtain an output sample of the // Calculate the number of blocks we'll need to decode to obtain an output sample of the
// target sample size, and allocate suitably sized buffers for input and decoded data. // target sample size, and allocate suitably sized buffers for input and decoded data.
int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock);
inputData = new byte[maxBlocksToDecode * header.blockSize]; inputData = new byte[maxBlocksToDecode * wavFormat.blockSize];
decodedData = decodedData =
new ParsableByteArray( new ParsableByteArray(
maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels));
// Create the format. We calculate the bitrate of the data before decoding, since this is the // Create the format. We calculate the bitrate of the data before decoding, since this is the
// bitrate of the stream itself. // bitrate of the stream itself.
int constantBitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; int constantBitrate = wavFormat.frameRateHz * wavFormat.blockSize * 8 / framesPerBlock;
format = format =
new Format.Builder() new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW) .setSampleMimeType(MimeTypes.AUDIO_RAW)
.setAverageBitrate(constantBitrate) .setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate) .setPeakBitrate(constantBitrate)
.setMaxInputSize(numOutputFramesToBytes(targetSampleSizeFrames, numChannels)) .setMaxInputSize(numOutputFramesToBytes(targetSampleSizeFrames, numChannels))
.setChannelCount(header.numChannels) .setChannelCount(wavFormat.numChannels)
.setSampleRate(header.frameRateHz) .setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(C.ENCODING_PCM_16BIT) .setPcmEncoding(C.ENCODING_PCM_16BIT)
.build(); .build();
} }
@ -443,7 +459,7 @@ public final class WavExtractor implements Extractor {
@Override @Override
public void init(int dataStartPosition, long dataEndPosition) { public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap( extractorOutput.seekMap(
new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); new WavSeekMap(wavFormat, framesPerBlock, dataStartPosition, dataEndPosition));
trackOutput.format(format); trackOutput.format(format);
} }
@ -455,7 +471,7 @@ public final class WavExtractor implements Extractor {
targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes);
// Calculate the whole number of blocks that we need to decode to obtain this many frames. // Calculate the whole number of blocks that we need to decode to obtain this many frames.
int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock);
int targetReadBytes = blocksToDecode * header.blockSize; int targetReadBytes = blocksToDecode * wavFormat.blockSize;
// Read input data until we've reached the target number of blocks, or the end of the data. // Read input data until we've reached the target number of blocks, or the end of the data.
boolean endOfSampleData = bytesLeft == 0; boolean endOfSampleData = bytesLeft == 0;
@ -469,11 +485,11 @@ public final class WavExtractor implements Extractor {
} }
} }
int pendingBlockCount = pendingInputBytes / header.blockSize; int pendingBlockCount = pendingInputBytes / wavFormat.blockSize;
if (pendingBlockCount > 0) { if (pendingBlockCount > 0) {
// We have at least one whole block to decode. // We have at least one whole block to decode.
decode(inputData, pendingBlockCount, decodedData); decode(inputData, pendingBlockCount, decodedData);
pendingInputBytes -= pendingBlockCount * header.blockSize; pendingInputBytes -= pendingBlockCount * wavFormat.blockSize;
// Write all of the decoded data to the track output. // Write all of the decoded data to the track output.
int decodedDataSize = decodedData.limit(); int decodedDataSize = decodedData.limit();
@ -501,7 +517,8 @@ public final class WavExtractor implements Extractor {
private void writeSampleMetadata(int sampleFrames) { private void writeSampleMetadata(int sampleFrames) {
long timeUs = long timeUs =
startTimeUs startTimeUs
+ Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
int size = numOutputFramesToBytes(sampleFrames); int size = numOutputFramesToBytes(sampleFrames);
int offset = pendingOutputBytes - size; int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata( trackOutput.sampleMetadata(
@ -519,7 +536,7 @@ public final class WavExtractor implements Extractor {
*/ */
private void decode(byte[] input, int blockCount, ParsableByteArray output) { private void decode(byte[] input, int blockCount, ParsableByteArray output) {
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) {
for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { for (int channelIndex = 0; channelIndex < wavFormat.numChannels; channelIndex++) {
decodeBlockForChannel(input, blockIndex, channelIndex, output.getData()); decodeBlockForChannel(input, blockIndex, channelIndex, output.getData());
} }
} }
@ -530,8 +547,8 @@ public final class WavExtractor implements Extractor {
private void decodeBlockForChannel( private void decodeBlockForChannel(
byte[] input, int blockIndex, int channelIndex, byte[] output) { byte[] input, int blockIndex, int channelIndex, byte[] output) {
int blockSize = header.blockSize; int blockSize = wavFormat.blockSize;
int numChannels = header.numChannels; int numChannels = wavFormat.numChannels;
// The input data consists for a four byte header [Ci] for each of the N channels, followed // The input data consists for a four byte header [Ci] for each of the N channels, followed
// by interleaved data segments [Ci-DATAj], each of which are four bytes long. // by interleaved data segments [Ci-DATAj], each of which are four bytes long.
@ -592,11 +609,11 @@ public final class WavExtractor implements Extractor {
} }
private int numOutputBytesToFrames(int bytes) { private int numOutputBytesToFrames(int bytes) {
return bytes / (2 * header.numChannels); return bytes / (2 * wavFormat.numChannels);
} }
private int numOutputFramesToBytes(int frames) { private int numOutputFramesToBytes(int frames) {
return numOutputFramesToBytes(frames, header.numChannels); return numOutputFramesToBytes(frames, wavFormat.numChannels);
} }
private static int numOutputFramesToBytes(int frames, int numChannels) { private static int numOutputFramesToBytes(int frames, int numChannels) {

View File

@ -15,8 +15,8 @@
*/ */
package androidx.media3.extractor.wav; package androidx.media3.extractor.wav;
/** Header for a WAV file. */ /** Format information for a WAV file. */
/* package */ final class WavHeader { /* package */ final class WavFormat {
/** /**
* The format type. Standard format types are the "WAVE form Registration Number" constants * The format type. Standard format types are the "WAVE form Registration Number" constants
@ -33,10 +33,10 @@ package androidx.media3.extractor.wav;
public final int blockSize; public final int blockSize;
/** Bits per sample for a single channel. */ /** Bits per sample for a single channel. */
public final int bitsPerSample; public final int bitsPerSample;
/** Extra data appended to the format chunk of the header. */ /** Extra data appended to the format chunk. */
public final byte[] extraData; public final byte[] extraData;
public WavHeader( public WavFormat(
int formatType, int formatType,
int numChannels, int numChannels,
int frameRateHz, int frameRateHz,

View File

@ -16,7 +16,6 @@
package androidx.media3.extractor.wav; package androidx.media3.extractor.wav;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
@ -27,45 +26,56 @@ import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.WavUtil; import androidx.media3.extractor.WavUtil;
import java.io.IOException; import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ /** Reads a WAV header from an input stream; supports resuming from input failures. */
/* package */ final class WavHeaderReader { /* package */ final class WavHeaderReader {
private static final String TAG = "WavHeaderReader"; private static final String TAG = "WavHeaderReader";
/** /**
* Peeks and returns a {@code WavHeader}. * Returns whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE
* tag.
* *
* @param input Input stream to peek the WAV header from. * @param input The input stream to peek from. The position should point to the start of the
* @throws ParserException If the input file is an incorrect RIFF WAV. * stream.
* @return Whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE
* tag.
* @throws IOException If peeking from the input fails. * @throws IOException If peeking from the input fails.
* @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a
* supported WAV format.
*/ */
@Nullable public static boolean checkFileType(ExtractorInput input) throws IOException {
public static WavHeader peek(ExtractorInput input) throws IOException { ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
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. // Attempt to read the RIFF chunk.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
if (chunkHeader.id != WavUtil.RIFF_FOURCC) { if (chunkHeader.id != WavUtil.RIFF_FOURCC) {
return null; return false;
} }
input.peekFully(scratch.getData(), 0, 4); input.peekFully(scratch.getData(), 0, 4);
scratch.setPosition(0); scratch.setPosition(0);
int riffFormat = scratch.readInt(); int formType = scratch.readInt();
if (riffFormat != WavUtil.WAVE_FOURCC) { if (formType != WavUtil.WAVE_FOURCC) {
Log.e(TAG, "Unsupported RIFF format: " + riffFormat); Log.e(TAG, "Unsupported form type: " + formType);
return null; return false;
} }
return true;
}
/**
* Reads and returns a {@code WavFormat}.
*
* @param input Input stream to read the WAV format from. The position should point to the byte
* following the WAVE tag.
* @throws IOException If reading from the input fails.
* @return A new {@code WavFormat} read from {@code input}.
*/
public static WavFormat readFormat(ExtractorInput input) throws IOException {
// Allocate a scratch buffer large enough to store the format chunk.
ParsableByteArray scratch = new ParsableByteArray(16);
// Skip chunks until we find the format chunk. // Skip chunks until we find the format chunk.
chunkHeader = ChunkHeader.peek(input, scratch); ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != WavUtil.FMT_FOURCC) { while (chunkHeader.id != WavUtil.FMT_FOURCC) {
input.advancePeekPosition((int) chunkHeader.size); input.skipFully(ChunkHeader.SIZE_IN_BYTES + (int) chunkHeader.size);
chunkHeader = ChunkHeader.peek(input, scratch); chunkHeader = ChunkHeader.peek(input, scratch);
} }
@ -88,7 +98,8 @@ import java.io.IOException;
extraData = Util.EMPTY_BYTE_ARRAY; extraData = Util.EMPTY_BYTE_ARRAY;
} }
return new WavHeader( input.skipFully((int) (input.getPeekPosition() - input.getPosition()));
return new WavFormat(
audioFormatType, audioFormatType,
numChannels, numChannels,
frameRateHz, frameRateHz,
@ -109,8 +120,6 @@ import java.io.IOException;
* @throws IOException If reading from the input fails. * @throws IOException If reading from the input fails.
*/ */
public static Pair<Long, Long> skipToSampleData(ExtractorInput input) throws IOException { public static Pair<Long, Long> skipToSampleData(ExtractorInput input) throws IOException {
Assertions.checkNotNull(input);
// Make sure the peek position is set to the read position before we peek the first header. // Make sure the peek position is set to the read position before we peek the first header.
input.resetPeekPosition(); input.resetPeekPosition();

View File

@ -22,18 +22,18 @@ import androidx.media3.extractor.SeekPoint;
/* package */ final class WavSeekMap implements SeekMap { /* package */ final class WavSeekMap implements SeekMap {
private final WavHeader wavHeader; private final WavFormat wavFormat;
private final int framesPerBlock; private final int framesPerBlock;
private final long firstBlockPosition; private final long firstBlockPosition;
private final long blockCount; private final long blockCount;
private final long durationUs; private final long durationUs;
public WavSeekMap( public WavSeekMap(
WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) { WavFormat wavFormat, int framesPerBlock, long dataStartPosition, long dataEndPosition) {
this.wavHeader = wavHeader; this.wavFormat = wavFormat;
this.framesPerBlock = framesPerBlock; this.framesPerBlock = framesPerBlock;
this.firstBlockPosition = dataStartPosition; this.firstBlockPosition = dataStartPosition;
this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize; this.blockCount = (dataEndPosition - dataStartPosition) / wavFormat.blockSize;
durationUs = blockIndexToTimeUs(blockCount); durationUs = blockIndexToTimeUs(blockCount);
} }
@ -50,17 +50,17 @@ import androidx.media3.extractor.SeekPoint;
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
// Calculate the containing block index, constraining to valid indices. // Calculate the containing block index, constraining to valid indices.
long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); long blockIndex = (timeUs * wavFormat.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock);
blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1);
long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); long seekPosition = firstBlockPosition + (blockIndex * wavFormat.blockSize);
long seekTimeUs = blockIndexToTimeUs(blockIndex); long seekTimeUs = blockIndexToTimeUs(blockIndex);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) {
return new SeekPoints(seekPoint); return new SeekPoints(seekPoint);
} else { } else {
long secondBlockIndex = blockIndex + 1; long secondBlockIndex = blockIndex + 1;
long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavFormat.blockSize);
long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex);
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint); return new SeekPoints(seekPoint, secondSeekPoint);
@ -69,6 +69,6 @@ import androidx.media3.extractor.SeekPoint;
private long blockIndexToTimeUs(long blockIndex) { private long blockIndexToTimeUs(long blockIndex) {
return Util.scaleLargeTimestamp( return Util.scaleLargeTimestamp(
blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz); blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
} }
} }