Factor out common audio processor functionality

PiperOrigin-RevId: 234025553
This commit is contained in:
andrewlewis 2019-02-14 22:06:03 +00:00 committed by Andrew Lewis
parent 03006f0595
commit 83545fb7b2
8 changed files with 218 additions and 465 deletions

View file

@ -38,7 +38,8 @@ public interface AudioProcessor {
/** Exception thrown when a processor can't be configured for a given input audio format. */
final class UnhandledFormatException extends Exception {
public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
public UnhandledFormatException(
int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding "
+ encoding);
}
@ -60,7 +61,7 @@ public interface AudioProcessor {
* @return Whether to {@link #flush()} the processor.
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
*/
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException;
/** Returns whether the processor is configured and will process input buffers. */
@ -78,7 +79,7 @@ public interface AudioProcessor {
* result of calling {@link #configure(int, int, int)} and is undefined if the instance is not
* active.
*/
@C.Encoding
@C.PcmEncoding
int getOutputEncoding();
/**

View file

@ -0,0 +1,155 @@
/*
* Copyright (C) 2019 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.audio;
import android.support.annotation.CallSuper;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Base class for audio processors that keep an output buffer and an internal buffer that is reused
* whenever input is queued.
*/
public abstract class BaseAudioProcessor implements AudioProcessor {
/** The configured input sample rate, in Hertz, or {@link Format#NO_VALUE} if not configured. */
protected int sampleRateHz;
/** The configured input channel count, or {@link Format#NO_VALUE} if not configured. */
protected int channelCount;
/** The configured input encoding, or {@link Format#NO_VALUE} if not configured. */
@C.PcmEncoding protected int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
public BaseAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
encoding = Format.NO_VALUE;
}
@Override
public boolean isActive() {
return sampleRateHz != Format.NO_VALUE;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return encoding;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public final void queueEndOfStream() {
inputEnded = true;
onQueueEndOfStream();
}
@CallSuper
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@CallSuper
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public final void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
onFlush();
}
@Override
public final void reset() {
flush();
buffer = EMPTY_BUFFER;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = Format.NO_VALUE;
onReset();
}
/** Sets the input format of this processor, returning whether the input format has changed. */
protected final boolean setInputFormat(
int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
if (sampleRateHz == this.sampleRateHz
&& channelCount == this.channelCount
&& encoding == this.encoding) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
return true;
}
/**
* Replaces the current output buffer with a buffer of at least {@code count} bytes and returns
* it. Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be
* read via {@link #getOutput()}.
*/
protected final ByteBuffer replaceOutputBuffer(int count) {
if (buffer.capacity() < count) {
buffer = ByteBuffer.allocateDirect(count).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
outputBuffer = buffer;
return buffer;
}
/** Returns whether the current output buffer has any data remaining. */
protected final boolean hasPendingOutput() {
return outputBuffer.hasRemaining();
}
/** Called when the end-of-stream is queued to the processor. */
protected void onQueueEndOfStream() {
// Do nothing.
}
/** Called when the processor is flushed, directly or as part of resetting. */
protected void onFlush() {
// Do nothing.
}
/** Called when the processor is reset. */
protected void onReset() {
// Do nothing.
}
}

View file

@ -17,36 +17,20 @@ package com.google.android.exoplayer2.audio;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
* channels. This can be used to reorder, duplicate or discard channels.
*/
/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor {
/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor {
private int channelCount;
private int sampleRateHz;
@Nullable private int[] pendingOutputChannels;
private boolean active;
@Nullable private int[] outputChannels;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/** Creates a new processor that applies a channel mapping. */
public ChannelMappingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
}
/**
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
@ -61,10 +45,12 @@ import java.util.Arrays;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException {
boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
outputChannels = pendingOutputChannels;
int[] outputChannels = this.outputChannels;
if (outputChannels == null) {
active = false;
return outputChannelsChanged;
@ -72,12 +58,9 @@ import java.util.Arrays;
if (encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
&& this.channelCount == channelCount) {
if (!outputChannelsChanged && !setInputFormat(sampleRateHz, channelCount, encoding)) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
active = channelCount != outputChannels.length;
for (int i = 0; i < outputChannels.length; i++) {
@ -100,16 +83,6 @@ import java.util.Arrays;
return outputChannels == null ? channelCount : outputChannels.length;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int[] outputChannels = Assertions.checkNotNull(this.outputChannels);
@ -117,11 +90,7 @@ import java.util.Arrays;
int limit = inputBuffer.limit();
int frameCount = (limit - position) / (2 * channelCount);
int outputSize = frameCount * outputChannels.length * 2;
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
ByteBuffer buffer = replaceOutputBuffer(outputSize);
while (position < limit) {
for (int channelIndex : outputChannels) {
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
@ -130,39 +99,10 @@ import java.util.Arrays;
}
inputBuffer.position(limit);
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void reset() {
flush();
buffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
protected void onReset() {
outputChannels = null;
pendingOutputChannels = null;
active = false;

View file

@ -16,61 +16,30 @@
package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM
* audio.
*/
/* package */ final class FloatResamplingAudioProcessor implements AudioProcessor {
/* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor {
private static final int FLOAT_NAN_AS_INT = Float.floatToIntBits(Float.NaN);
private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF;
private int sampleRateHz;
private int channelCount;
@C.PcmEncoding private int sourceEncoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/** Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_FLOAT}. */
public FloatResamplingAudioProcessor() {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
sourceEncoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException {
if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz
&& this.channelCount == channelCount
&& sourceEncoding == encoding) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
sourceEncoding = encoding;
return true;
return setInputFormat(sampleRateHz, channelCount, encoding);
}
@Override
public boolean isActive() {
return Util.isEncodingHighResolutionIntegerPcm(sourceEncoding);
}
@Override
public int getOutputChannelCount() {
return channelCount;
return Util.isEncodingHighResolutionIntegerPcm(encoding);
}
@Override
@ -78,24 +47,15 @@ import java.nio.ByteOrder;
return C.ENCODING_PCM_FLOAT;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
boolean isInput32Bit = sourceEncoding == C.ENCODING_PCM_32BIT;
boolean isInput32Bit = encoding == C.ENCODING_PCM_32BIT;
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int size = limit - position;
int resampledSize = isInput32Bit ? size : (size / 3) * 4;
if (buffer.capacity() < resampledSize) {
buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
ByteBuffer buffer = replaceOutputBuffer(resampledSize);
if (isInput32Bit) {
for (int i = position; i < limit; i += 4) {
int pcm32BitInteger =
@ -117,40 +77,6 @@ import java.nio.ByteOrder;
inputBuffer.position(inputBuffer.limit());
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void reset() {
flush();
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
sourceEncoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
}
/**

View file

@ -18,45 +18,21 @@ package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit
* integer PCM audio.
*/
/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
private int sampleRateHz;
private int channelCount;
@C.PcmEncoding private int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/** Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */
public ResamplingAudioProcessor() {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor {
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
&& this.encoding == encoding) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
return true;
return setInputFormat(sampleRateHz, channelCount, encoding);
}
@Override
@ -64,21 +40,11 @@ import java.nio.ByteOrder;
return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
// Prepare the output buffer.
@ -105,13 +71,9 @@ import java.nio.ByteOrder;
default:
throw new IllegalStateException();
}
if (buffer.capacity() < resampledSize) {
buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
// Resample the little endian input and update the input/output buffers.
ByteBuffer buffer = replaceOutputBuffer(resampledSize);
switch (encoding) {
case C.ENCODING_PCM_8BIT:
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
@ -146,40 +108,6 @@ import java.nio.ByteOrder;
}
inputBuffer.position(inputBuffer.limit());
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void reset() {
flush();
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
}
}

View file

@ -17,19 +17,17 @@ package com.google.android.exoplayer2.audio;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit
* PCM.
*/
public final class SilenceSkippingAudioProcessor implements AudioProcessor {
public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
/**
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
@ -70,16 +68,10 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
/** State when the input is silent. */
private static final int STATE_SILENT = 2;
private int channelCount;
private int sampleRateHz;
private int bytesPerFrame;
private boolean enabled;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Buffers audio data that may be classified as silence while in {@link #STATE_MAYBE_SILENT}. If
* the input becomes noisy before the buffer has filled, it will be output. Otherwise, the buffer
@ -101,10 +93,6 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
/** Creates a new silence trimming audio processor. */
public SilenceSkippingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;
paddingBuffer = Util.EMPTY_BYTE_ARRAY;
}
@ -131,43 +119,23 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
// AudioProcessor implementation.
@Override
public boolean configure(int sampleRateHz, int channelCount, int encoding)
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
bytesPerFrame = channelCount * 2;
return true;
return setInputFormat(sampleRateHz, channelCount, encoding);
}
@Override
public boolean isActive() {
return sampleRateHz != Format.NO_VALUE && enabled;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public @C.Encoding int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
return super.isActive() && enabled;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
while (inputBuffer.hasRemaining() && !outputBuffer.hasRemaining()) {
while (inputBuffer.hasRemaining() && !hasPendingOutput()) {
switch (state) {
case STATE_NOISY:
processNoisy(inputBuffer);
@ -185,8 +153,7 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
}
@Override
public void queueEndOfStream() {
inputEnded = true;
protected void onQueueEndOfStream() {
if (maybeSilenceBufferSize > 0) {
// We haven't received enough silence to transition to the silent state, so output the buffer.
output(maybeSilenceBuffer, maybeSilenceBufferSize);
@ -197,20 +164,7 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
protected void onFlush() {
if (isActive()) {
int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame;
if (maybeSilenceBuffer.length != maybeSilenceBufferSize) {
@ -222,20 +176,14 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
}
}
state = STATE_NOISY;
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
skippedFrames = 0;
maybeSilenceBufferSize = 0;
hasOutputNoise = false;
}
@Override
public void reset() {
protected void onReset() {
enabled = false;
flush();
buffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
paddingSize = 0;
maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;
paddingBuffer = Util.EMPTY_BYTE_ARRAY;
@ -330,30 +278,19 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
* processor.
*/
private void output(byte[] data, int length) {
prepareForOutput(length);
buffer.put(data, 0, length);
buffer.flip();
outputBuffer = buffer;
replaceOutputBuffer(length).put(data, 0, length).flip();
if (length > 0) {
hasOutputNoise = true;
}
}
/**
* Copies remaining bytes from {@code data} to populate a new output buffer from the processor.
*/
private void output(ByteBuffer data) {
prepareForOutput(data.remaining());
buffer.put(data);
buffer.flip();
outputBuffer = buffer;
}
/** Prepares to output {@code size} bytes in {@code buffer}. */
private void prepareForOutput(int size) {
if (buffer.capacity() < size) {
buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
if (size > 0) {
int length = data.remaining();
replaceOutputBuffer(length).put(data).flip();
if (length > 0) {
hasOutputNoise = true;
}
}

View file

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@ -36,13 +35,13 @@ import java.nio.ByteOrder;
* custom {@link com.google.android.exoplayer2.audio.DefaultAudioSink.AudioProcessorChain} when
* creating the audio sink, and include this audio processor after all other audio processors.
*/
public final class TeeAudioProcessor implements AudioProcessor {
public final class TeeAudioProcessor extends BaseAudioProcessor {
/** A sink for audio buffers handled by the audio processor. */
public interface AudioBufferSink {
/** Called when the audio processor is flushed with a format of subsequent input. */
void flush(int sampleRateHz, int channelCount, @C.Encoding int encoding);
void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding);
/**
* Called when data is written to the audio processor.
@ -54,14 +53,6 @@ public final class TeeAudioProcessor implements AudioProcessor {
private final AudioBufferSink audioBufferSink;
private int sampleRateHz;
private int channelCount;
@C.Encoding private int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Creates a new tee audio processor, sending incoming data to the given {@link AudioBufferSink}.
*
@ -70,100 +61,28 @@ public final class TeeAudioProcessor implements AudioProcessor {
*/
public TeeAudioProcessor(AudioBufferSink audioBufferSink) {
this.audioBufferSink = Assertions.checkNotNull(audioBufferSink);
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
boolean formatChanged =
sampleRateHz != this.sampleRateHz
|| channelCount != this.channelCount
|| encoding != this.encoding;
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
// The sink always needs to be flushed if the format is changing.
return formatChanged;
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
return setInputFormat(sampleRateHz, channelCount, encoding);
}
@Override
public boolean isActive() {
return sampleRateHz != Format.NO_VALUE;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return encoding;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer buffer) {
int remaining = buffer.remaining();
public void queueInput(ByteBuffer inputBuffer) {
int remaining = inputBuffer.remaining();
if (remaining == 0) {
return;
}
audioBufferSink.handleBuffer(inputBuffer.asReadOnlyBuffer());
replaceOutputBuffer(remaining).put(inputBuffer).flip();
}
audioBufferSink.handleBuffer(buffer.asReadOnlyBuffer());
if (this.buffer.capacity() < remaining) {
this.buffer = ByteBuffer.allocateDirect(remaining).order(ByteOrder.nativeOrder());
} else {
this.buffer.clear();
@Override
protected void onFlush() {
if (isActive()) {
audioBufferSink.flush(sampleRateHz, channelCount, encoding);
}
this.buffer.put(buffer);
this.buffer.flip();
outputBuffer = this.buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
audioBufferSink.flush(sampleRateHz, channelCount, encoding);
}
@Override
public void reset() {
flush();
buffer = EMPTY_BUFFER;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = Format.NO_VALUE;
}
/**
@ -188,8 +107,8 @@ public final class TeeAudioProcessor implements AudioProcessor {
private int sampleRateHz;
private int channelCount;
private @C.Encoding int encoding;
private @Nullable RandomAccessFile randomAccessFile;
@C.PcmEncoding private int encoding;
@Nullable private RandomAccessFile randomAccessFile;
private int counter;
private int bytesWritten;
@ -205,7 +124,7 @@ public final class TeeAudioProcessor implements AudioProcessor {
}
@Override
public void flush(int sampleRateHz, int channelCount, int encoding) {
public void flush(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {
try {
reset();
} catch (IOException e) {

View file

@ -16,39 +16,27 @@
package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/** Audio processor for trimming samples from the start/end of data. */
/* package */ final class TrimmingAudioProcessor implements AudioProcessor {
/* package */ final class TrimmingAudioProcessor extends BaseAudioProcessor {
@C.Encoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT;
@C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT;
private boolean isActive;
private int trimStartFrames;
private int trimEndFrames;
private int channelCount;
private int sampleRateHz;
private int bytesPerFrame;
private boolean receivedInputSinceConfigure;
private int pendingTrimStartBytes;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private byte[] endBuffer;
private int endBufferSize;
private boolean inputEnded;
private long trimmedFrameCount;
/** Creates a new audio processor for trimming samples from the start/end of data. */
public TrimmingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
endBuffer = Util.EMPTY_BYTE_ARRAY;
}
@ -80,7 +68,7 @@ import java.nio.ByteOrder;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)
throws UnhandledFormatException {
if (encoding != OUTPUT_ENCODING) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
@ -88,8 +76,6 @@ import java.nio.ByteOrder;
if (endBufferSize > 0) {
trimmedFrameCount += endBufferSize / bytesPerFrame;
}
this.channelCount = channelCount;
this.sampleRateHz = sampleRateHz;
bytesPerFrame = Util.getPcmFrameSize(OUTPUT_ENCODING, channelCount);
endBuffer = new byte[trimEndFrames * bytesPerFrame];
endBufferSize = 0;
@ -97,6 +83,7 @@ import java.nio.ByteOrder;
boolean wasActive = isActive;
isActive = trimStartFrames != 0 || trimEndFrames != 0;
receivedInputSinceConfigure = false;
setInputFormat(sampleRateHz, channelCount, encoding);
return wasActive != isActive;
}
@ -105,21 +92,6 @@ import java.nio.ByteOrder;
return isActive;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return OUTPUT_ENCODING;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int position = inputBuffer.position();
@ -147,11 +119,7 @@ import java.nio.ByteOrder;
// endBuffer as full as possible, the output should be any surplus bytes currently in endBuffer
// followed by any surplus bytes in the new inputBuffer.
int remainingBytesToOutput = endBufferSize + remaining - endBuffer.length;
if (buffer.capacity() < remainingBytesToOutput) {
buffer = ByteBuffer.allocateDirect(remainingBytesToOutput).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
ByteBuffer buffer = replaceOutputBuffer(remainingBytesToOutput);
// Output from endBuffer.
int endBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, endBufferSize);
@ -172,48 +140,31 @@ import java.nio.ByteOrder;
endBufferSize += remaining;
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@SuppressWarnings("ReferenceEquality")
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
if (inputEnded && endBufferSize > 0 && outputBuffer == EMPTY_BUFFER) {
if (super.isEnded() && endBufferSize > 0) {
// Because audio processors may be drained in the middle of the stream we assume that the
// contents of the end buffer need to be output. Gapless transitions don't involve a call to
// queueEndOfStream so won't be affected. When audio is actually ending we play the padding
// data which is incorrect. This behavior can be fixed once we have the timestamps associated
// with input buffers.
if (buffer.capacity() < endBufferSize) {
buffer = ByteBuffer.allocateDirect(endBufferSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
buffer.put(endBuffer, 0, endBufferSize);
replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip();
endBufferSize = 0;
buffer.flip();
outputBuffer = buffer;
}
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
return super.getOutput();
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && endBufferSize == 0 && outputBuffer == EMPTY_BUFFER;
return super.isEnded() && endBufferSize == 0;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
protected void onFlush() {
if (receivedInputSinceConfigure) {
// Audio processors are flushed after initial configuration, so we leave the pending trim
// start byte count unmodified if the processor was just configured. Otherwise we (possibly
@ -226,11 +177,7 @@ import java.nio.ByteOrder;
}
@Override
public void reset() {
flush();
buffer = EMPTY_BUFFER;
channelCount = Format.NO_VALUE;
sampleRateHz = Format.NO_VALUE;
protected void onReset() {
endBuffer = Util.EMPTY_BYTE_ARRAY;
}