From 7ecaebe3d62c096743a40d8c0efc7f44fce0dd0d Mon Sep 17 00:00:00 2001 From: ivanbuper Date: Fri, 3 Jan 2025 09:23:47 -0800 Subject: [PATCH] Fix underflow in Sonic#getOutputSize() after #queueEndOfStream() For the [0.5; 1) speed range, the combination of having a "slow down" speed (i.e. more output frames than input frames), and Sonic potentially needing to copy more input frames that are available in the input buffer can lead to an unexpected underflow. Specifically, the underflow happens in Sonic#queueEndOfStream() when the following conditions are met (skipping some minor ones): 1. `inputFrameCount < remainingInputToCopyFrameCount` 2. `0.5f <= speed < 1`. 3. `outputFrameCount < (inputFrameCount / remainingInputToCopyFrameCount) / 2`. This underflow caused `SonicAudioProcessor#isEnded()` to return a false negative (because `getOutputSize() != 0`), which would stall the `DefaultAudioSink` waiting for processing to end after EOS. In practical terms, the underflow is relatively easy to reproduce if we consume all of Sonic's output and then immediately queue EOS without queueing any more input in between. This should cause both `inputFrameCount` and `outputFrameCount` to drop to 0. PiperOrigin-RevId: 711773565 --- RELEASENOTES.md | 2 + .../androidx/media3/common/audio/Sonic.java | 6 +- .../audio/RandomParameterizedSonicTest.java | 4 +- .../common/audio/SonicAudioProcessorTest.java | 17 ++++ .../media3/common/audio/SonicTest.java | 48 ++++++++++ .../SpeedChangingAudioProcessorTest.java | 91 +++++++++++-------- .../androidx/media3/test/utils/TestUtil.java | 40 ++++++++ 7 files changed, 167 insertions(+), 41 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a5914577c3..f52a00afe4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Audio: * Do not bypass `SonicAudioProcessor` when `SpeedChangingAudioProcessor` is configured with default parameters. + * Fix underflow in `Sonic#getOutputSize()` that could cause + `DefaultAudioSink` to stall. * Video: * Text: * Metadata: diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/Sonic.java b/libraries/common/src/main/java/androidx/media3/common/audio/Sonic.java index a16913899c..a4e8189d87 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/Sonic.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/Sonic.java @@ -17,6 +17,7 @@ package androidx.media3.common.audio; import static androidx.media3.common.util.Assertions.checkState; +import static java.lang.Math.max; import static java.lang.Math.min; import java.math.BigDecimal; @@ -257,6 +258,7 @@ import java.util.Arrays; * @param buffer A {@link ShortBuffer} into which output will be written. */ public void getOutput(ShortBuffer buffer) { + checkState(outputFrameCount >= 0); int framesToRead = min(buffer.remaining() / channelCount, outputFrameCount); buffer.put(outputBuffer, 0, framesToRead * channelCount); outputFrameCount -= framesToRead; @@ -306,7 +308,8 @@ import java.util.Arrays; processStreamInput(); // Throw away any extra frames we generated due to the silence we added. if (outputFrameCount > expectedOutputFrames) { - outputFrameCount = expectedOutputFrames; + // expectedOutputFrames might be negative, so set lower bound to 0. + outputFrameCount = max(expectedOutputFrames, 0); } // Empty input and pitch buffers. inputFrameCount = 0; @@ -331,6 +334,7 @@ import java.util.Arrays; /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ public int getOutputSize() { + checkState(outputFrameCount >= 0); return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } diff --git a/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSonicTest.java b/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSonicTest.java index c2500b26ea..6560c0f6fe 100644 --- a/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSonicTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSonicTest.java @@ -160,8 +160,8 @@ public final class RandomParameterizedSonicTest { readSampleCount += outBuffer.position(); outBuffer.clear(); } + assertThat(sonic.getOutputSize()).isAtLeast(0); } - sonic.flush(); long expectedSamples = Sonic.getExpectedFrameCountAfterProcessorApplied( @@ -196,8 +196,8 @@ public final class RandomParameterizedSonicTest { readSampleCount += outBuffer.position(); outBuffer.clear(); } + assertThat(sonic.getOutputSize()).isAtLeast(0); } - sonic.flush(); long expectedSamples = Sonic.getExpectedFrameCountAfterProcessorApplied( diff --git a/libraries/common/src/test/java/androidx/media3/common/audio/SonicAudioProcessorTest.java b/libraries/common/src/test/java/androidx/media3/common/audio/SonicAudioProcessorTest.java index b5581a3a4b..74c14d4f6e 100644 --- a/libraries/common/src/test/java/androidx/media3/common/audio/SonicAudioProcessorTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/audio/SonicAudioProcessorTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.common.audio; +import static androidx.media3.test.utils.TestUtil.getPeriodicSamplesBuffer; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -99,6 +100,22 @@ public final class SonicAudioProcessorTest { assertThat(processor.isActive()).isTrue(); } + @Test + public void queueEndOfStream_withOutputFrameCountUnderflow_setsIsEndedToTrue() throws Exception { + sonicAudioProcessor.setSpeed(0.95f); + sonicAudioProcessor.configure(AUDIO_FORMAT_48000_HZ); + sonicAudioProcessor.flush(); + + // Multiply by channel count. + sonicAudioProcessor.queueInput( + getPeriodicSamplesBuffer(/* sampleCount= */ 1700 * 2, /* period= */ 192 * 2)); + // Drain output, so that pending output frame count is 0. + assertThat(sonicAudioProcessor.getOutput().hasRemaining()).isTrue(); + sonicAudioProcessor.queueEndOfStream(); + + assertThat(sonicAudioProcessor.isEnded()).isTrue(); + } + @Test public void doesNotSupportNon16BitInput() throws Exception { try { diff --git a/libraries/common/src/test/java/androidx/media3/common/audio/SonicTest.java b/libraries/common/src/test/java/androidx/media3/common/audio/SonicTest.java index 3855111249..ff5ac63f5c 100644 --- a/libraries/common/src/test/java/androidx/media3/common/audio/SonicTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/audio/SonicTest.java @@ -18,6 +18,7 @@ package androidx.media3.common.audio; import static androidx.media3.common.audio.Sonic.calculateAccumulatedTruncationErrorForResampling; import static androidx.media3.common.audio.Sonic.getExpectedFrameCountAfterProcessorApplied; import static androidx.media3.common.audio.Sonic.getExpectedInputFrameCountForOutputFrameCount; +import static androidx.media3.test.utils.TestUtil.getPeriodicSamplesBuffer; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -112,6 +113,53 @@ public class SonicTest { assertThat(outputBuffer.array()).isEqualTo(new short[] {0, 4, 8}); } + @Test + public void queueEndOfStream_withOutputCountUnderflow_setsNonNegativeOutputSize() { + // For speed ranges [0.5; 1) and (1; 1.5], Sonic might need to copy more input frames onto its + // output buffer than are available in the input buffer. Sonic keeps track of this "borrowed + // frames" number in #remainingInputToCopyFrameCount. When we call #queueEndOfStream(), then + // Sonic outputs a final number of frames based roughly on pendingOutputFrameCount + + // (inputFrameCount - remainingInputToCopyFrameCount) / speed + remainingInputToCopyFrameCount, + // which could result in a negative number if inputFrameCount < remainingInputToCopyFrameCount + // and 0.5 <= speed < 1. #getOutputSize() should still always return a non-negative number. + ShortBuffer inputBuffer = + getPeriodicSamplesBuffer(/* sampleCount= */ 1700, /* period= */ 192).asShortBuffer(); + Sonic sonic = + new Sonic( + /* inputSampleRateHz= */ 48000, + /* channelCount= */ 1, + /* speed= */ 0.95f, + /* pitch= */ 1, + /* outputSampleRateHz= */ 48000); + + sonic.queueInput(inputBuffer); + ShortBuffer outputBuffer = ShortBuffer.allocate(sonic.getOutputSize() / 2); + // Drain output, so that pending output frame count is 0. + sonic.getOutput(outputBuffer); + assertThat(sonic.getOutputSize()).isEqualTo(0); + // Queue EOS with empty pending input and output. + sonic.queueEndOfStream(); + + assertThat(sonic.getOutputSize()).isEqualTo(0); + } + + @Test + public void queueEndOfStream_withNoInput_setsNonNegativeOutputSize() { + Sonic sonic = + new Sonic( + /* inputSampleRateHz= */ 48000, + /* channelCount= */ 1, + /* speed= */ 0.95f, + /* pitch= */ 1, + /* outputSampleRateHz= */ 48000); + ShortBuffer outputBuffer = ShortBuffer.allocate(sonic.getOutputSize() / 2); + + sonic.getOutput(outputBuffer); + sonic.queueEndOfStream(); + + assertThat(sonic.getOutputSize()).isAtLeast(0); + } + @Test public void getExpectedFrameCountAfterProcessorApplied_timeStretchingFaster_returnsExpectedSampleCount() { diff --git a/libraries/common/src/test/java/androidx/media3/common/audio/SpeedChangingAudioProcessorTest.java b/libraries/common/src/test/java/androidx/media3/common/audio/SpeedChangingAudioProcessorTest.java index 1e1bf0560b..b116aa29d8 100644 --- a/libraries/common/src/test/java/androidx/media3/common/audio/SpeedChangingAudioProcessorTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/audio/SpeedChangingAudioProcessorTest.java @@ -17,6 +17,7 @@ package androidx.media3.common.audio; import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.test.utils.TestUtil.getNonRandomByteBuffer; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -46,12 +47,14 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); - assertThat(inputBuffer).isEqualTo(getInputBuffer(/* frameCount= */ 5)); + assertThat(inputBuffer) + .isEqualTo(getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame)); } @Test @@ -61,12 +64,14 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); - assertThat(inputBuffer).isEqualTo(getInputBuffer(/* frameCount= */ 5)); + assertThat(inputBuffer) + .isEqualTo(getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame)); } @Test @@ -76,7 +81,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -93,7 +99,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -111,7 +118,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -131,7 +139,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -159,7 +168,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {3, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -187,7 +197,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 3}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); ByteBuffer outputBuffer = getAudioProcessorOutput(speedChangingAudioProcessor); @@ -214,7 +225,8 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); int inputBufferLimit = inputBuffer.limit(); speedChangingAudioProcessor.queueInput(inputBuffer); @@ -233,7 +245,8 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); int inputBufferLimit = inputBuffer.limit(); speedChangingAudioProcessor.queueInput(inputBuffer); @@ -252,7 +265,8 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); // SpeedChangingAudioProcessor only queues samples until the next speed change. while (inputBuffer.hasRemaining()) { @@ -276,7 +290,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -295,7 +310,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -314,7 +330,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -330,7 +347,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -358,7 +376,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); @@ -373,7 +392,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); @@ -390,7 +410,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( /* inputTimeUs= */ 50L, outputTimesUs::add); @@ -421,7 +442,8 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {2, 1, 3}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); // Use the audio processor before a flush speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); @@ -458,7 +480,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 3); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 3, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( /* inputTimeUs= */ 300L, outputTimesUs::add); @@ -487,7 +510,8 @@ public class SpeedChangingAudioProcessorTest { AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); inputBuffer.rewind(); @@ -513,7 +537,8 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {2, 1, 5, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 441 * 4); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 441 * 4, AUDIO_FORMAT.bytesPerFrame); while (inputBuffer.position() < inputBuffer.limit()) { speedChangingAudioProcessor.queueInput(inputBuffer); } @@ -552,7 +577,7 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {2, 4, 2}); // 500, 250, 500 = 1250 SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer input = getInputBuffer(1000); + ByteBuffer input = getNonRandomByteBuffer(1000, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(input); outputFrameCount += @@ -587,7 +612,7 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {2, 3, 8, 4}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer input = getInputBuffer(12); + ByteBuffer input = getNonRandomByteBuffer(12, AUDIO_FORMAT.bytesPerFrame); while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); @@ -614,7 +639,7 @@ public class SpeedChangingAudioProcessorTest { SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); // 1500 input frames falls in the middle of the 2x region. - ByteBuffer input = getInputBuffer(1500); + ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT.bytesPerFrame); int outputFrameCount = 0; while (input.hasRemaining()) { @@ -653,7 +678,7 @@ public class SpeedChangingAudioProcessorTest { SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); // 1500 input frames falls in the middle of the 2x region. - ByteBuffer input = getInputBuffer(1500); + ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT.bytesPerFrame); int outputFrameCount = 0; while (input.hasRemaining()) { @@ -810,16 +835,6 @@ public class SpeedChangingAudioProcessorTest { return speedChangingAudioProcessor; } - private static ByteBuffer getInputBuffer(int frameCount) { - int bufferSize = frameCount * AUDIO_FORMAT.bytesPerFrame; - ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder()); - for (int i = 0; i < bufferSize; i++) { - buffer.put((byte) (i % (Byte.MAX_VALUE + 1))); - } - buffer.rewind(); - return buffer; - } - private static ByteBuffer getAudioProcessorOutput(AudioProcessor audioProcessor) { ByteBuffer concatenatedOutputBuffers = EMPTY_BUFFER; while (true) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java index 39326de156..bdc511ecec 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestUtil.java @@ -890,6 +890,46 @@ public class TestUtil { return (long) (origin + random.nextFloat() * (bound - origin)); } + /** + * Returns a non-random {@link ByteBuffer} filled with {@code frameCount * bytesPerFrame} bytes. + */ + public static ByteBuffer getNonRandomByteBuffer(int frameCount, int bytesPerFrame) { + int bufferSize = frameCount * bytesPerFrame; + ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder()); + for (int i = 0; i < bufferSize; i++) { + buffer.put((byte) i); + } + buffer.rewind(); + return buffer; + } + + /** + * Returns a {@link ByteBuffer} filled with alternating 16-bit PCM samples as per the provided + * period length. + * + *

The generated samples alternate between {@link Short#MAX_VALUE} and {@link Short#MIN_VALUE} + * every {@code period / 2} samples. + * + * @param sampleCount Number of total PCM samples (not frames) to generate. + * @param period Length in PCM samples of one full cycle. + */ + public static ByteBuffer getPeriodicSamplesBuffer(int sampleCount, int period) { + int halfPeriod = period / 2; + ByteBuffer buffer = ByteBuffer.allocateDirect(sampleCount * 2).order(ByteOrder.nativeOrder()); + boolean isHigh = false; + int counter = 0; + while (counter < sampleCount) { + short sample = isHigh ? Short.MAX_VALUE : Short.MIN_VALUE; + for (int i = 0; i < halfPeriod && counter < sampleCount; i++) { + buffer.putShort(sample); + counter++; + } + isHigh = !isHigh; + } + buffer.rewind(); + return buffer; + } + private static final class NoUidOrShufflingTimeline extends Timeline { private final Timeline delegate;