blob: 69d10f16a1f0e98d7ac34aac9ad567706a8b7b27 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.wav;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.util.Pair;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.WavUtil;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
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;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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
* output for each second of media, meaning that each sample will have a duration of ~100ms.
*/
private static final int TARGET_SAMPLES_PER_SECOND = 10;
/** Factory for {@link WavExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
/** Parser state. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@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
})
private @interface State {}
private static final int STATE_READING_FILE_TYPE = 0;
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;
}
@Override
public boolean sniff(ExtractorInput input) throws IOException {
return WavHeaderReader.checkFileType(input);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
output.endTracks();
}
@Override
public void seek(long position, long timeUs) {
state = position == 0 ? STATE_READING_FILE_TYPE : STATE_READING_SAMPLE_DATA;
if (outputWriter != null) {
outputWriter.reset(timeUs);
}
}
@Override
public void release() {
// Do nothing
}
@Override
@ReadResult
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
assertInitialized();
switch (state) {
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;
case STATE_SKIPPING_TO_SAMPLE_DATA:
skipToSampleData(input);
return Extractor.RESULT_CONTINUE;
case STATE_READING_SAMPLE_DATA:
return readSampleData(input);
default:
throw new IllegalStateException();
}
}
@EnsuresNonNull({"extractorOutput", "trackOutput"})
private void assertInitialized() {
Assertions.checkStateNotNull(trackOutput);
Util.castNonNull(extractorOutput);
}
private void readFileType(ExtractorInput input) throws IOException {
Assertions.checkState(input.getPosition() == 0);
if (dataStartPosition != C.POSITION_UNSET) {
input.skipFully(dataStartPosition);
state = STATE_READING_SAMPLE_DATA;
return;
}
if (!WavHeaderReader.checkFileType(input)) {
// Should only happen if the media wasn't sniffed.
throw ParserException.createForMalformedContainer(
"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;
}
@RequiresNonNull({"extractorOutput", "trackOutput"})
private void readFormat(ExtractorInput input) throws IOException {
WavFormat wavFormat = WavHeaderReader.readFormat(input);
if (wavFormat.formatType == WavUtil.TYPE_IMA_ADPCM) {
outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, wavFormat);
} else if (wavFormat.formatType == WavUtil.TYPE_ALAW) {
outputWriter =
new PassthroughOutputWriter(
extractorOutput,
trackOutput,
wavFormat,
MimeTypes.AUDIO_ALAW,
/* pcmEncoding= */ Format.NO_VALUE);
} else if (wavFormat.formatType == WavUtil.TYPE_MLAW) {
outputWriter =
new PassthroughOutputWriter(
extractorOutput,
trackOutput,
wavFormat,
MimeTypes.AUDIO_MLAW,
/* pcmEncoding= */ Format.NO_VALUE);
} else {
@C.PcmEncoding
int pcmEncoding =
WavUtil.getPcmEncodingForType(wavFormat.formatType, wavFormat.bitsPerSample);
if (pcmEncoding == C.ENCODING_INVALID) {
throw ParserException.createForUnsupportedContainerFeature(
"Unsupported WAV format type: " + wavFormat.formatType);
}
outputWriter =
new PassthroughOutputWriter(
extractorOutput, trackOutput, wavFormat, MimeTypes.AUDIO_RAW, pcmEncoding);
}
state = STATE_SKIPPING_TO_SAMPLE_DATA;
}
private void skipToSampleData(ExtractorInput input) throws IOException {
Pair<Long, Long> dataBounds = WavHeaderReader.skipToSampleData(input);
dataStartPosition = dataBounds.first.intValue();
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;
}
@ReadResult
private int readSampleData(ExtractorInput input) throws IOException {
Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
long bytesLeft = dataEndPosition - input.getPosition();
return Assertions.checkNotNull(outputWriter).sampleData(input, bytesLeft)
? RESULT_END_OF_INPUT
: RESULT_CONTINUE;
}
/** Writes to the extractor's output. */
private interface OutputWriter {
/**
* Resets the writer.
*
* @param timeUs The new start position in microseconds.
*/
void reset(long timeUs);
/**
* Initializes the writer.
*
* <p>Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}.
*
* @param dataStartPosition The byte position (inclusive) in the stream at which data starts.
* @param dataEndPosition The end position (exclusive) in the stream at which data ends.
* @throws ParserException If an error occurs initializing the writer.
*/
void init(int dataStartPosition, long dataEndPosition) throws ParserException;
/**
* Consumes sample data from {@code input}, writing corresponding samples to the extractor's
* output.
*
* <p>Must not be called until after {@link #init(int, long)} has been called.
*
* @param input The input from which to read.
* @param bytesLeft The number of sample data bytes left to be read from the input.
* @return Whether the end of the sample data has been reached.
* @throws IOException If an error occurs reading from the input.
*/
boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException;
}
private static final class PassthroughOutputWriter implements OutputWriter {
private final ExtractorOutput extractorOutput;
private final TrackOutput trackOutput;
private final WavFormat wavFormat;
private final Format format;
/** 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;
public PassthroughOutputWriter(
ExtractorOutput extractorOutput,
TrackOutput trackOutput,
WavFormat wavFormat,
String mimeType,
@C.PcmEncoding int pcmEncoding)
throws ParserException {
this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput;
this.wavFormat = wavFormat;
int bytesPerFrame = wavFormat.numChannels * wavFormat.bitsPerSample / 8;
// Validate the WAV format. Blocks are expected to correspond to single frames.
if (wavFormat.blockSize != bytesPerFrame) {
throw ParserException.createForMalformedContainer(
"Expected block size: " + bytesPerFrame + "; got: " + wavFormat.blockSize,
/* cause= */ null);
}
int constantBitrate = wavFormat.frameRateHz * bytesPerFrame * 8;
targetSampleSizeBytes =
max(bytesPerFrame, wavFormat.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
format =
new Format.Builder()
.setSampleMimeType(mimeType)
.setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate)
.setMaxInputSize(targetSampleSizeBytes)
.setChannelCount(wavFormat.numChannels)
.setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(pcmEncoding)
.build();
}
@Override
public void reset(long timeUs) {
startTimeUs = timeUs;
pendingOutputBytes = 0;
outputFrameCount = 0;
}
@Override
public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap(
new WavSeekMap(wavFormat, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
trackOutput.format(format);
}
@Override
public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException {
// Write sample data until we've reached the target sample size, or the end of the data.
while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) {
int bytesToRead = (int) min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft);
int bytesAppended = trackOutput.sampleData(input, bytesToRead, true);
if (bytesAppended == RESULT_END_OF_INPUT) {
bytesLeft = 0;
} else {
pendingOutputBytes += bytesAppended;
bytesLeft -= bytesAppended;
}
}
// Write the corresponding sample metadata. Samples must be a whole number of frames. It's
// possible that the number of pending output bytes is not a whole number of frames if the
// stream ended unexpectedly.
int bytesPerFrame = wavFormat.blockSize;
int pendingFrames = pendingOutputBytes / bytesPerFrame;
if (pendingFrames > 0) {
long timeUs =
startTimeUs
+ Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, wavFormat.frameRateHz);
int size = pendingFrames * bytesPerFrame;
int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata(
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
outputFrameCount += pendingFrames;
pendingOutputBytes = offset;
}
return bytesLeft <= 0;
}
}
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 WavFormat wavFormat;
/** Number of frames per block of the input (yet to be decoded) data. */
private final int framesPerBlock;
/** Target for the input (yet to be decoded) data. */
private final byte[] inputData;
/** Target for decoded (yet to be output) data. */
private final ParsableByteArray decodedData;
/** The target size of each output sample, in frames. */
private final int targetSampleSizeFrames;
/** The output format. */
private final Format format;
/** The number of pending bytes in {@link #inputData}. */
private int pendingInputBytes;
/** 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, WavFormat wavFormat)
throws ParserException {
this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput;
this.wavFormat = wavFormat;
targetSampleSizeFrames = max(1, wavFormat.frameRateHz / TARGET_SAMPLES_PER_SECOND);
ParsableByteArray scratch = new ParsableByteArray(wavFormat.extraData);
scratch.readLittleEndianUnsignedShort();
framesPerBlock = scratch.readLittleEndianUnsignedShort();
int numChannels = wavFormat.numChannels;
// Validate the WAV format. 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 expectedFramesPerBlock =
(((wavFormat.blockSize - (4 * numChannels)) * 8)
/ (wavFormat.bitsPerSample * numChannels))
+ 1;
if (framesPerBlock != expectedFramesPerBlock) {
throw ParserException.createForMalformedContainer(
"Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock,
/* cause= */ null);
}
// 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 * wavFormat.blockSize];
decodedData =
new ParsableByteArray(
maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels));
// Create the format. We calculate the bitrate of the data before decoding, since this is the
// bitrate of the stream itself.
int constantBitrate = wavFormat.frameRateHz * wavFormat.blockSize * 8 / framesPerBlock;
format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setAverageBitrate(constantBitrate)
.setPeakBitrate(constantBitrate)
.setMaxInputSize(numOutputFramesToBytes(targetSampleSizeFrames, numChannels))
.setChannelCount(wavFormat.numChannels)
.setSampleRate(wavFormat.frameRateHz)
.setPcmEncoding(C.ENCODING_PCM_16BIT)
.build();
}
@Override
public void reset(long timeUs) {
pendingInputBytes = 0;
startTimeUs = timeUs;
pendingOutputBytes = 0;
outputFrameCount = 0;
}
@Override
public void init(int dataStartPosition, long dataEndPosition) {
extractorOutput.seekMap(
new WavSeekMap(wavFormat, framesPerBlock, dataStartPosition, dataEndPosition));
trackOutput.format(format);
}
@Override
public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException {
// 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 * wavFormat.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) 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 / wavFormat.blockSize;
if (pendingBlockCount > 0) {
// We have at least one whole block to decode.
decode(inputData, pendingBlockCount, decodedData);
pendingInputBytes -= pendingBlockCount * wavFormat.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, wavFormat.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 < wavFormat.numChannels; channelIndex++) {
decodeBlockForChannel(input, blockIndex, channelIndex, output.getData());
}
}
int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount);
output.setPosition(0);
output.setLimit(decodedDataSize);
}
private void decodeBlockForChannel(
byte[] input, int blockIndex, int channelIndex, byte[] output) {
int blockSize = wavFormat.blockSize;
int numChannels = wavFormat.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 = 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 * wavFormat.numChannels);
}
private int numOutputFramesToBytes(int frames) {
return numOutputFramesToBytes(frames, wavFormat.numChannels);
}
private static int numOutputFramesToBytes(int frames, int numChannels) {
return frames * 2 * numChannels;
}
}
}