mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Create WavExtractor.OutputWriter to handle different data formats
- Create PcmOutputWriter for PCM. - In a future change an ImaAdPcmOutputWriter will be introduced for IMA ADPCM support. PiperOrigin-RevId: 285238246
This commit is contained in:
parent
227b2242ed
commit
ae2449915d
@ -35,19 +35,17 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public final class WavExtractor implements Extractor {
|
public final class WavExtractor implements Extractor {
|
||||||
|
|
||||||
|
/** Arbitrary maximum sample size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
|
||||||
|
private static final int MAX_SAMPLE_SIZE = 32 * 1024;
|
||||||
|
|
||||||
/** Factory for {@link WavExtractor} instances. */
|
/** Factory for {@link WavExtractor} instances. */
|
||||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
|
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
|
||||||
|
|
||||||
/** 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 ExtractorOutput extractorOutput;
|
||||||
private TrackOutput trackOutput;
|
private TrackOutput trackOutput;
|
||||||
private WavHeader header;
|
private OutputWriter outputWriter;
|
||||||
private WavSeekMap seekMap;
|
|
||||||
private int dataStartPosition;
|
private int dataStartPosition;
|
||||||
private long dataEndPosition;
|
private long dataEndPosition;
|
||||||
private int pendingBytes;
|
|
||||||
|
|
||||||
public WavExtractor() {
|
public WavExtractor() {
|
||||||
dataStartPosition = C.POSITION_UNSET;
|
dataStartPosition = C.POSITION_UNSET;
|
||||||
@ -63,13 +61,14 @@ 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);
|
||||||
header = null;
|
|
||||||
output.endTracks();
|
output.endTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position, long timeUs) {
|
public void seek(long position, long timeUs) {
|
||||||
pendingBytes = 0;
|
if (outputWriter != null) {
|
||||||
|
outputWriter.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -80,8 +79,8 @@ 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 (header == null) {
|
if (outputWriter == null) {
|
||||||
header = WavHeaderReader.peek(input);
|
WavHeader header = WavHeaderReader.peek(input);
|
||||||
if (header == 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.");
|
||||||
@ -92,41 +91,14 @@ public final class WavExtractor implements Extractor {
|
|||||||
if (pcmEncoding == C.ENCODING_INVALID) {
|
if (pcmEncoding == C.ENCODING_INVALID) {
|
||||||
throw new ParserException("Unsupported WAV format type: " + header.formatType);
|
throw new ParserException("Unsupported WAV format type: " + header.formatType);
|
||||||
}
|
}
|
||||||
|
outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataStartPosition == C.POSITION_UNSET) {
|
if (dataStartPosition == C.POSITION_UNSET) {
|
||||||
Pair<Long, Long> dataBounds = WavHeaderReader.skipToData(input);
|
Pair<Long, Long> dataBounds = WavHeaderReader.skipToData(input);
|
||||||
dataStartPosition = dataBounds.first.intValue();
|
dataStartPosition = dataBounds.first.intValue();
|
||||||
dataEndPosition = dataBounds.second;
|
dataEndPosition = dataBounds.second;
|
||||||
seekMap =
|
outputWriter.init(dataStartPosition, dataEndPosition);
|
||||||
new WavSeekMap(header, /* samplesPerBlock= */ 1, dataStartPosition, dataEndPosition);
|
|
||||||
extractorOutput.seekMap(seekMap);
|
|
||||||
} else if (input.getPosition() == 0) {
|
} else if (input.getPosition() == 0) {
|
||||||
input.skipFully(dataStartPosition);
|
input.skipFully(dataStartPosition);
|
||||||
}
|
}
|
||||||
@ -137,23 +109,122 @@ public final class WavExtractor implements Extractor {
|
|||||||
return Extractor.RESULT_END_OF_INPUT;
|
return Extractor.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
int maxBytesToRead = (int) Math.min(MAX_INPUT_SIZE - pendingBytes, bytesLeft);
|
return outputWriter.sampleData(input, bytesLeft) ? RESULT_CONTINUE : RESULT_END_OF_INPUT;
|
||||||
int bytesAppended = trackOutput.sampleData(input, maxBytesToRead, true);
|
}
|
||||||
if (bytesAppended != RESULT_END_OF_INPUT) {
|
|
||||||
pendingBytes += bytesAppended;
|
/** Writes to the extractor's output. */
|
||||||
|
private interface OutputWriter {
|
||||||
|
|
||||||
|
/** Resets the writer. */
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the writer.
|
||||||
|
*
|
||||||
|
* <p>Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}.
|
||||||
|
*
|
||||||
|
* @param dataStartPosition The byte position (inclusive) in the stream at which data starts.
|
||||||
|
* @param dataEndPosition The end position (exclusive) in the stream at which data ends.
|
||||||
|
* @throws ParserException If an error occurs initializing the writer.
|
||||||
|
*/
|
||||||
|
void init(int dataStartPosition, long dataEndPosition) throws ParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes sample data from {@code input}, writing corresponding samples to the extractor's
|
||||||
|
* output.
|
||||||
|
*
|
||||||
|
* <p>Must not be called until after {@link #init(int, long)} has been called.
|
||||||
|
*
|
||||||
|
* @param input The input from which to read.
|
||||||
|
* @param bytesLeft The number of sample data bytes left to be read from the input.
|
||||||
|
* @return True if data was consumed. False if the end of the stream has been reached.
|
||||||
|
* @throws IOException If an error occurs reading from the input.
|
||||||
|
* @throws InterruptedException If the thread has been interrupted.
|
||||||
|
*/
|
||||||
|
boolean sampleData(ExtractorInput input, long bytesLeft)
|
||||||
|
throws IOException, InterruptedException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PcmOutputWriter implements OutputWriter {
|
||||||
|
|
||||||
|
private final ExtractorOutput extractorOutput;
|
||||||
|
private final TrackOutput trackOutput;
|
||||||
|
private final WavHeader header;
|
||||||
|
private final @C.PcmEncoding int pcmEncoding;
|
||||||
|
|
||||||
|
private WavSeekMap seekMap;
|
||||||
|
private int pendingBytes;
|
||||||
|
|
||||||
|
public PcmOutputWriter(
|
||||||
|
ExtractorOutput extractorOutput,
|
||||||
|
TrackOutput trackOutput,
|
||||||
|
WavHeader header,
|
||||||
|
@C.PcmEncoding int pcmEncoding) {
|
||||||
|
this.extractorOutput = extractorOutput;
|
||||||
|
this.trackOutput = trackOutput;
|
||||||
|
this.header = header;
|
||||||
|
this.pcmEncoding = pcmEncoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For PCM blockAlign is the frame size, and samples must consist of a whole number of frames.
|
@Override
|
||||||
int bytesPerFrame = header.blockAlign;
|
public void reset() {
|
||||||
int pendingFrames = pendingBytes / bytesPerFrame;
|
pendingBytes = 0;
|
||||||
if (pendingFrames > 0) {
|
|
||||||
long timeUs = seekMap.getTimeUs(input.getPosition() - pendingBytes);
|
|
||||||
int size = pendingFrames * bytesPerFrame;
|
|
||||||
pendingBytes -= size;
|
|
||||||
trackOutput.sampleMetadata(
|
|
||||||
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, /* encryptionData= */ null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
@Override
|
||||||
|
public void init(int dataStartPosition, long dataEndPosition) throws ParserException {
|
||||||
|
// Validate the header.
|
||||||
|
int expectedBytesPerFrame = header.numChannels * header.bitsPerSample / 8;
|
||||||
|
if (header.blockAlign != expectedBytesPerFrame) {
|
||||||
|
throw new ParserException(
|
||||||
|
"Expected block alignment: " + expectedBytesPerFrame + "; got: " + header.blockAlign);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the seek map.
|
||||||
|
seekMap =
|
||||||
|
new WavSeekMap(header, /* samplesPerBlock= */ 1, dataStartPosition, dataEndPosition);
|
||||||
|
extractorOutput.seekMap(seekMap);
|
||||||
|
|
||||||
|
// Output the format.
|
||||||
|
Format format =
|
||||||
|
Format.createAudioSampleFormat(
|
||||||
|
/* id= */ null,
|
||||||
|
MimeTypes.AUDIO_RAW,
|
||||||
|
/* codecs= */ null,
|
||||||
|
/* bitrate= */ header.averageBytesPerSecond * 8,
|
||||||
|
MAX_SAMPLE_SIZE,
|
||||||
|
header.numChannels,
|
||||||
|
header.sampleRateHz,
|
||||||
|
pcmEncoding,
|
||||||
|
/* initializationData= */ null,
|
||||||
|
/* drmInitData= */ null,
|
||||||
|
/* selectionFlags= */ 0,
|
||||||
|
/* language= */ null);
|
||||||
|
trackOutput.format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean sampleData(ExtractorInput input, long bytesLeft)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
int maxBytesToRead = (int) Math.min(MAX_SAMPLE_SIZE - pendingBytes, bytesLeft);
|
||||||
|
int numBytesAppended = trackOutput.sampleData(input, maxBytesToRead, true);
|
||||||
|
boolean wereBytesAppended = numBytesAppended != RESULT_END_OF_INPUT;
|
||||||
|
if (wereBytesAppended) {
|
||||||
|
pendingBytes += numBytesAppended;
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockAlign is the frame size, and samples must consist of a whole number of frames.
|
||||||
|
int bytesPerFrame = header.blockAlign;
|
||||||
|
int pendingFrames = pendingBytes / bytesPerFrame;
|
||||||
|
if (pendingFrames > 0) {
|
||||||
|
long timeUs = seekMap.getTimeUs(input.getPosition() - pendingBytes);
|
||||||
|
int size = pendingFrames * bytesPerFrame;
|
||||||
|
pendingBytes -= size;
|
||||||
|
trackOutput.sampleMetadata(
|
||||||
|
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, /* encryptionData= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wereBytesAppended;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user