Remove assumption that WAV files only contain PCM encoded data
- WavHeader is now immutable and contains only values parsed out of the WAVE FMT chunk. It no longer contains a C.PcmEncoding encoding, or mutable data bounds. - WavHeaderReader now parses the WAVE header chunks without any additional logic (e.g. validating the block alignment value, which is format type dependent). - The SeekMap part of WavHeader is split out into WavSeekMap. PiperOrigin-RevId: 285232498
This commit is contained in:
parent
1144926380
commit
d62dc9dcfb
@ -176,7 +176,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor {
|
|||||||
// Write the rest of the header as little endian data.
|
// Write the rest of the header as little endian data.
|
||||||
scratchByteBuffer.clear();
|
scratchByteBuffer.clear();
|
||||||
scratchByteBuffer.putInt(16);
|
scratchByteBuffer.putInt(16);
|
||||||
scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding));
|
scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding));
|
||||||
scratchByteBuffer.putShort((short) channelCount);
|
scratchByteBuffer.putShort((short) channelCount);
|
||||||
scratchByteBuffer.putInt(sampleRateHz);
|
scratchByteBuffer.putInt(sampleRateHz);
|
||||||
int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
|
int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
|
||||||
|
@ -42,9 +42,16 @@ public final class WavUtil {
|
|||||||
/** WAVE type value for extended WAVE format. */
|
/** WAVE type value for extended WAVE format. */
|
||||||
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
|
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
|
||||||
|
|
||||||
/** Returns the WAVE type value for the given {@code encoding}. */
|
/**
|
||||||
public static int getTypeForEncoding(@C.PcmEncoding int encoding) {
|
* Returns the WAVE format type value for the given {@link C.PcmEncoding}.
|
||||||
switch (encoding) {
|
*
|
||||||
|
* @param pcmEncoding The {@link C.PcmEncoding} value.
|
||||||
|
* @return The corresponding WAVE format type.
|
||||||
|
* @throws IllegalArgumentException If {@code pcmEncoding} is not a {@link C.PcmEncoding}, or if
|
||||||
|
* it's {@link C#ENCODING_INVALID} or {@link Format#NO_VALUE}.
|
||||||
|
*/
|
||||||
|
public static int getTypeForPcmEncoding(@C.PcmEncoding int pcmEncoding) {
|
||||||
|
switch (pcmEncoding) {
|
||||||
case C.ENCODING_PCM_8BIT:
|
case C.ENCODING_PCM_8BIT:
|
||||||
case C.ENCODING_PCM_16BIT:
|
case C.ENCODING_PCM_16BIT:
|
||||||
case C.ENCODING_PCM_24BIT:
|
case C.ENCODING_PCM_24BIT:
|
||||||
@ -63,8 +70,11 @@ public final class WavUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the PCM encoding for the given WAVE {@code type} value. */
|
/**
|
||||||
public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) {
|
* Returns the {@link C.PcmEncoding} for the given WAVE format type value, or {@link
|
||||||
|
* C#ENCODING_INVALID} if the type is not a known PCM type.
|
||||||
|
*/
|
||||||
|
public static @C.PcmEncoding int getPcmEncodingForType(int type, int bitsPerSample) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TYPE_PCM:
|
case TYPE_PCM:
|
||||||
case TYPE_WAVE_FORMAT_EXTENSIBLE:
|
case TYPE_WAVE_FORMAT_EXTENSIBLE:
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.wav;
|
package com.google.android.exoplayer2.extractor.wav;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.audio.WavUtil;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
@ -41,10 +43,17 @@ public final class WavExtractor implements Extractor {
|
|||||||
|
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
private TrackOutput trackOutput;
|
private TrackOutput trackOutput;
|
||||||
private WavHeader wavHeader;
|
private WavHeader header;
|
||||||
private int bytesPerFrame;
|
private WavSeekMap seekMap;
|
||||||
|
private int dataStartPosition;
|
||||||
|
private long dataEndPosition;
|
||||||
private int pendingBytes;
|
private int pendingBytes;
|
||||||
|
|
||||||
|
public WavExtractor() {
|
||||||
|
dataStartPosition = C.POSITION_UNSET;
|
||||||
|
dataEndPosition = C.POSITION_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
return WavHeaderReader.peek(input) != null;
|
return WavHeaderReader.peek(input) != null;
|
||||||
@ -54,7 +63,7 @@ public final class WavExtractor implements Extractor {
|
|||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
extractorOutput = output;
|
extractorOutput = output;
|
||||||
trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
|
trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
|
||||||
wavHeader = null;
|
header = null;
|
||||||
output.endTracks();
|
output.endTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,29 +80,58 @@ public final class WavExtractor implements Extractor {
|
|||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
if (wavHeader == null) {
|
if (header == null) {
|
||||||
wavHeader = WavHeaderReader.peek(input);
|
header = WavHeaderReader.peek(input);
|
||||||
if (wavHeader == null) {
|
if (header == null) {
|
||||||
// Should only happen if the media wasn't sniffed.
|
// Should only happen if the media wasn't sniffed.
|
||||||
throw new ParserException("Unsupported or unrecognized wav header.");
|
throw new ParserException("Unsupported or unrecognized wav header.");
|
||||||
}
|
}
|
||||||
Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null,
|
|
||||||
wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(),
|
@C.PcmEncoding
|
||||||
wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null);
|
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample);
|
||||||
|
if (pcmEncoding == C.ENCODING_INVALID) {
|
||||||
|
throw new ParserException("Unsupported WAV format type: " + header.formatType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PCM specific header validation.
|
||||||
|
int expectedBytesPerFrame = header.numChannels * header.bitsPerSample / 8;
|
||||||
|
if (header.blockAlign != expectedBytesPerFrame) {
|
||||||
|
throw new ParserException(
|
||||||
|
"Unexpected bytes per frame: "
|
||||||
|
+ header.blockAlign
|
||||||
|
+ "; expected: "
|
||||||
|
+ expectedBytesPerFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
Format format =
|
||||||
|
Format.createAudioSampleFormat(
|
||||||
|
/* id= */ null,
|
||||||
|
MimeTypes.AUDIO_RAW,
|
||||||
|
/* codecs= */ null,
|
||||||
|
/* bitrate= */ header.averageBytesPerSecond * 8,
|
||||||
|
MAX_INPUT_SIZE,
|
||||||
|
header.numChannels,
|
||||||
|
header.sampleRateHz,
|
||||||
|
pcmEncoding,
|
||||||
|
/* initializationData= */ null,
|
||||||
|
/* drmInitData= */ null,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
/* language= */ null);
|
||||||
trackOutput.format(format);
|
trackOutput.format(format);
|
||||||
bytesPerFrame = wavHeader.getBytesPerFrame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wavHeader.hasDataBounds()) {
|
if (dataStartPosition == C.POSITION_UNSET) {
|
||||||
WavHeaderReader.skipToData(input, wavHeader);
|
Pair<Long, Long> dataBounds = WavHeaderReader.skipToData(input);
|
||||||
extractorOutput.seekMap(wavHeader);
|
dataStartPosition = dataBounds.first.intValue();
|
||||||
|
dataEndPosition = dataBounds.second;
|
||||||
|
seekMap =
|
||||||
|
new WavSeekMap(header, /* samplesPerBlock= */ 1, dataStartPosition, dataEndPosition);
|
||||||
|
extractorOutput.seekMap(seekMap);
|
||||||
} else if (input.getPosition() == 0) {
|
} else if (input.getPosition() == 0) {
|
||||||
input.skipFully(wavHeader.getDataStartPosition());
|
input.skipFully(dataStartPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
long dataEndPosition = wavHeader.getDataEndPosition();
|
|
||||||
Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
|
Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
|
||||||
|
|
||||||
long bytesLeft = dataEndPosition - input.getPosition();
|
long bytesLeft = dataEndPosition - input.getPosition();
|
||||||
if (bytesLeft <= 0) {
|
if (bytesLeft <= 0) {
|
||||||
return Extractor.RESULT_END_OF_INPUT;
|
return Extractor.RESULT_END_OF_INPUT;
|
||||||
@ -105,16 +143,17 @@ public final class WavExtractor implements Extractor {
|
|||||||
pendingBytes += bytesAppended;
|
pendingBytes += bytesAppended;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Samples must consist of a whole number of frames.
|
// For PCM blockAlign is the frame size, and samples must consist of a whole number of frames.
|
||||||
|
int bytesPerFrame = header.blockAlign;
|
||||||
int pendingFrames = pendingBytes / bytesPerFrame;
|
int pendingFrames = pendingBytes / bytesPerFrame;
|
||||||
if (pendingFrames > 0) {
|
if (pendingFrames > 0) {
|
||||||
long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes);
|
long timeUs = seekMap.getTimeUs(input.getPosition() - pendingBytes);
|
||||||
int size = pendingFrames * bytesPerFrame;
|
int size = pendingFrames * bytesPerFrame;
|
||||||
pendingBytes -= size;
|
pendingBytes -= size;
|
||||||
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null);
|
trackOutput.sampleMetadata(
|
||||||
|
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, /* encryptionData= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,160 +15,41 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.wav;
|
package com.google.android.exoplayer2.extractor.wav;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
|
|
||||||
/** Header for a WAV file. */
|
/** Header for a WAV file. */
|
||||||
/* package */ final class WavHeader implements SeekMap {
|
/* package */ final class WavHeader {
|
||||||
|
|
||||||
/** Number of audio channels. */
|
/**
|
||||||
private final int numChannels;
|
* The format type. Standard format types are the "WAVE form Registration Number" constants
|
||||||
/** Sample rate in Hertz. */
|
* defined in RFC 2361 Appendix A.
|
||||||
private final int sampleRateHz;
|
*/
|
||||||
/** Average bytes per second for the sample data. */
|
public final int formatType;
|
||||||
private final int averageBytesPerSecond;
|
/** The number of channels. */
|
||||||
/** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */
|
public final int numChannels;
|
||||||
private final int blockAlignment;
|
/** The sample rate in Hertz. */
|
||||||
/** Bits per sample for the audio data. */
|
public final int sampleRateHz;
|
||||||
private final int bitsPerSample;
|
/** The average bytes per second for the sample data. */
|
||||||
/** Number of samples in each block. */
|
public final int averageBytesPerSecond;
|
||||||
private final int samplesPerBlock;
|
/** The block size in bytes. */
|
||||||
/** The PCM encoding. */
|
public final int blockAlign;
|
||||||
@C.PcmEncoding private final int encoding;
|
/** Bits per sample for a single channel. */
|
||||||
|
public final int bitsPerSample;
|
||||||
/** Position of the start of the sample data, in bytes. */
|
/** Extra data appended to the format chunk of the header. */
|
||||||
private int dataStartPosition;
|
public final byte[] extraData;
|
||||||
/** Position of the end of the sample data (exclusive), in bytes. */
|
|
||||||
private long dataEndPosition;
|
|
||||||
|
|
||||||
public WavHeader(
|
public WavHeader(
|
||||||
|
int formatType,
|
||||||
int numChannels,
|
int numChannels,
|
||||||
int sampleRateHz,
|
int sampleRateHz,
|
||||||
int averageBytesPerSecond,
|
int averageBytesPerSecond,
|
||||||
int blockAlignment,
|
int bytesPerFrame,
|
||||||
int bitsPerSample,
|
int bitsPerSample,
|
||||||
int samplesPerBlock,
|
byte[] extraData) {
|
||||||
@C.PcmEncoding int encoding) {
|
this.formatType = formatType;
|
||||||
this.numChannels = numChannels;
|
this.numChannels = numChannels;
|
||||||
this.sampleRateHz = sampleRateHz;
|
this.sampleRateHz = sampleRateHz;
|
||||||
this.averageBytesPerSecond = averageBytesPerSecond;
|
this.averageBytesPerSecond = averageBytesPerSecond;
|
||||||
this.blockAlignment = blockAlignment;
|
this.blockAlign = bytesPerFrame;
|
||||||
this.bitsPerSample = bitsPerSample;
|
this.bitsPerSample = bitsPerSample;
|
||||||
this.samplesPerBlock = samplesPerBlock;
|
this.extraData = extraData;
|
||||||
this.encoding = encoding;
|
|
||||||
dataStartPosition = C.POSITION_UNSET;
|
|
||||||
dataEndPosition = C.POSITION_UNSET;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data bounds.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the data start position and size in bytes of sample data in this WAV.
|
|
||||||
*
|
|
||||||
* @param dataStartPosition The position of the start of the sample data, in bytes.
|
|
||||||
* @param dataEndPosition The position of the end of the sample data (exclusive), in bytes.
|
|
||||||
*/
|
|
||||||
public void setDataBounds(int dataStartPosition, long dataEndPosition) {
|
|
||||||
this.dataStartPosition = dataStartPosition;
|
|
||||||
this.dataEndPosition = dataEndPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if
|
|
||||||
* the data bounds have not been set.
|
|
||||||
*/
|
|
||||||
public int getDataStartPosition() {
|
|
||||||
return dataStartPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the position of the end of the sample data (exclusive), in bytes, or {@link
|
|
||||||
* C#POSITION_UNSET} if the data bounds have not been set.
|
|
||||||
*/
|
|
||||||
public long getDataEndPosition() {
|
|
||||||
return dataEndPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether the data start position and size have been set. */
|
|
||||||
public boolean hasDataBounds() {
|
|
||||||
return dataStartPosition != C.POSITION_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeekMap implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSeekable() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getDurationUs() {
|
|
||||||
long numBlocks = (dataEndPosition - dataStartPosition) / blockAlignment;
|
|
||||||
return numBlocks * samplesPerBlock * C.MICROS_PER_SECOND / sampleRateHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SeekPoints getSeekPoints(long timeUs) {
|
|
||||||
long dataSize = dataEndPosition - dataStartPosition;
|
|
||||||
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
|
||||||
// Constrain to nearest preceding frame offset.
|
|
||||||
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
|
|
||||||
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
|
|
||||||
long seekPosition = dataStartPosition + positionOffset;
|
|
||||||
long seekTimeUs = getTimeUs(seekPosition);
|
|
||||||
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
|
|
||||||
if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) {
|
|
||||||
return new SeekPoints(seekPoint);
|
|
||||||
} else {
|
|
||||||
long secondSeekPosition = seekPosition + blockAlignment;
|
|
||||||
long secondSeekTimeUs = getTimeUs(secondSeekPosition);
|
|
||||||
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
|
|
||||||
return new SeekPoints(seekPoint, secondSeekPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Misc getters.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the time in microseconds for the given position in bytes.
|
|
||||||
*
|
|
||||||
* @param position The position in bytes.
|
|
||||||
*/
|
|
||||||
public long getTimeUs(long position) {
|
|
||||||
long positionOffset = Math.max(0, position - dataStartPosition);
|
|
||||||
return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the bytes per frame of this WAV. */
|
|
||||||
public int getBytesPerFrame() {
|
|
||||||
return blockAlignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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 number of samples in each block. */
|
|
||||||
public int getSamplesPerBlock() {
|
|
||||||
return samplesPerBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the PCM encoding. **/
|
|
||||||
public @C.PcmEncoding int getEncoding() {
|
|
||||||
return encoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.wav;
|
package com.google.android.exoplayer2.extractor.wav;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
@ -23,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
|
|||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
|
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
|
||||||
@ -71,57 +73,46 @@ import java.io.IOException;
|
|||||||
Assertions.checkState(chunkHeader.size >= 16);
|
Assertions.checkState(chunkHeader.size >= 16);
|
||||||
input.peekFully(scratch.data, 0, 16);
|
input.peekFully(scratch.data, 0, 16);
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
int type = scratch.readLittleEndianUnsignedShort();
|
int audioFormatType = scratch.readLittleEndianUnsignedShort();
|
||||||
int numChannels = scratch.readLittleEndianUnsignedShort();
|
int numChannels = scratch.readLittleEndianUnsignedShort();
|
||||||
int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();
|
int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();
|
||||||
int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
|
int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
|
||||||
int blockAlignment = scratch.readLittleEndianUnsignedShort();
|
int blockAlignment = scratch.readLittleEndianUnsignedShort();
|
||||||
int bitsPerSample = scratch.readLittleEndianUnsignedShort();
|
int bitsPerSample = scratch.readLittleEndianUnsignedShort();
|
||||||
|
|
||||||
int expectedBlockAlignment = numChannels * bitsPerSample / 8;
|
int bytesLeft = (int) chunkHeader.size - 16;
|
||||||
if (blockAlignment != expectedBlockAlignment) {
|
byte[] extraData;
|
||||||
throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: "
|
if (bytesLeft > 0) {
|
||||||
+ blockAlignment);
|
extraData = new byte[bytesLeft];
|
||||||
|
input.peekFully(extraData, 0, bytesLeft);
|
||||||
|
} else {
|
||||||
|
extraData = Util.EMPTY_BYTE_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample);
|
|
||||||
if (encoding == C.ENCODING_INVALID) {
|
|
||||||
Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, type " + type);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...
|
|
||||||
input.advancePeekPosition((int) chunkHeader.size - 16);
|
|
||||||
|
|
||||||
return new WavHeader(
|
return new WavHeader(
|
||||||
|
audioFormatType,
|
||||||
numChannels,
|
numChannels,
|
||||||
sampleRateHz,
|
sampleRateHz,
|
||||||
averageBytesPerSecond,
|
averageBytesPerSecond,
|
||||||
blockAlignment,
|
blockAlignment,
|
||||||
bitsPerSample,
|
bitsPerSample,
|
||||||
/* samplesPerBlock= */ 1,
|
extraData);
|
||||||
encoding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips to the data in the given WAV input stream. After calling, the input stream's position
|
* Skips to the data in the given WAV input stream, and returns its bounds. After calling, the
|
||||||
* will point to the start of sample data in the WAV, and the data bounds of the provided {@link
|
* input stream's position will point to the start of sample data in the WAV. If an exception is
|
||||||
* WavHeader} will have been set.
|
* thrown, the input position will be left pointing to a chunk header.
|
||||||
*
|
*
|
||||||
* <p>If an exception is thrown, the input position will be left pointing to a chunk header and
|
* @param input The input stream, whose read position must be pointing to a valid chunk header.
|
||||||
* the bounds of the provided {@link WavHeader} will not have been set.
|
* @return The byte positions at which the data starts (inclusive) and ends (exclusive).
|
||||||
*
|
|
||||||
* @param input Input stream to skip to the data chunk in. Its peek position must be pointing to a
|
|
||||||
* valid chunk header.
|
|
||||||
* @param wavHeader WAV header to populate with data bounds.
|
|
||||||
* @throws ParserException If an error occurs parsing chunks.
|
* @throws ParserException If an error occurs parsing chunks.
|
||||||
* @throws IOException If reading from the input fails.
|
* @throws IOException If reading from the input fails.
|
||||||
* @throws InterruptedException If interrupted while reading from input.
|
* @throws InterruptedException If interrupted while reading from input.
|
||||||
*/
|
*/
|
||||||
public static void skipToData(ExtractorInput input, WavHeader wavHeader)
|
public static Pair<Long, Long> skipToData(ExtractorInput input)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
Assertions.checkNotNull(input);
|
Assertions.checkNotNull(input);
|
||||||
Assertions.checkNotNull(wavHeader);
|
|
||||||
|
|
||||||
// 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();
|
||||||
@ -147,14 +138,14 @@ import java.io.IOException;
|
|||||||
// Skip past the "data" header.
|
// Skip past the "data" header.
|
||||||
input.skipFully(ChunkHeader.SIZE_IN_BYTES);
|
input.skipFully(ChunkHeader.SIZE_IN_BYTES);
|
||||||
|
|
||||||
int dataStartPosition = (int) input.getPosition();
|
long dataStartPosition = input.getPosition();
|
||||||
long dataEndPosition = dataStartPosition + chunkHeader.size;
|
long dataEndPosition = dataStartPosition + chunkHeader.size;
|
||||||
long inputLength = input.getLength();
|
long inputLength = input.getLength();
|
||||||
if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {
|
if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {
|
||||||
Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength);
|
Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength);
|
||||||
dataEndPosition = inputLength;
|
dataEndPosition = inputLength;
|
||||||
}
|
}
|
||||||
wavHeader.setDataBounds(dataStartPosition, dataEndPosition);
|
return Pair.create(dataStartPosition, dataEndPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private WavHeaderReader() {
|
private WavHeaderReader() {
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 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.exoplayer2.extractor.wav;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/* package */ final class WavSeekMap implements SeekMap {
|
||||||
|
|
||||||
|
/** The WAV header for the stream. */
|
||||||
|
private final WavHeader wavHeader;
|
||||||
|
/** Number of samples in each block. */
|
||||||
|
private final int samplesPerBlock;
|
||||||
|
/** Position of the start of the sample data, in bytes. */
|
||||||
|
private final long dataStartPosition;
|
||||||
|
/** Position of the end of the sample data (exclusive), in bytes. */
|
||||||
|
private final long dataEndPosition;
|
||||||
|
|
||||||
|
public WavSeekMap(
|
||||||
|
WavHeader wavHeader, int samplesPerBlock, long dataStartPosition, long dataEndPosition) {
|
||||||
|
this.wavHeader = wavHeader;
|
||||||
|
this.samplesPerBlock = samplesPerBlock;
|
||||||
|
this.dataStartPosition = dataStartPosition;
|
||||||
|
this.dataEndPosition = dataEndPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSeekable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationUs() {
|
||||||
|
long numBlocks = (dataEndPosition - dataStartPosition) / wavHeader.blockAlign;
|
||||||
|
return numBlocks * samplesPerBlock * C.MICROS_PER_SECOND / wavHeader.sampleRateHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SeekPoints getSeekPoints(long timeUs) {
|
||||||
|
long blockAlign = wavHeader.blockAlign;
|
||||||
|
long dataSize = dataEndPosition - dataStartPosition;
|
||||||
|
long positionOffset = (timeUs * wavHeader.averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
||||||
|
// Constrain to nearest preceding frame offset.
|
||||||
|
positionOffset = (positionOffset / blockAlign) * blockAlign;
|
||||||
|
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlign);
|
||||||
|
long seekPosition = dataStartPosition + positionOffset;
|
||||||
|
long seekTimeUs = getTimeUs(seekPosition);
|
||||||
|
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
|
||||||
|
if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlign) {
|
||||||
|
return new SeekPoints(seekPoint);
|
||||||
|
} else {
|
||||||
|
long secondSeekPosition = seekPosition + blockAlign;
|
||||||
|
long secondSeekTimeUs = getTimeUs(secondSeekPosition);
|
||||||
|
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
|
||||||
|
return new SeekPoints(seekPoint, secondSeekPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time in microseconds for the given position in bytes.
|
||||||
|
*
|
||||||
|
* @param position The position in bytes.
|
||||||
|
*/
|
||||||
|
public long getTimeUs(long position) {
|
||||||
|
long positionOffset = Math.max(0, position - dataStartPosition);
|
||||||
|
return (positionOffset * C.MICROS_PER_SECOND) / wavHeader.averageBytesPerSecond;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user