diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7c278fa114..8c770912c4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ `buildVideoRenderers()` or `buildAudioRenderers()` can access the codec adapter factory and pass it to `MediaCodecRenderer` instances they create. +* Extractors: + * WAV: Add support for RF64 streams + ([#9543](https://github.com/google/ExoPlayer/issues/9543). ### 2.16.0 (2021-11-04) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 208989124a..c0f4b4c2d5 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -30,6 +30,10 @@ public final class WavUtil { public static final int FMT_FOURCC = 0x666d7420; /** Four character code for "data". */ public static final int DATA_FOURCC = 0x64617461; + /** Four character code for "RF64". */ + public static final int RF64_FOURCC = 0x52463634; + /** Four character code for "ds64". */ + public static final int DS64_FOURCC = 0x64733634; /** WAVE type value for integer PCM audio data. */ public static final int TYPE_PCM = 0x0001; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 6d3fc254b1..69d10f16a1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -47,6 +48,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Extracts data from WAV byte streams. */ public final class WavExtractor implements Extractor { + private static final String TAG = "WavExtractor"; + /** * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped * into each sample, and hence each sample's duration. This is the target number of samples to @@ -63,6 +66,7 @@ public final class WavExtractor implements Extractor { @Target({ElementType.TYPE_USE}) @IntDef({ STATE_READING_FILE_TYPE, + STATE_READING_RF64_SAMPLE_DATA_SIZE, STATE_READING_FORMAT, STATE_SKIPPING_TO_SAMPLE_DATA, STATE_READING_SAMPLE_DATA @@ -70,19 +74,22 @@ public final class WavExtractor implements Extractor { private @interface State {} 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 static final int STATE_READING_RF64_SAMPLE_DATA_SIZE = 1; + private static final int STATE_READING_FORMAT = 2; + private static final int STATE_SKIPPING_TO_SAMPLE_DATA = 3; + private static final int STATE_READING_SAMPLE_DATA = 4; private @MonotonicNonNull ExtractorOutput extractorOutput; private @MonotonicNonNull TrackOutput trackOutput; private @State int state; + private long rf64SampleDataSize; private @MonotonicNonNull OutputWriter outputWriter; private int dataStartPosition; private long dataEndPosition; public WavExtractor() { state = STATE_READING_FILE_TYPE; + rf64SampleDataSize = C.LENGTH_UNSET; dataStartPosition = C.POSITION_UNSET; dataEndPosition = C.POSITION_UNSET; } @@ -120,6 +127,9 @@ public final class WavExtractor implements Extractor { case STATE_READING_FILE_TYPE: readFileType(input); return Extractor.RESULT_CONTINUE; + case STATE_READING_RF64_SAMPLE_DATA_SIZE: + readRf64SampleDataSize(input); + return Extractor.RESULT_CONTINUE; case STATE_READING_FORMAT: readFormat(input); return Extractor.RESULT_CONTINUE; @@ -152,6 +162,11 @@ public final class WavExtractor implements Extractor { "Unsupported or unrecognized wav file type.", /* cause= */ null); } input.skipFully((int) (input.getPeekPosition() - input.getPosition())); + state = STATE_READING_RF64_SAMPLE_DATA_SIZE; + } + + private void readRf64SampleDataSize(ExtractorInput input) throws IOException { + rf64SampleDataSize = WavHeaderReader.readRf64SampleDataSize(input); state = STATE_READING_FORMAT; } @@ -194,7 +209,18 @@ public final class WavExtractor implements Extractor { private void skipToSampleData(ExtractorInput input) throws IOException { Pair dataBounds = WavHeaderReader.skipToSampleData(input); dataStartPosition = dataBounds.first.intValue(); - dataEndPosition = dataBounds.second; + long dataSize = dataBounds.second; + if (rf64SampleDataSize != C.LENGTH_UNSET && dataSize == 0xFFFFFFFFL) { + // Following EBU - Tech 3306-2007, the data size indicated in the ds64 chunk should only be + // used if the size of the data chunk is unset. + dataSize = rf64SampleDataSize; + } + dataEndPosition = dataStartPosition + dataSize; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } Assertions.checkNotNull(outputWriter).init(dataStartPosition, dataEndPosition); state = STATE_READING_SAMPLE_DATA; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index f05a510191..03cbb9645a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -32,20 +32,19 @@ import java.io.IOException; private static final String TAG = "WavHeaderReader"; /** - * Returns whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE - * tag. + * Returns whether the given {@code input} starts with a RIFF or RF64 chunk header, followed by a + * WAVE tag. * * @param input The input stream to peek from. The position should point to the start of the * stream. - * @return Whether the given {@code input} starts with a RIFF chunk header, followed by a WAVE - * tag. + * @return Whether the given {@code input} starts with a RIFF or RF64 chunk header, followed by a + * WAVE tag. * @throws IOException If peeking from the input fails. */ public static boolean checkFileType(ExtractorInput input) throws IOException { ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); - // Attempt to read the RIFF chunk. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - if (chunkHeader.id != WavUtil.RIFF_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.RF64_FOURCC) { return false; } @@ -60,11 +59,36 @@ import java.io.IOException; return true; } + /** + * Reads the ds64 chunk defined in EBU - TECH 3306-2007, if present. If there is no such chunk, + * the input's position is left unchanged. + * + * @param input Input stream to read from. The position should point to the byte following the + * WAVE tag. + * @throws IOException If reading from the input fails. + * @return The value of the data size field in the ds64 chunk, or {@link C#LENGTH_UNSET} if there + * is no such chunk. + */ + public static long readRf64SampleDataSize(ExtractorInput input) throws IOException { + ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + if (chunkHeader.id != WavUtil.DS64_FOURCC) { + input.resetPeekPosition(); + return C.LENGTH_UNSET; + } + input.advancePeekPosition(8); // RIFF size + scratch.setPosition(0); + input.peekFully(scratch.getData(), 0, 8); + long sampleDataSize = scratch.readLittleEndianLong(); + input.skipFully(ChunkHeader.SIZE_IN_BYTES + (int) chunkHeader.size); + return sampleDataSize; + } + /** * 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. + * following the ds64 chunk if present, or to the byte following the WAVE tag otherwise. * @throws IOException If reading from the input fails. * @return A new {@code WavFormat} read from {@code input}. */ @@ -104,13 +128,14 @@ import java.io.IOException; } /** - * Skips to the data in the given WAV input stream, and returns its bounds. After calling, the - * input stream's position will point to the start of sample data in the WAV. If an exception is - * thrown, the input position will be left pointing to a chunk header (that may not be the data - * chunk header). + * Skips to the data in the given WAV input stream, and returns its start position and size. After + * calling, the input stream's position will point to the start of sample data in the WAV. If an + * exception is thrown, the input position will be left pointing to a chunk header (that may not + * be the data chunk header). * * @param input The input stream, whose read position must be pointing to a valid chunk header. - * @return The byte positions at which the data starts (inclusive) and ends (exclusive). + * @return The byte positions at which the data starts (inclusive) and the size of the data, in + * bytes. * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. */ @@ -125,13 +150,7 @@ import java.io.IOException; input.skipFully(ChunkHeader.SIZE_IN_BYTES); long dataStartPosition = input.getPosition(); - long dataEndPosition = dataStartPosition + chunkHeader.size; - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { - Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); - dataEndPosition = inputLength; - } - return Pair.create(dataStartPosition, dataEndPosition); + return Pair.create(dataStartPosition, chunkHeader.size); } /** diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index 4217a1528a..976ef3e82e 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -53,4 +53,10 @@ public final class WavExtractorTest { ExtractorAsserts.assertBehavior( WavExtractor::new, "media/wav/sample_ima_adpcm.wav", simulationConfig); } + + @Test + public void sample_rf64() throws Exception { + ExtractorAsserts.assertBehavior( + WavExtractor::new, "media/wav/sample_rf64.wav", simulationConfig); + } } diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.0.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump new file mode 100644 index 0000000000..f4d925bad3 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.1.dump @@ -0,0 +1,32 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 44628 + sample count = 3 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 116208 + flags = 1 + data = length 19200, hash E4B962ED + sample 1: + time = 216208 + flags = 1 + data = length 19200, hash 4F13D6CF + sample 2: + time = 316208 + flags = 1 + data = length 6228, hash 3FB5F446 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump new file mode 100644 index 0000000000..2b42e93b5a --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.2.dump @@ -0,0 +1,28 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 22316 + sample count = 2 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 232416 + flags = 1 + data = length 19200, hash F82E494B + sample 1: + time = 332416 + flags = 1 + data = length 3116, hash 93C99CFD +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump new file mode 100644 index 0000000000..2a6345d4a8 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.3.dump @@ -0,0 +1,24 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 4 + sample count = 1 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 348625 + flags = 1 + data = length 4, hash FFD4C53F +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump new file mode 100644 index 0000000000..1e129acd70 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/wav/sample_rf64.wav.unknown_length.dump @@ -0,0 +1,36 @@ +seekMap: + isSeekable = true + duration = 348625 + getPosition(0) = [[timeUs=0, position=80]] + getPosition(1) = [[timeUs=0, position=80], [timeUs=20, position=84]] + getPosition(174312) = [[timeUs=174291, position=33544], [timeUs=174312, position=33548]] + getPosition(348625) = [[timeUs=348604, position=67012]] +numberOfTracks = 1 +track 0: + total output bytes = 66936 + sample count = 4 + format 0: + averageBitrate = 1536000 + peakBitrate = 1536000 + sampleMimeType = audio/raw + maxInputSize = 19200 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = 2 + sample 0: + time = 0 + flags = 1 + data = length 19200, hash EF6C7C27 + sample 1: + time = 100000 + flags = 1 + data = length 19200, hash 5AB97AFC + sample 2: + time = 200000 + flags = 1 + data = length 19200, hash 37920F33 + sample 3: + time = 300000 + flags = 1 + data = length 9336, hash 135F1C30 +tracksEnded = true diff --git a/testdata/src/test/assets/media/wav/sample_rf64.wav b/testdata/src/test/assets/media/wav/sample_rf64.wav new file mode 100644 index 0000000000..b2dd53c687 Binary files /dev/null and b/testdata/src/test/assets/media/wav/sample_rf64.wav differ