1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.wav; 17 18 import android.util.Log; 19 import org.mozilla.thirdparty.com.google.android.exoplayer2.C; 20 import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; 21 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput; 22 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; 23 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; 24 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; 25 import java.io.IOException; 26 27 /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ 28 /*package*/ final class WavHeaderReader { 29 30 private static final String TAG = "WavHeaderReader"; 31 32 /** Integer PCM audio data. */ 33 private static final int TYPE_PCM = 0x0001; 34 /** Extended WAVE format. */ 35 private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; 36 37 /** 38 * Peeks and returns a {@code WavHeader}. 39 * 40 * @param input Input stream to peek the WAV header from. 41 * @throws ParserException If the input file is an incorrect RIFF WAV. 42 * @throws IOException If peeking from the input fails. 43 * @throws InterruptedException If interrupted while peeking from input. 44 * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a 45 * supported WAV format. 46 */ peek(ExtractorInput input)47 public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { 48 Assertions.checkNotNull(input); 49 50 // Allocate a scratch buffer large enough to store the format chunk. 51 ParsableByteArray scratch = new ParsableByteArray(16); 52 53 // Attempt to read the RIFF chunk. 54 ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); 55 if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) { 56 return null; 57 } 58 59 input.peekFully(scratch.data, 0, 4); 60 scratch.setPosition(0); 61 int riffFormat = scratch.readInt(); 62 if (riffFormat != Util.getIntegerCodeForString("WAVE")) { 63 Log.e(TAG, "Unsupported RIFF format: " + riffFormat); 64 return null; 65 } 66 67 // Skip chunks until we find the format chunk. 68 chunkHeader = ChunkHeader.peek(input, scratch); 69 while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) { 70 input.advancePeekPosition((int) chunkHeader.size); 71 chunkHeader = ChunkHeader.peek(input, scratch); 72 } 73 74 Assertions.checkState(chunkHeader.size >= 16); 75 input.peekFully(scratch.data, 0, 16); 76 scratch.setPosition(0); 77 int type = scratch.readLittleEndianUnsignedShort(); 78 int numChannels = scratch.readLittleEndianUnsignedShort(); 79 int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt(); 80 int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); 81 int blockAlignment = scratch.readLittleEndianUnsignedShort(); 82 int bitsPerSample = scratch.readLittleEndianUnsignedShort(); 83 84 int expectedBlockAlignment = numChannels * bitsPerSample / 8; 85 if (blockAlignment != expectedBlockAlignment) { 86 throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " 87 + blockAlignment); 88 } 89 90 @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); 91 if (encoding == C.ENCODING_INVALID) { 92 Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); 93 return null; 94 } 95 96 if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { 97 Log.e(TAG, "Unsupported WAV format type: " + type); 98 return null; 99 } 100 101 // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... 102 input.advancePeekPosition((int) chunkHeader.size - 16); 103 104 return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, 105 bitsPerSample, encoding); 106 } 107 108 /** 109 * Skips to the data in the given WAV input stream and returns its data size. After calling, the 110 * input stream's position will point to the start of sample data in the WAV. 111 * <p> 112 * If an exception is thrown, the input position will be left pointing to a chunk header. 113 * 114 * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to 115 * a valid chunk header. 116 * @param wavHeader WAV header to populate with data bounds. 117 * @throws ParserException If an error occurs parsing chunks. 118 * @throws IOException If reading from the input fails. 119 * @throws InterruptedException If interrupted while reading from input. 120 */ skipToData(ExtractorInput input, WavHeader wavHeader)121 public static void skipToData(ExtractorInput input, WavHeader wavHeader) 122 throws IOException, InterruptedException { 123 Assertions.checkNotNull(input); 124 Assertions.checkNotNull(wavHeader); 125 126 // Make sure the peek position is set to the read position before we peek the first header. 127 input.resetPeekPosition(); 128 129 ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); 130 // Skip all chunks until we hit the data header. 131 ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); 132 while (chunkHeader.id != Util.getIntegerCodeForString("data")) { 133 Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); 134 long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; 135 // Override size of RIFF chunk, since it describes its size as the entire file. 136 if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { 137 bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; 138 } 139 if (bytesToSkip > Integer.MAX_VALUE) { 140 throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); 141 } 142 input.skipFully((int) bytesToSkip); 143 chunkHeader = ChunkHeader.peek(input, scratch); 144 } 145 // Skip past the "data" header. 146 input.skipFully(ChunkHeader.SIZE_IN_BYTES); 147 148 wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); 149 } 150 151 /** Container for a WAV chunk header. */ 152 private static final class ChunkHeader { 153 154 /** Size in bytes of a WAV chunk header. */ 155 public static final int SIZE_IN_BYTES = 8; 156 157 /** 4-character identifier, stored as an integer, for this chunk. */ 158 public final int id; 159 /** Size of this chunk in bytes. */ 160 public final long size; 161 ChunkHeader(int id, long size)162 private ChunkHeader(int id, long size) { 163 this.id = id; 164 this.size = size; 165 } 166 167 /** 168 * Peeks and returns a {@link ChunkHeader}. 169 * 170 * @param input Input stream to peek the chunk header from. 171 * @param scratch Buffer for temporary use. 172 * @throws IOException If peeking from the input fails. 173 * @throws InterruptedException If interrupted while peeking from input. 174 * @return A new {@code ChunkHeader} peeked from {@code input}. 175 */ peek(ExtractorInput input, ParsableByteArray scratch)176 public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) 177 throws IOException, InterruptedException { 178 input.peekFully(scratch.data, 0, SIZE_IN_BYTES); 179 scratch.setPosition(0); 180 181 int id = scratch.readInt(); 182 long size = scratch.readLittleEndianUnsignedInt(); 183 184 return new ChunkHeader(id, size); 185 } 186 } 187 } 188