Merge pull request #9543 from KasemJaffer:rf64

PiperOrigin-RevId: 408816643
This commit is contained in:
tonihei 2021-11-11 10:21:29 +00:00
commit 04517e17d4
11 changed files with 237 additions and 23 deletions

View File

@ -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)

View File

@ -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;

View File

@ -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<Long, Long> 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;
}

View File

@ -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);
}
/**

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.