diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java index d194bade2a..be20fa8024 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java @@ -25,6 +25,7 @@ import static java.lang.Math.min; import static java.lang.Math.round; import androidx.annotation.GuardedBy; +import androidx.annotation.IntRange; import androidx.media3.common.C; import androidx.media3.common.util.LongArray; import androidx.media3.common.util.LongArrayQueue; @@ -105,6 +106,42 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor { resetState(); } + /** Returns the estimated number of samples output given the provided parameters. */ + public static long getSampleCountAfterProcessorApplied( + SpeedProvider speedProvider, + @IntRange(from = 1) int inputSampleRateHz, + @IntRange(from = 1) long inputSamples) { + checkArgument(speedProvider != null); + checkArgument(inputSampleRateHz > 0); + checkArgument(inputSamples > 0); + + long outputSamples = 0; + long positionSamples = 0; + + while (positionSamples < inputSamples) { + long boundarySamples = + getNextSpeedChangeSamplePosition(speedProvider, positionSamples, inputSampleRateHz); + + if (boundarySamples == C.INDEX_UNSET || boundarySamples > inputSamples) { + boundarySamples = inputSamples; + } + + float speed = getSampleAlignedSpeed(speedProvider, positionSamples, inputSampleRateHz); + // Input and output sample rates match because SpeedChangingAudioProcessor does not modify the + // output sample rate. + outputSamples += + Sonic.getExpectedFrameCountAfterProcessorApplied( + /* inputSampleRateHz= */ inputSampleRateHz, + /* outputSampleRateHz= */ inputSampleRateHz, + /* speed= */ speed, + /* pitch= */ speed, + /* inputFrameCount= */ boundarySamples - positionSamples); + positionSamples = boundarySamples; + } + + return outputSamples; + } + @Override public long getDurationAfterProcessorApplied(long durationUs) { return SpeedProviderUtil.getDurationAfterSpeedProviderApplied(speedProvider, durationUs); 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 index 51fc65055a..f6a3b7fcc5 100644 --- a/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/audio/RandomParameterizedSpeedChangingAudioProcessorTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.common.audio; +import static androidx.media3.common.audio.SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied; import static androidx.media3.test.utils.TestUtil.buildTestData; import static androidx.media3.test.utils.TestUtil.generateFloatInRange; import static androidx.media3.test.utils.TestUtil.generateLong; @@ -104,18 +105,9 @@ public class RandomParameterizedSpeedChangingAudioProcessorTest { ByteBuffer outBuffer; long outputFrameCount = 0; long totalInputFrameCount = 0; - long expectedOutputFrames = 0; for (int i = 0; i < frameCounts.size(); i++) { totalInputFrameCount += frameCounts.get(i); - float speed = speeds.get(i).floatValue(); - expectedOutputFrames += - Sonic.getExpectedFrameCountAfterProcessorApplied( - /* inputSampleRateHz= */ AUDIO_FORMAT.sampleRate, - /* outputSampleRateHz= */ AUDIO_FORMAT.sampleRate, - /* speed= */ speed, - /* pitch= */ speed, - /* inputFrameCount= */ frameCounts.get(i)); } SpeedProvider speedProvider = @@ -124,6 +116,10 @@ public class RandomParameterizedSpeedChangingAudioProcessorTest { /* frameCounts= */ Ints.toArray(frameCounts), /* speeds= */ Floats.toArray(speeds)); + long expectedOutputFrames = + getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, totalInputFrameCount); + SpeedChangingAudioProcessor speedChangingAudioProcessor = new SpeedChangingAudioProcessor(speedProvider); speedChangingAudioProcessor.configure(AUDIO_FORMAT); 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 785dfad9a2..c95aff64bd 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 @@ -18,8 +18,11 @@ package androidx.media3.common.audio; import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER; import static androidx.media3.common.util.Assertions.checkState; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import android.annotation.SuppressLint; import androidx.media3.common.C; +import androidx.media3.common.audio.AudioProcessor.AudioFormat; import androidx.media3.test.utils.TestSpeedProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.nio.ByteBuffer; @@ -32,8 +35,8 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class SpeedChangingAudioProcessorTest { - private static final AudioProcessor.AudioFormat AUDIO_FORMAT = - new AudioProcessor.AudioFormat( + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat( /* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); @Test @@ -600,6 +603,104 @@ public class SpeedChangingAudioProcessorTest { assertThat(outputFrameCount).isWithin(1).of(4); } + @Test + public void getSampleCountAfterProcessorApplied_withConstantSpeed_outputsExpectedSamples() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + new AudioFormat(/* sampleRate= */ 48000, /* channelCount= */ 1, C.ENCODING_PCM_16BIT), + /* frameCounts= */ new int[] {100}, + /* speeds= */ new float[] {2.f}); + + long sampleCountAfterProcessorApplied = + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 100); + assertThat(sampleCountAfterProcessorApplied).isEqualTo(50); + } + + @Test + public void getSampleCountAfterProcessorApplied_withMultipleSpeeds_outputsExpectedSamples() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ new int[] {100, 400, 50}, + /* speeds= */ new float[] {2.f, 4f, 0.5f}); + + long sampleCountAfterProcessorApplied = + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 550); + assertThat(sampleCountAfterProcessorApplied).isEqualTo(250); + } + + @Test + public void + getSampleCountAfterProcessorApplied_beyondLastSpeedRegion_stillAppliesLastSpeedValue() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ new int[] {100, 400, 50}, + /* speeds= */ new float[] {2.f, 4f, 0.5f}); + + long sampleCountAfterProcessorApplied = + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3000); + assertThat(sampleCountAfterProcessorApplied).isEqualTo(5150); + } + + @Test + public void + getSampleCountAfterProcessorApplied_withInputCountBeyondIntRange_outputsExpectedSamples() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ new int[] {1000, 10000, 8200}, + /* speeds= */ new float[] {0.2f, 8f, 0.5f}); + long sampleCountAfterProcessorApplied = + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3_000_000_000L); + assertThat(sampleCountAfterProcessorApplied).isEqualTo(5999984250L); + } + + // Testing range validation. + @SuppressLint("Range") + @Test + public void getSampleCountAfterProcessorApplied_withNegativeSampleCount_throws() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ new int[] {1000, 10000, 8200}, + /* speeds= */ new float[] {0.2f, 8f, 0.5f}); + assertThrows( + IllegalArgumentException.class, + () -> + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ -2L)); + } + + // Testing range validation. + @SuppressLint("Range") + @Test + public void getSampleCountAfterProcessorApplied_withZeroSampleRate_throws() { + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, + /* frameCounts= */ new int[] {1000, 10000, 8200}, + /* speeds= */ new float[] {0.2f, 8f, 0.5f}); + assertThrows( + IllegalArgumentException.class, + () -> + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + speedProvider, /* inputSampleRateHz= */ 0, /* inputSamples= */ 1000L)); + } + + @Test + public void getSampleCountAfterProcessorApplied_withNullSpeedProvider_throws() { + assertThrows( + IllegalArgumentException.class, + () -> + SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( + /* speedProvider= */ null, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 1000L)); + } + private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor( SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException { SpeedChangingAudioProcessor speedChangingAudioProcessor =