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