summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java352
1 files changed, 352 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
new file mode 100644
index 0000000000..6a2c5ae9a6
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java
@@ -0,0 +1,352 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.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;
+
+/**
+ * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit
+ * PCM.
+ */
+public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor {
+
+ /**
+ * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
+ * that part of audio as silent, in microseconds.
+ */
+ private static final long MINIMUM_SILENCE_DURATION_US = 150_000;
+ /**
+ * The duration of silence by which to extend non-silent sections, in microseconds. The value must
+ * not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
+ */
+ private static final long PADDING_SILENCE_US = 20_000;
+ /**
+ * The absolute level below which an individual PCM sample is classified as silent. Note: the
+ * specified value will be rounded so that the threshold check only depends on the more
+ * significant byte, for efficiency.
+ */
+ private static final short SILENCE_THRESHOLD_LEVEL = 1024;
+
+ /**
+ * Threshold for classifying an individual PCM sample as silent based on its more significant
+ * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding.
+ */
+ private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8;
+
+ /** Trimming states. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_NOISY,
+ STATE_MAYBE_SILENT,
+ STATE_SILENT,
+ })
+ private @interface State {}
+ /** State when the input is not silent. */
+ private static final int STATE_NOISY = 0;
+ /** State when the input may be silent but we haven't read enough yet to know. */
+ private static final int STATE_MAYBE_SILENT = 1;
+ /** State when the input is silent. */
+ private static final int STATE_SILENT = 2;
+
+ private int bytesPerFrame;
+
+ private boolean enabled;
+
+ /**
+ * 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
+ * contents will be dropped and the state will transition to {@link #STATE_SILENT}.
+ */
+ private byte[] maybeSilenceBuffer;
+
+ /**
+ * Stores the latest part of the input while silent. It will be output as padding if the next
+ * input is noisy.
+ */
+ private byte[] paddingBuffer;
+
+ @State private int state;
+ private int maybeSilenceBufferSize;
+ private int paddingSize;
+ private boolean hasOutputNoise;
+ private long skippedFrames;
+
+ /** Creates a new silence trimming audio processor. */
+ public SilenceSkippingAudioProcessor() {
+ maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;
+ paddingBuffer = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ /**
+ * Sets whether to skip silence in the input. This method may only be called after draining data
+ * through the processor. The value returned by {@link #isActive()} may change, and the processor
+ * must be {@link #flush() flushed} before queueing more data.
+ *
+ * @param enabled Whether to skip silence in the input.
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Returns the total number of frames of input audio that were skipped due to being classified as
+ * silence since the last call to {@link #flush()}.
+ */
+ public long getSkippedFrames() {
+ return skippedFrames;
+ }
+
+ // AudioProcessor implementation.
+
+ @Override
+ public AudioFormat onConfigure(AudioFormat inputAudioFormat)
+ throws UnhandledAudioFormatException {
+ if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
+ throw new UnhandledAudioFormatException(inputAudioFormat);
+ }
+ return enabled ? inputAudioFormat : AudioFormat.NOT_SET;
+ }
+
+ @Override
+ public boolean isActive() {
+ return enabled;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ while (inputBuffer.hasRemaining() && !hasPendingOutput()) {
+ switch (state) {
+ case STATE_NOISY:
+ processNoisy(inputBuffer);
+ break;
+ case STATE_MAYBE_SILENT:
+ processMaybeSilence(inputBuffer);
+ break;
+ case STATE_SILENT:
+ processSilence(inputBuffer);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Override
+ 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);
+ }
+ if (!hasOutputNoise) {
+ skippedFrames += paddingSize / bytesPerFrame;
+ }
+ }
+
+ @Override
+ protected void onFlush() {
+ if (enabled) {
+ bytesPerFrame = inputAudioFormat.bytesPerFrame;
+ int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame;
+ if (maybeSilenceBuffer.length != maybeSilenceBufferSize) {
+ maybeSilenceBuffer = new byte[maybeSilenceBufferSize];
+ }
+ paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame;
+ if (paddingBuffer.length != paddingSize) {
+ paddingBuffer = new byte[paddingSize];
+ }
+ }
+ state = STATE_NOISY;
+ skippedFrames = 0;
+ maybeSilenceBufferSize = 0;
+ hasOutputNoise = false;
+ }
+
+ @Override
+ protected void onReset() {
+ enabled = false;
+ paddingSize = 0;
+ maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY;
+ paddingBuffer = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ // Internal methods.
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_NOISY},
+ * updating the state if needed.
+ */
+ private void processNoisy(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+
+ // Check if there's any noise within the maybe silence buffer duration.
+ inputBuffer.limit(Math.min(limit, inputBuffer.position() + maybeSilenceBuffer.length));
+ int noiseLimit = findNoiseLimit(inputBuffer);
+ if (noiseLimit == inputBuffer.position()) {
+ // The buffer contains the start of possible silence.
+ state = STATE_MAYBE_SILENT;
+ } else {
+ inputBuffer.limit(noiseLimit);
+ output(inputBuffer);
+ }
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link
+ * #STATE_MAYBE_SILENT}, updating the state if needed.
+ */
+ private void processMaybeSilence(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+ int noisePosition = findNoisePosition(inputBuffer);
+ int maybeSilenceInputSize = noisePosition - inputBuffer.position();
+ int maybeSilenceBufferRemaining = maybeSilenceBuffer.length - maybeSilenceBufferSize;
+ if (noisePosition < limit && maybeSilenceInputSize < maybeSilenceBufferRemaining) {
+ // The maybe silence buffer isn't full, so output it and switch back to the noisy state.
+ output(maybeSilenceBuffer, maybeSilenceBufferSize);
+ maybeSilenceBufferSize = 0;
+ state = STATE_NOISY;
+ } else {
+ // Fill as much of the maybe silence buffer as possible.
+ int bytesToWrite = Math.min(maybeSilenceInputSize, maybeSilenceBufferRemaining);
+ inputBuffer.limit(inputBuffer.position() + bytesToWrite);
+ inputBuffer.get(maybeSilenceBuffer, maybeSilenceBufferSize, bytesToWrite);
+ maybeSilenceBufferSize += bytesToWrite;
+ if (maybeSilenceBufferSize == maybeSilenceBuffer.length) {
+ // We've reached a period of silence, so skip it, taking in to account padding for both
+ // the noisy to silent transition and any future silent to noisy transition.
+ if (hasOutputNoise) {
+ output(maybeSilenceBuffer, paddingSize);
+ skippedFrames += (maybeSilenceBufferSize - paddingSize * 2) / bytesPerFrame;
+ } else {
+ skippedFrames += (maybeSilenceBufferSize - paddingSize) / bytesPerFrame;
+ }
+ updatePaddingBuffer(inputBuffer, maybeSilenceBuffer, maybeSilenceBufferSize);
+ maybeSilenceBufferSize = 0;
+ state = STATE_SILENT;
+ }
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+ }
+
+ /**
+ * Incrementally processes new input from {@code inputBuffer} while in {@link #STATE_SILENT},
+ * updating the state if needed.
+ */
+ private void processSilence(ByteBuffer inputBuffer) {
+ int limit = inputBuffer.limit();
+ int noisyPosition = findNoisePosition(inputBuffer);
+ inputBuffer.limit(noisyPosition);
+ skippedFrames += inputBuffer.remaining() / bytesPerFrame;
+ updatePaddingBuffer(inputBuffer, paddingBuffer, paddingSize);
+ if (noisyPosition < limit) {
+ // Output the padding, which may include previous input as well as new input, then transition
+ // back to the noisy state.
+ output(paddingBuffer, paddingSize);
+ state = STATE_NOISY;
+
+ // Restore the limit.
+ inputBuffer.limit(limit);
+ }
+ }
+
+ /**
+ * Copies {@code length} elements from {@code data} to populate a new output buffer from the
+ * processor.
+ */
+ private void output(byte[] data, int length) {
+ 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) {
+ int length = data.remaining();
+ replaceOutputBuffer(length).put(data).flip();
+ if (length > 0) {
+ hasOutputNoise = true;
+ }
+ }
+
+ /**
+ * Fills {@link #paddingBuffer} using data from {@code input}, plus any additional buffered data
+ * at the end of {@code buffer} (up to its {@code size}) required to fill it, advancing the input
+ * position.
+ */
+ private void updatePaddingBuffer(ByteBuffer input, byte[] buffer, int size) {
+ int fromInputSize = Math.min(input.remaining(), paddingSize);
+ int fromBufferSize = paddingSize - fromInputSize;
+ System.arraycopy(
+ /* src= */ buffer,
+ /* srcPos= */ size - fromBufferSize,
+ /* dest= */ paddingBuffer,
+ /* destPos= */ 0,
+ /* length= */ fromBufferSize);
+ input.position(input.limit() - fromInputSize);
+ input.get(paddingBuffer, fromBufferSize, fromInputSize);
+ }
+
+ /**
+ * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio.
+ */
+ private int durationUsToFrames(long durationUs) {
+ return (int) ((durationUs * inputAudioFormat.sampleRate) / C.MICROS_PER_SECOND);
+ }
+
+ /**
+ * Returns the earliest byte position in [position, limit) of {@code buffer} that contains a frame
+ * classified as a noisy frame, or the limit of the buffer if no such frame exists.
+ */
+ private int findNoisePosition(ByteBuffer buffer) {
+ // The input is in ByteOrder.nativeOrder(), which is little endian on Android.
+ for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) {
+ if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {
+ // Round to the start of the frame.
+ return bytesPerFrame * (i / bytesPerFrame);
+ }
+ }
+ return buffer.limit();
+ }
+
+ /**
+ * Returns the earliest byte position in [position, limit) of {@code buffer} such that all frames
+ * from the byte position to the limit are classified as silent.
+ */
+ private int findNoiseLimit(ByteBuffer buffer) {
+ // The input is in ByteOrder.nativeOrder(), which is little endian on Android.
+ for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) {
+ if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) {
+ // Return the start of the next frame.
+ return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame;
+ }
+ }
+ return buffer.position();
+ }
+}