From 36a5a83b224cf22c2032ac5e3302ce517cf48b21 Mon Sep 17 00:00:00 2001 From: ivanbuper Date: Tue, 22 Oct 2024 09:23:47 -0700 Subject: [PATCH] Implement parameterized testing for SpeedChangingAudioProcessor `RandomParameterizedSpeedChangingAudioProcessorTest` follows the structure of `RandomParameterizedSonicAudioProcessorTest` and will help improve coverage and confidence in the output length of `SpeedChangingAudioProcessor`. This CL is prework for removing the synchronization between the video pipeline and the audio pipeline for speed changing effects. PiperOrigin-RevId: 688578597 --- ...erizedSpeedChangingAudioProcessorTest.java | 159 ++++++++++++++++++ .../androidx/media3/test/utils/TestUtil.java | 8 + 2 files changed, 167 insertions(+) create mode 100644 libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java diff --git a/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java b/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java new file mode 100644 index 0000000000..5a4f8e2074 --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2024 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 androidx.media3.common.audio; + +import static androidx.media3.common.audio.SonicTestingUtils.calculateAccumulatedTruncationErrorForResampling; +import static androidx.media3.test.utils.TestUtil.buildTestData; +import static androidx.media3.test.utils.TestUtil.generateFloatInRange; +import static androidx.media3.test.utils.TestUtil.generateLong; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.audio.AudioProcessor.AudioFormat; +import androidx.media3.test.utils.TestSpeedProvider; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.google.common.primitives.Floats; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; + +/** Parameterized Robolectric test for {@link SpeedChangingAudioProcessor}. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public class RandomParameterizedSpeedChangingAudioProcessorTest { + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat(/* sampleRate= */ 48000, /* channelCount= */ 1, C.ENCODING_PCM_16BIT); + private static final int ITERATION_COUNT = 25; + private static final int MAX_SPEED_COUNT = 10; + private static final int SPEED_DECIMAL_PRECISION = 2; + private static final int MAX_FRAME_COUNT = 96000; // 2 mins. + private static final int BUFFER_SIZE = 8092; + + private static final ImmutableList> SPEED_RANGES = + ImmutableList.of( + Range.closedOpen(0.01f, 0.5f), + Range.closedOpen(0.5f, 1f), + Range.closedOpen(1f, 2f), + Range.closedOpen(2f, 20f)); + + private static final Random random = new Random(/* seed */ 0); + + private static final ImmutableList params = initParams(); + + @ParameterizedRobolectricTestRunner.Parameters(name = "speeds={0}, frameDurations={1}") + public static ImmutableList params() { + // params() is called multiple times, so return cached parameters to avoid regenerating + // different random parameter values. + return params; + } + + private static ImmutableList initParams() { + ImmutableList.Builder paramsBuilder = new ImmutableList.Builder<>(); + + for (int i = 0; i < ITERATION_COUNT; i++) { + int changeCount = + (int) generateLong(random, /* origin= */ 2, /* bound= */ MAX_SPEED_COUNT + 1); + ImmutableList.Builder speeds = new ImmutableList.Builder<>(); + ImmutableList.Builder frameCounts = new ImmutableList.Builder<>(); + + for (int j = 0; j < changeCount; j++) { + Range r = SPEED_RANGES.get(j % SPEED_RANGES.size()); + float speed = generateFloatInRange(random, r); + speeds.add( + BigDecimal.valueOf(speed).setScale(SPEED_DECIMAL_PRECISION, RoundingMode.HALF_EVEN)); + frameCounts.add((int) generateLong(random, /* origin= */ 1, /* bound= */ MAX_FRAME_COUNT)); + } + + paramsBuilder.add(new Object[] {speeds.build(), frameCounts.build()}); + } + + return paramsBuilder.build(); + } + + @Parameter(0) + public List speeds; + + @Parameter(1) + public List frameCounts; + + @Test + public void process_withResampling_outputsExpectedFrameCount() + throws AudioProcessor.UnhandledAudioFormatException { + ByteBuffer inputBuffer = + ByteBuffer.wrap( + buildTestData(/* length= */ BUFFER_SIZE * AUDIO_FORMAT.bytesPerFrame, random)); + ByteBuffer outBuffer; + BigDecimal expectedTotalOutputFrameCount = BigDecimal.ZERO; + long outputFrameCount = 0; + long totalInputFrameCount = 0; + long expectedResamplingError = 0; + + for (int i = 0; i < frameCounts.size(); i++) { + totalInputFrameCount += frameCounts.get(i); + BigDecimal frameCount = BigDecimal.valueOf(frameCounts.get(i)); + BigDecimal speed = speeds.get(i); + BigDecimal expectedOutputFrameCountForSection = + frameCount.divide(speed, RoundingMode.HALF_EVEN); + expectedTotalOutputFrameCount = + expectedTotalOutputFrameCount.add(expectedOutputFrameCountForSection); + // SpeedChangingAudioProcessor currently uses resampling on Sonic, instead of time-stretching. + // See b/359649531. + expectedResamplingError += + calculateAccumulatedTruncationErrorForResampling( + frameCount, BigDecimal.valueOf(AUDIO_FORMAT.sampleRate), speed); + } + + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ frameCounts.stream().mapToInt(Math::toIntExact).toArray(), + /* speeds= */ Floats.toArray(speeds)); + + SpeedChangingAudioProcessor speedChangingAudioProcessor = + new SpeedChangingAudioProcessor(speedProvider); + speedChangingAudioProcessor.configure(AUDIO_FORMAT); + speedChangingAudioProcessor.flush(); + + while (totalInputFrameCount > 0) { + // To input exact number of bytes, set limit to input buffer. + if (totalInputFrameCount < BUFFER_SIZE) { + inputBuffer.limit((int) totalInputFrameCount * AUDIO_FORMAT.bytesPerFrame); + } + + speedChangingAudioProcessor.queueInput(inputBuffer); + totalInputFrameCount -= inputBuffer.position() / AUDIO_FORMAT.bytesPerFrame; + + outBuffer = speedChangingAudioProcessor.getOutput(); + outputFrameCount += outBuffer.remaining() / AUDIO_FORMAT.bytesPerFrame; + + inputBuffer.rewind(); + } + speedChangingAudioProcessor.queueEndOfStream(); + outBuffer = speedChangingAudioProcessor.getOutput(); + outputFrameCount += outBuffer.remaining() / AUDIO_FORMAT.bytesPerFrame; + + // We allow 1 frame of tolerance per speed change. + assertThat(outputFrameCount) + .isWithin(frameCounts.size()) + .of(expectedTotalOutputFrameCount.longValueExact() - expectedResamplingError); + } +} 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 e3c42b555e..9608b662bd 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 @@ -684,6 +684,14 @@ public class TestUtil { return bottom + random.nextFloat() * (top - bottom); } + /** + * Returns a long between {@code origin} (inclusive) and {@code bound} (exclusive), given {@code + * random}. + */ + public static long generateLong(Random random, long origin, long bound) { + return (long) (origin + random.nextFloat() * (bound - origin)); + } + private static final class NoUidOrShufflingTimeline extends Timeline { private final Timeline delegate;