Skip to content

Commit

Permalink
WavExtractor: split header reading state into 2 states
Browse files Browse the repository at this point in the history
This refactoring is the basis to support RF64 (see
Issue: #9543).

#minor-release

PiperOrigin-RevId: 407301056
  • Loading branch information
kim-vde authored and tonihei committed Nov 3, 2021
1 parent ac66487 commit 9e247d2
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,18 @@ public final class WavExtractor implements Extractor {
@Documented
@Retention(RetentionPolicy.SOURCE)
@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 static final int STATE_READING_HEADER = 0;
private static final int STATE_SKIPPING_TO_SAMPLE_DATA = 1;
private static final int STATE_READING_SAMPLE_DATA = 2;
private static final int STATE_READING_FILE_TYPE = 0;
private static final int STATE_READING_FORMAT = 1;
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 TrackOutput trackOutput;
Expand All @@ -76,14 +82,14 @@ public final class WavExtractor implements Extractor {
private long dataEndPosition;

public WavExtractor() {
state = STATE_READING_HEADER;
state = STATE_READING_FILE_TYPE;
dataStartPosition = C.POSITION_UNSET;
dataEndPosition = C.POSITION_UNSET;
}

@Override
public boolean sniff(ExtractorInput input) throws IOException {
return WavHeaderReader.peek(input) != null;
return WavHeaderReader.checkFileType(input);
}

@Override
Expand All @@ -95,7 +101,7 @@ public void init(ExtractorOutput output) {

@Override
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) {
outputWriter.reset(timeUs);
}
Expand All @@ -111,8 +117,11 @@ public void release() {
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
assertInitialized();
switch (state) {
case STATE_READING_HEADER:
readHeader(input);
case STATE_READING_FILE_TYPE:
readFileType(input);
return Extractor.RESULT_CONTINUE;
case STATE_READING_FORMAT:
readFormat(input);
return Extractor.RESULT_CONTINUE;
case STATE_SKIPPING_TO_SAMPLE_DATA:
skipToSampleData(input);
Expand All @@ -130,50 +139,54 @@ private void assertInitialized() {
Util.castNonNull(extractorOutput);
}

@RequiresNonNull({"extractorOutput", "trackOutput"})
private void readHeader(ExtractorInput input) throws IOException {
private void readFileType(ExtractorInput input) throws IOException {
Assertions.checkState(input.getPosition() == 0);
if (dataStartPosition != C.POSITION_UNSET) {
input.skipFully(dataStartPosition);
state = STATE_READING_SAMPLE_DATA;
return;
}
WavHeader header = WavHeaderReader.peek(input);
if (header == null) {
if (!WavHeaderReader.checkFileType(input)) {
// Should only happen if the media wasn't sniffed.
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()));
state = STATE_READING_FORMAT;
}

if (header.formatType == WavUtil.TYPE_IMA_ADPCM) {
outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header);
} else if (header.formatType == WavUtil.TYPE_ALAW) {
@RequiresNonNull({"extractorOutput", "trackOutput"})
private void readFormat(ExtractorInput input) throws IOException {
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 =
new PassthroughOutputWriter(
extractorOutput,
trackOutput,
header,
wavFormat,
MimeTypes.AUDIO_ALAW,
/* pcmEncoding= */ Format.NO_VALUE);
} else if (header.formatType == WavUtil.TYPE_MLAW) {
} else if (wavFormat.formatType == WavUtil.TYPE_MLAW) {
outputWriter =
new PassthroughOutputWriter(
extractorOutput,
trackOutput,
header,
wavFormat,
MimeTypes.AUDIO_MLAW,
/* pcmEncoding= */ Format.NO_VALUE);
} else {
@C.PcmEncoding
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample);
int pcmEncoding =
WavUtil.getPcmEncodingForType(wavFormat.formatType, wavFormat.bitsPerSample);
if (pcmEncoding == C.ENCODING_INVALID) {
throw ParserException.createForUnsupportedContainerFeature(
"Unsupported WAV format type: " + header.formatType);
"Unsupported WAV format type: " + wavFormat.formatType);
}
outputWriter =
new PassthroughOutputWriter(
extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding);
extractorOutput, trackOutput, wavFormat, MimeTypes.AUDIO_RAW, pcmEncoding);
}
state = STATE_SKIPPING_TO_SAMPLE_DATA;
}
Expand Down Expand Up @@ -234,7 +247,7 @@ private static final class PassthroughOutputWriter implements OutputWriter {

private final ExtractorOutput extractorOutput;
private final TrackOutput trackOutput;
private final WavHeader header;
private final WavFormat wavFormat;
private final Format format;
/** The target size of each output sample, in bytes. */
private final int targetSampleSizeBytes;
Expand All @@ -256,33 +269,33 @@ private static final class PassthroughOutputWriter implements OutputWriter {
public PassthroughOutputWriter(
ExtractorOutput extractorOutput,
TrackOutput trackOutput,
WavHeader header,
WavFormat wavFormat,
String mimeType,
@C.PcmEncoding int pcmEncoding)
throws ParserException {
this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput;
this.header = header;
this.wavFormat = wavFormat;

int bytesPerFrame = header.numChannels * header.bitsPerSample / 8;
// Validate the header. Blocks are expected to correspond to single frames.
if (header.blockSize != bytesPerFrame) {
int bytesPerFrame = wavFormat.numChannels * wavFormat.bitsPerSample / 8;
// Validate the WAV format. Blocks are expected to correspond to single frames.
if (wavFormat.blockSize != bytesPerFrame) {
throw ParserException.createForMalformedContainer(
"Expected block size: " + bytesPerFrame + "; got: " + header.blockSize,
"Expected block size: " + bytesPerFrame + "; got: " + wavFormat.blockSize,
/* cause= */ null);
}

int constantBitrate = header.frameRateHz * bytesPerFrame * 8;
int constantBitrate = wavFormat.frameRateHz * bytesPerFrame * 8;
targetSampleSizeBytes =
max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
max(bytesPerFrame, wavFormat.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
format =
new Format.Builder()
.setSampleMimeType(mimeType)
.setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate)
.setMaxInputSize(targetSampleSizeBytes)
.setChannelCount(header.numChannels)
.setSampleRate(header.frameRateHz)
.setChannelCount(wavFormat.numChannels)
.setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(pcmEncoding)
.build();
}
Expand All @@ -297,7 +310,7 @@ public void reset(long timeUs) {
@Override
public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap(
new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
new WavSeekMap(wavFormat, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
trackOutput.format(format);
}

Expand All @@ -318,13 +331,13 @@ public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOExcepti
// 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
// stream ended unexpectedly.
int bytesPerFrame = header.blockSize;
int bytesPerFrame = wavFormat.blockSize;
int pendingFrames = pendingOutputBytes / bytesPerFrame;
if (pendingFrames > 0) {
long timeUs =
startTimeUs
+ Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
outputFrameCount, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
int size = pendingFrames * bytesPerFrame;
int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata(
Expand Down Expand Up @@ -354,7 +367,7 @@ private static final class ImaAdPcmOutputWriter implements OutputWriter {

private final ExtractorOutput extractorOutput;
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. */
private final int framesPerBlock;
Expand Down Expand Up @@ -384,23 +397,26 @@ private static final class ImaAdPcmOutputWriter implements OutputWriter {
private long outputFrameCount;

public ImaAdPcmOutputWriter(
ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header)
ExtractorOutput extractorOutput, TrackOutput trackOutput, WavFormat wavFormat)
throws ParserException {
this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput;
this.header = header;
targetSampleSizeFrames = max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND);
this.wavFormat = wavFormat;
targetSampleSizeFrames = max(1, wavFormat.frameRateHz / TARGET_SAMPLES_PER_SECOND);

ParsableByteArray scratch = new ParsableByteArray(header.extraData);
ParsableByteArray scratch = new ParsableByteArray(wavFormat.extraData);
scratch.readLittleEndianUnsignedShort();
framesPerBlock = scratch.readLittleEndianUnsignedShort();

int numChannels = header.numChannels;
// Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update
int numChannels = wavFormat.numChannels;
// 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
// ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter.
int expectedFramesPerBlock =
(((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1;
(((wavFormat.blockSize - (4 * numChannels)) * 8)
/ (wavFormat.bitsPerSample * numChannels))
+ 1;
if (framesPerBlock != expectedFramesPerBlock) {
throw ParserException.createForMalformedContainer(
"Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock,
Expand All @@ -410,22 +426,22 @@ public ImaAdPcmOutputWriter(
// 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.
int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock);
inputData = new byte[maxBlocksToDecode * header.blockSize];
inputData = new byte[maxBlocksToDecode * wavFormat.blockSize];
decodedData =
new ParsableByteArray(
maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels));

// Create the format. We calculate the bitrate of the data before decoding, since this is the
// bitrate of the stream itself.
int constantBitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock;
int constantBitrate = wavFormat.frameRateHz * wavFormat.blockSize * 8 / framesPerBlock;
format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate)
.setMaxInputSize(numOutputFramesToBytes(targetSampleSizeFrames, numChannels))
.setChannelCount(header.numChannels)
.setSampleRate(header.frameRateHz)
.setChannelCount(wavFormat.numChannels)
.setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(C.ENCODING_PCM_16BIT)
.build();
}
Expand All @@ -441,7 +457,7 @@ public void reset(long timeUs) {
@Override
public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap(
new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition));
new WavSeekMap(wavFormat, framesPerBlock, dataStartPosition, dataEndPosition));
trackOutput.format(format);
}

Expand All @@ -453,7 +469,7 @@ public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOExcepti
targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes);
// Calculate the whole number of blocks that we need to decode to obtain this many frames.
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.
boolean endOfSampleData = bytesLeft == 0;
Expand All @@ -467,11 +483,11 @@ public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOExcepti
}
}

int pendingBlockCount = pendingInputBytes / header.blockSize;
int pendingBlockCount = pendingInputBytes / wavFormat.blockSize;
if (pendingBlockCount > 0) {
// We have at least one whole block to decode.
decode(inputData, pendingBlockCount, decodedData);
pendingInputBytes -= pendingBlockCount * header.blockSize;
pendingInputBytes -= pendingBlockCount * wavFormat.blockSize;

// Write all of the decoded data to the track output.
int decodedDataSize = decodedData.limit();
Expand Down Expand Up @@ -499,7 +515,8 @@ public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOExcepti
private void writeSampleMetadata(int sampleFrames) {
long timeUs =
startTimeUs
+ Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
+ Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
int size = numOutputFramesToBytes(sampleFrames);
int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata(
Expand All @@ -517,7 +534,7 @@ private void writeSampleMetadata(int sampleFrames) {
*/
private void decode(byte[] input, int blockCount, ParsableByteArray output) {
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());
}
}
Expand All @@ -528,8 +545,8 @@ private void decode(byte[] input, int blockCount, ParsableByteArray output) {

private void decodeBlockForChannel(
byte[] input, int blockIndex, int channelIndex, byte[] output) {
int blockSize = header.blockSize;
int numChannels = header.numChannels;
int blockSize = wavFormat.blockSize;
int numChannels = wavFormat.numChannels;

// 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.
Expand Down Expand Up @@ -590,11 +607,11 @@ private void decodeBlockForChannel(
}

private int numOutputBytesToFrames(int bytes) {
return bytes / (2 * header.numChannels);
return bytes / (2 * wavFormat.numChannels);
}

private int numOutputFramesToBytes(int frames) {
return numOutputFramesToBytes(frames, header.numChannels);
return numOutputFramesToBytes(frames, wavFormat.numChannels);
}

private static int numOutputFramesToBytes(int frames, int numChannels) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.wav;

/** Header for a WAV file. */
/* package */ final class WavHeader {
/** Format information for a WAV file. */
/* package */ final class WavFormat {

/**
* The format type. Standard format types are the "WAVE form Registration Number" constants
Expand All @@ -33,10 +33,10 @@
public final int blockSize;
/** Bits per sample for a single channel. */
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 WavHeader(
public WavFormat(
int formatType,
int numChannels,
int frameRateHz,
Expand Down
Loading

0 comments on commit 9e247d2

Please sign in to comment.