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 d2a0e68766..d194bade2a 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 @@ -17,6 +17,9 @@ package androidx.media3.common.audio; import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.SpeedProviderUtil.getNextSpeedChangeSamplePosition; +import static androidx.media3.common.util.SpeedProviderUtil.getSampleAlignedSpeed; import static androidx.media3.common.util.Util.sampleCountToDurationUs; import static java.lang.Math.min; import static java.lang.Math.round; @@ -116,39 +119,17 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor { @Override public void queueInput(ByteBuffer inputBuffer) { long currentTimeUs = sampleCountToDurationUs(framesRead, inputAudioFormat.sampleRate); - float newSpeed = speedProvider.getSpeed(currentTimeUs); - long nextSpeedChangeTimeUs = speedProvider.getNextSpeedChangeTimeUs(currentTimeUs); - long sampleRateAlignedNextSpeedChangeTimeUs = - getSampleRateAlignedTimestamp(nextSpeedChangeTimeUs, inputAudioFormat.sampleRate); - - // If next speed change falls between the current sample position and the next sample, then get - // the next speed and next speed change from the following sample. If needed, this will ignore - // one or more mid-sample speed changes. - if (sampleRateAlignedNextSpeedChangeTimeUs == currentTimeUs) { - long sampleDuration = - sampleCountToDurationUs(/* sampleCount= */ 1, inputAudioFormat.sampleRate); - newSpeed = speedProvider.getSpeed(currentTimeUs + sampleDuration); - nextSpeedChangeTimeUs = - speedProvider.getNextSpeedChangeTimeUs(currentTimeUs + sampleDuration); - } + float newSpeed = getSampleAlignedSpeed(speedProvider, framesRead, inputAudioFormat.sampleRate); + long nextSpeedChangeSamplePosition = + getNextSpeedChangeSamplePosition(speedProvider, framesRead, inputAudioFormat.sampleRate); updateSpeed(newSpeed, currentTimeUs); int inputBufferLimit = inputBuffer.limit(); int bytesToNextSpeedChange; - if (nextSpeedChangeTimeUs != C.TIME_UNSET) { + if (nextSpeedChangeSamplePosition != C.INDEX_UNSET) { bytesToNextSpeedChange = - (int) - Util.scaleLargeTimestamp( - /* timestamp= */ nextSpeedChangeTimeUs - currentTimeUs, - /* multiplier= */ (long) inputAudioFormat.sampleRate - * inputAudioFormat.bytesPerFrame, - /* divisor= */ C.MICROS_PER_SECOND); - int bytesToNextFrame = - inputAudioFormat.bytesPerFrame - bytesToNextSpeedChange % inputAudioFormat.bytesPerFrame; - if (bytesToNextFrame != inputAudioFormat.bytesPerFrame) { - bytesToNextSpeedChange += bytesToNextFrame; - } + (int) ((nextSpeedChangeSamplePosition - framesRead) * inputAudioFormat.bytesPerFrame); // Update the input buffer limit to make sure that all samples processed have the same speed. inputBuffer.limit(min(inputBufferLimit, inputBuffer.position() + bytesToNextSpeedChange)); } else { @@ -170,7 +151,10 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor { } buffer.flip(); } - framesRead += (inputBuffer.position() - startPosition) / inputAudioFormat.bytesPerFrame; + long bytesRead = inputBuffer.position() - startPosition; + checkState( + bytesRead % inputAudioFormat.bytesPerFrame == 0, "A frame was not queued completely."); + framesRead += bytesRead / inputAudioFormat.bytesPerFrame; updateLastProcessedInputTime(); inputBuffer.limit(inputBufferLimit); } @@ -414,15 +398,4 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor { // because some clients register callbacks with getSpeedAdjustedTimeAsync before this audio // processor is flushed. } - - /** - * Returns the timestamp in microseconds of the sample defined by {@code sampleRate} that is - * closest to {@code timestampUs}, using the rounding mode specified in {@link - * Util#scaleLargeTimestamp}. - */ - private static long getSampleRateAlignedTimestamp(long timestampUs, int sampleRate) { - long exactSamplePosition = - Util.scaleLargeTimestamp(timestampUs, sampleRate, C.MICROS_PER_SECOND); - return Util.scaleLargeTimestamp(exactSamplePosition, C.MICROS_PER_SECOND, sampleRate); - } } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java index ac6ea31233..a48394ed31 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java @@ -15,6 +15,9 @@ */ package androidx.media3.common.util; +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Util.durationUsToSampleCount; +import static androidx.media3.common.util.Util.sampleCountToDurationUs; import static java.lang.Math.floor; import static java.lang.Math.min; @@ -48,4 +51,42 @@ public class SpeedProviderUtil { // Use floor to be consistent with Util#scaleLargeTimestamp(). return (long) floor(outputDurationUs); } + + /** + * Returns the speed at the specified sample position. + * + *
This method is consistent with the alignment done by {@link + * #getNextSpeedChangeSamplePosition}. + */ + public static float getSampleAlignedSpeed( + SpeedProvider speedProvider, long samplePosition, int sampleRate) { + checkArgument(samplePosition >= 0); + checkArgument(sampleRate > 0); + + long durationUs = sampleCountToDurationUs(samplePosition, sampleRate); + return speedProvider.getSpeed(durationUs); + } + + /** + * Returns the sample position of the next speed change or {@link C#INDEX_UNSET} if none is set. + * + *
If the next speed change falls between sample boundaries, this method will return the next + * closest sample position, which ensures that speed regions stay consistent with {@link + * #getSampleAlignedSpeed}. + */ + public static long getNextSpeedChangeSamplePosition( + SpeedProvider speedProvider, long samplePosition, int sampleRate) { + checkArgument(samplePosition >= 0); + checkArgument(sampleRate > 0); + + long durationUs = sampleCountToDurationUs(samplePosition, sampleRate); + long nextSpeedChangeTimeUs = speedProvider.getNextSpeedChangeTimeUs(durationUs); + + if (nextSpeedChangeTimeUs == C.TIME_UNSET) { + return C.INDEX_UNSET; + } + + // Use RoundingMode#UP to return next closest sample if duration falls between samples. + return durationUsToSampleCount(nextSpeedChangeTimeUs, sampleRate); + } } 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 c9ba087d7a..785dfad9a2 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 @@ -251,13 +251,18 @@ public class SpeedChangingAudioProcessorTest { getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); - speedChangingAudioProcessor.queueInput(inputBuffer); - outputFrames += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; + // SpeedChangingAudioProcessor only queues samples until the next speed change. + while (inputBuffer.hasRemaining()) { + speedChangingAudioProcessor.queueInput(inputBuffer); + outputFrames += + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; + } + speedChangingAudioProcessor.queueEndOfStream(); outputFrames += speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; - assertThat(outputFrames).isEqualTo(3); + // We allow 1 sample of tolerance per speed change. + assertThat(outputFrames).isWithin(1).of(3); } @Test diff --git a/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java index 749b4f87c7..c9b44e0d47 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java @@ -16,8 +16,11 @@ package androidx.media3.common.util; import static androidx.media3.common.util.SpeedProviderUtil.getDurationAfterSpeedProviderApplied; +import static androidx.media3.common.util.SpeedProviderUtil.getNextSpeedChangeSamplePosition; +import static androidx.media3.common.util.SpeedProviderUtil.getSampleAlignedSpeed; import static com.google.common.truth.Truth.assertThat; +import androidx.media3.common.C; import androidx.media3.common.audio.SpeedProvider; import androidx.media3.test.utils.TestSpeedProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -48,4 +51,153 @@ public class SpeedProviderUtilTest { assertThat(getDurationAfterSpeedProviderApplied(speedProvider, /* durationUs= */ 113)) .isEqualTo(56); } + + @Test + public void getSampleAlignedSpeed_withExactBoundaries_returnsExpectedSpeed() { + // 50Khz = 20us period. + int sampleRate = 50000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 20, 40, 60}, /* speeds= */ new float[] {2, 1, 5, 8}); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(2f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 1, sampleRate)) + .isEqualTo(1f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 2, sampleRate)) + .isEqualTo(5f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 3, sampleRate)) + .isEqualTo(8f); + } + + @Test + public void getSampleAlignedSpeed_beyondLastSpeedChange_returnsLastSetSpeed() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 20}, /* speeds= */ new float[] {2f, 0.5f}); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 2, sampleRate)) + .isEqualTo(0.5f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 500, sampleRate)) + .isEqualTo(0.5f); + } + + @Test + public void getSampleAlignedSpeed_withNonAlignedBoundaries_returnsAlignedSpeed() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 35, 62}, /* speeds= */ new float[] {2, 8, 10}); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(2f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 1, sampleRate)) + .isEqualTo(2f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 2, sampleRate)) + .isEqualTo(8f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 3, sampleRate)) + .isEqualTo(10f); + } + + @Test + public void + getSampleAlignedSpeed_withMultipleBoundariesBetweenSamples_ignoresIntermediateChanges() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 20, 25, 30, 35, 40}, + /* speeds= */ new float[] {2, 0.5f, 20, 5, 3, 9}); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(2f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 1, sampleRate)) + .isEqualTo(0.5f); + + assertThat(getSampleAlignedSpeed(speedProvider, /* samplePosition= */ 2, sampleRate)) + .isEqualTo(9f); + } + + @Test + public void getNextSpeedChangeSamplePosition_withExactBoundaries_returnsExpectedPositions() { + // 50Khz = 20us period. + int sampleRate = 50000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 20, 40, 60}, /* speeds= */ new float[] {2, 1, 5, 8}); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(1); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 1, sampleRate)) + .isEqualTo(2); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 2, sampleRate)) + .isEqualTo(3); + } + + @Test + public void getNextSpeedChangeSamplePosition_beyondLastChange_returnsIndexUnset() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 20, 41, 62}, /* speeds= */ new float[] {2, 1, 5, 8}); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 3, sampleRate)) + .isEqualTo(C.INDEX_UNSET); + } + + @Test + public void getNextSpeedChangeSamplePosition_withNonAlignedBoundaries_returnsNextClosestSample() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 50}, /* speeds= */ new float[] {2, 1}); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(3); + } + + @Test + public void + getNextSpeedChangeSamplePosition_withMultipleBoundariesBetweenSamples_ignoresIntermediateChanges() { + // 48Khz = 20.83us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 45, 55, 58}, + /* speeds= */ new float[] {2, 3, 0.1f, 9}); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(3); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 3, sampleRate)) + .isEqualTo(C.INDEX_UNSET); + } + + @Test + public void + getNextSpeedChangeSamplePosition_withChangeOneUsAfterBoundary_returnsNextClosestSample() { + // 48Khz = 20.8us period. + int sampleRate = 48000; + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 63}, /* speeds= */ new float[] {2, 3}); + + assertThat(getNextSpeedChangeSamplePosition(speedProvider, /* samplePosition= */ 0, sampleRate)) + .isEqualTo(4); + } }