From 826083db923f26f2000ca42f7b54b9531726d713 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 2 Jan 2020 18:19:41 +0000 Subject: [PATCH] Add support for IMA ADPCM in WAV PiperOrigin-RevId: 287854701 --- RELEASENOTES.md | 1 + .../android/exoplayer2/audio/WavUtil.java | 12 +- .../extractor/wav/WavExtractor.java | 314 +++++++++++++++++- .../src/test/assets/wav/sample_ima_adpcm.wav | Bin 0 -> 22622 bytes .../assets/wav/sample_ima_adpcm.wav.0.dump | 75 +++++ .../assets/wav/sample_ima_adpcm.wav.1.dump | 59 ++++ .../assets/wav/sample_ima_adpcm.wav.2.dump | 47 +++ .../assets/wav/sample_ima_adpcm.wav.3.dump | 35 ++ .../extractor/wav/WavExtractorTest.java | 5 + 9 files changed, 526 insertions(+), 22 deletions(-) create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump create mode 100644 library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 12b08c3c80..2dba34486b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,7 @@ ([#6602](https://github.com/google/ExoPlayer/issues/6602)). * Support "twos" codec (big endian PCM) in MP4 ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* WAV: Support IMA ADPCM encoded data. ### 2.11.1 (2019-12-20) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 29b772f838..25261f1686 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -32,15 +32,17 @@ public final class WavUtil { public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - private static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_A_LAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MU_LAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; /** * Returns the WAVE format type value for the given {@link C.PcmEncoding}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index c1eb357bb9..0c6e538f43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -28,6 +28,7 @@ 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.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -91,12 +92,16 @@ public final class WavExtractor implements Extractor { throw new ParserException("Unsupported or unrecognized wav header."); } - @C.PcmEncoding - int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported WAV format type: " + header.formatType); + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } if (dataStartPosition == C.POSITION_UNSET) { @@ -156,11 +161,22 @@ public final class WavExtractor implements Extractor { private final TrackOutput trackOutput; private final WavHeader header; private final @C.PcmEncoding int pcmEncoding; - private final int targetSampleSize; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ private long outputFrameCount; - private int pendingBytes; public PcmOutputWriter( ExtractorOutput extractorOutput, @@ -173,15 +189,15 @@ public final class WavExtractor implements Extractor { this.pcmEncoding = pcmEncoding; // For PCM blocks correspond to single frames. This is validated in init(int, long). int bytesPerFrame = header.blockSize; - targetSampleSize = + targetSampleSizeBytes = Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); } @Override public void reset(long timeUs) { startTimeUs = timeUs; + pendingOutputBytes = 0; outputFrameCount = 0; - pendingBytes = 0; } @Override @@ -204,7 +220,7 @@ public final class WavExtractor implements Extractor { MimeTypes.AUDIO_RAW, /* codecs= */ null, /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, - targetSampleSize, + /* maxInputSize= */ targetSampleSizeBytes, header.numChannels, header.frameRateHz, pcmEncoding, @@ -220,34 +236,298 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException { // Write sample data until we've reached the target sample size, or the end of the data. boolean endOfSampleData = bytesLeft == 0; - while (!endOfSampleData && pendingBytes < targetSampleSize) { - int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft); + while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); if (bytesAppended == RESULT_END_OF_INPUT) { endOfSampleData = true; } else { - pendingBytes += bytesAppended; + pendingOutputBytes += bytesAppended; } } // Write the corresponding sample metadata. Samples must be a whole number of frames. It's - // possible pendingBytes is not a whole number of frames if the stream ended unexpectedly. + // 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 pendingFrames = pendingBytes / bytesPerFrame; + int pendingFrames = pendingOutputBytes / bytesPerFrame; if (pendingFrames > 0) { long timeUs = startTimeUs + Util.scaleLargeTimestamp( outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); int size = pendingFrames * bytesPerFrame; - int offset = pendingBytes - size; + int offset = pendingOutputBytes - size; trackOutput.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); outputFrameCount += pendingFrames; - pendingBytes = offset; + pendingOutputBytes = offset; } return endOfSampleData; } } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + + // Properties of the input (yet to be decoded) data. + private int framesPerBlock; + private byte[] inputData; + private int pendingInputBytes; + + // Target for decoded (yet to be output) data. + private ParsableByteArray decodedData; + + // Properties of the output. + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + } + + @Override + public void reset(long timeUs) { + // Reset the input side. + pendingInputBytes = 0; + // Reset the output side. + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) throws ParserException { + // Validate the header. + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + // 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 numChannels = header.numChannels; + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // 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]; + decodedData = + new ParsableByteArray(maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock)); + + // Output the seek map. + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + + // Output the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + 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; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.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. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return frames * 2 * header.numChannels; + } + } } diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav b/library/core/src/test/assets/wav/sample_ima_adpcm.wav new file mode 100644 index 0000000000000000000000000000000000000000..661d54d1d716df8f4e7deef62e4cdf8a30783b44 GIT binary patch literal 22622 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 UP!6gOpHVE!w*LS7&wK;_0whkDivR!s literal 0 HcmV?d00001 diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump new file mode 100644 index 0000000000..a16ad68dfa --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump @@ -0,0 +1,75 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 89804 + sample count = 11 + sample 0: + time = 0 + flags = 1 + data = length 8820, hash E90A457C + sample 1: + time = 100000 + flags = 1 + data = length 8820, hash EA798370 + sample 2: + time = 200000 + flags = 1 + data = length 8820, hash A57ED989 + sample 3: + time = 300000 + flags = 1 + data = length 8820, hash 8B681816 + sample 4: + time = 400000 + flags = 1 + data = length 8820, hash 48177BEB + sample 5: + time = 500000 + flags = 1 + data = length 8820, hash 70197776 + sample 6: + time = 600000 + flags = 1 + data = length 8820, hash DB4A4704 + sample 7: + time = 700000 + flags = 1 + data = length 8820, hash 84A525D0 + sample 8: + time = 800000 + flags = 1 + data = length 8820, hash 197A4377 + sample 9: + time = 900000 + flags = 1 + data = length 8820, hash 6982BC91 + sample 10: + time = 1000000 + flags = 1 + data = length 1604, hash 3DED68ED +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump new file mode 100644 index 0000000000..3eb13e82bf --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 61230 + sample count = 7 + sample 0: + time = 339395 + flags = 1 + data = length 8820, hash 25FCA092 + sample 1: + time = 439395 + flags = 1 + data = length 8820, hash 9400B4BE + sample 2: + time = 539395 + flags = 1 + data = length 8820, hash 5BA7E45D + sample 3: + time = 639395 + flags = 1 + data = length 8820, hash 5AC42905 + sample 4: + time = 739395 + flags = 1 + data = length 8820, hash D57059C + sample 5: + time = 839395 + flags = 1 + data = length 8820, hash DEF5C480 + sample 6: + time = 939395 + flags = 1 + data = length 8310, hash 10B3FC93 +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump new file mode 100644 index 0000000000..bef16523d4 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump @@ -0,0 +1,47 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 32656 + sample count = 4 + sample 0: + time = 678790 + flags = 1 + data = length 8820, hash DB7FF64C + sample 1: + time = 778790 + flags = 1 + data = length 8820, hash B895DFDC + sample 2: + time = 878790 + flags = 1 + data = length 8820, hash E3AB416D + sample 3: + time = 978790 + flags = 1 + data = length 6196, hash E27E175A +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump new file mode 100644 index 0000000000..085fe5e592 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 4082 + sample count = 1 + sample 0: + time = 1018185 + flags = 1 + data = length 4082, hash 4CB1A490 +tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index c617b672e2..7f9549ea75 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -28,4 +28,9 @@ public final class WavExtractorTest { public void testSample() throws Exception { ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); } + + @Test + public void testSampleImaAdpcm() throws Exception { + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); + } }