blob: fac1c4e3229fc61cae12040e5548ba11428e4e1e [file] [log] [blame]
/*
* Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link SilenceSkippingAudioProcessor}. */
@RunWith(AndroidJUnit4.class)
public final class SilenceSkippingAudioProcessorTest {
private static final AudioFormat AUDIO_FORMAT =
new AudioFormat(
/* sampleRate= */ 1000, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT);
private static final int TEST_SIGNAL_SILENCE_DURATION_MS = 1000;
private static final int TEST_SIGNAL_NOISE_DURATION_MS = 1000;
private static final int TEST_SIGNAL_FRAME_COUNT = 100000;
private static final int INPUT_BUFFER_SIZE = 100;
private SilenceSkippingAudioProcessor silenceSkippingAudioProcessor;
@Before
public void setUp() {
silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();
}
@Test
public void enabledProcessor_isActive() throws Exception {
// Given an enabled processor.
silenceSkippingAudioProcessor.setEnabled(true);
// When configuring it.
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
// It's active.
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
}
@Test
public void disabledProcessor_isNotActive() throws Exception {
// Given a disabled processor.
silenceSkippingAudioProcessor.setEnabled(false);
// When configuring it.
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
// It's not active.
assertThat(silenceSkippingAudioProcessor.isActive()).isFalse();
}
@Test
public void defaultProcessor_isNotEnabled() throws Exception {
// Given a processor in its default state.
// When reconfigured.
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
// It's not active.
assertThat(silenceSkippingAudioProcessor.isActive()).isFalse();
}
@Test
public void skipInSilentSignal_skipsEverything() throws Exception {
// Given a signal with only noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
/* noiseDurationMs= */ 0,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal.
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);
// The entire signal is skipped.
assertThat(totalOutputFrames).isEqualTo(0);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(TEST_SIGNAL_FRAME_COUNT);
}
@Test
public void skipInNoisySignal_skipsNothing() throws Exception {
// Given a signal with only silence.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
/* silenceDurationMs= */ 0,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);
// None of the signal is skipped.
assertThat(totalOutputFrames).isEqualTo(TEST_SIGNAL_FRAME_COUNT);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0);
}
@Test
public void skipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCounts() throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);
// The right number of frames are skipped/output.
assertThat(totalOutputFrames).isEqualTo(57980);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);
}
@Test
public void skipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts()
throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal with a smaller input buffer size.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 80);
// The right number of frames are skipped/output.
assertThat(totalOutputFrames).isEqualTo(57980);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);
}
@Test
public void skipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts()
throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal with a larger input buffer size.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
long totalOutputFrames =
process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120);
// The right number of frames are skipped/output.
assertThat(totalOutputFrames).isEqualTo(57980);
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);
}
@Test
public void skipThenFlush_resetsSkippedFrameCount() throws Exception {
// Given a signal that alternates between silence and noise.
InputBufferProvider inputBufferProvider =
getInputBufferProviderForAlternatingSilenceAndNoise(
TEST_SIGNAL_SILENCE_DURATION_MS,
TEST_SIGNAL_NOISE_DURATION_MS,
TEST_SIGNAL_FRAME_COUNT);
// When processing the entire signal then flushing.
SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =
new SilenceSkippingAudioProcessor();
silenceSkippingAudioProcessor.setEnabled(true);
silenceSkippingAudioProcessor.configure(AUDIO_FORMAT);
silenceSkippingAudioProcessor.flush();
assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();
process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);
silenceSkippingAudioProcessor.flush();
// The skipped frame count is zero.
assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0);
}
/**
* Processes the entire stream provided by {@code inputBufferProvider} in chunks of {@code
* inputBufferSize} and returns the total number of output frames.
*/
private static long process(
SilenceSkippingAudioProcessor processor,
InputBufferProvider inputBufferProvider,
int inputBufferSize) {
int bytesPerFrame = AUDIO_FORMAT.bytesPerFrame;
processor.flush();
long totalOutputFrames = 0;
while (inputBufferProvider.hasRemaining()) {
ByteBuffer inputBuffer = inputBufferProvider.getNextInputBuffer(inputBufferSize);
while (inputBuffer.hasRemaining()) {
processor.queueInput(inputBuffer);
ByteBuffer outputBuffer = processor.getOutput();
totalOutputFrames += outputBuffer.remaining() / bytesPerFrame;
outputBuffer.clear();
}
}
processor.queueEndOfStream();
while (!processor.isEnded()) {
ByteBuffer outputBuffer = processor.getOutput();
totalOutputFrames += outputBuffer.remaining() / bytesPerFrame;
outputBuffer.clear();
}
return totalOutputFrames;
}
/**
* Returns an {@link InputBufferProvider} that provides input buffers for a stream that alternates
* between silence/noise of the specified durations to fill {@code totalFrameCount}.
*/
private static InputBufferProvider getInputBufferProviderForAlternatingSilenceAndNoise(
int silenceDurationMs,
int noiseDurationMs,
int totalFrameCount) {
int sampleRate = AUDIO_FORMAT.sampleRate;
int channelCount = AUDIO_FORMAT.channelCount;
Pcm16BitAudioBuilder audioBuilder = new Pcm16BitAudioBuilder(channelCount, totalFrameCount);
while (!audioBuilder.isFull()) {
int silenceDurationFrames = (silenceDurationMs * sampleRate) / 1000;
audioBuilder.appendFrames(
/* count= */ silenceDurationFrames, /* channelLevels...= */ (short) 0);
int noiseDurationFrames = (noiseDurationMs * sampleRate) / 1000;
audioBuilder.appendFrames(
/* count= */ noiseDurationFrames, /* channelLevels...= */ Short.MAX_VALUE);
}
return new InputBufferProvider(audioBuilder.build());
}
/**
* Wraps a {@link ShortBuffer} and provides a sequence of {@link ByteBuffer}s of specified sizes
* that contain copies of its data.
*/
private static final class InputBufferProvider {
private final ShortBuffer buffer;
public InputBufferProvider(ShortBuffer buffer) {
this.buffer = buffer;
}
/** Returns the next buffer with size up to {@code sizeBytes}. */
public ByteBuffer getNextInputBuffer(int sizeBytes) {
ByteBuffer inputBuffer = ByteBuffer.allocate(sizeBytes).order(ByteOrder.nativeOrder());
ShortBuffer inputBufferAsShortBuffer = inputBuffer.asShortBuffer();
int limit = buffer.limit();
buffer.limit(Math.min(buffer.position() + sizeBytes / 2, limit));
inputBufferAsShortBuffer.put(buffer);
buffer.limit(limit);
inputBuffer.limit(inputBufferAsShortBuffer.position() * 2);
return inputBuffer;
}
/** Returns whether any more input can be provided via {@link #getNextInputBuffer(int)}. */
public boolean hasRemaining() {
return buffer.hasRemaining();
}
}
/** Builder for {@link ShortBuffer}s that contain 16-bit PCM audio samples. */
private static final class Pcm16BitAudioBuilder {
private final int channelCount;
private final ShortBuffer buffer;
private boolean built;
public Pcm16BitAudioBuilder(int channelCount, int frameCount) {
this.channelCount = channelCount;
buffer = ByteBuffer.allocate(frameCount * channelCount * 2).asShortBuffer();
}
/**
* Appends {@code count} audio frames, using the specified {@code channelLevels} in each frame.
*/
public void appendFrames(int count, short... channelLevels) {
Assertions.checkState(!built);
for (int i = 0; i < count; i += channelCount) {
for (short channelLevel : channelLevels) {
buffer.put(channelLevel);
}
}
}
/** Returns whether the buffer is full. */
public boolean isFull() {
Assertions.checkState(!built);
return !buffer.hasRemaining();
}
/** Returns the built buffer. After calling this method the builder should not be reused. */
public ShortBuffer build() {
Assertions.checkState(!built);
built = true;
buffer.flip();
return buffer;
}
}
}