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 0a33ac9177..83e9cf598e 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 @@ -21,24 +21,25 @@ 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 androidx.media3.common.util.Util.scaleLargeValue; import static java.lang.Math.min; +import static java.lang.Math.round; import androidx.annotation.GuardedBy; import androidx.annotation.IntRange; -import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; -import androidx.media3.common.Format; +import androidx.media3.common.util.LongArray; import androidx.media3.common.util.LongArrayQueue; import androidx.media3.common.util.SpeedProviderUtil; import androidx.media3.common.util.TimestampConsumer; import androidx.media3.common.util.UnstableApi; -import java.math.RoundingMode; +import androidx.media3.common.util.Util; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.Queue; import java.util.function.LongConsumer; import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * An {@link AudioProcessor} that changes the speed of audio samples depending on their timestamp. @@ -66,12 +67,34 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { @GuardedBy("lock") private final Queue pendingCallbacks; + // Elements in the same positions in the arrays are associated. + + @GuardedBy("lock") + private LongArray inputSegmentStartTimesUs; + + @GuardedBy("lock") + private LongArray outputSegmentStartTimesUs; + + @GuardedBy("lock") + private long lastProcessedInputTimeUs; + + @GuardedBy("lock") + private long lastSpeedAdjustedInputTimeUs; + + @GuardedBy("lock") + private long lastSpeedAdjustedOutputTimeUs; + + @GuardedBy("lock") + private long speedAdjustedTimeAsyncInputTimeUs; + + @GuardedBy("lock") private float currentSpeed; + private long framesRead; + private boolean endOfStreamQueuedToSonic; /** The current input audio format. */ - @GuardedBy("lock") private AudioFormat inputAudioFormat; private AudioFormat pendingInputAudioFormat; @@ -89,6 +112,7 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { new SynchronizedSonicAudioProcessor(lock, /* keepActiveWithDefaultParameters= */ true); pendingCallbackInputTimesUs = new LongArrayQueue(); pendingCallbacks = new ArrayDeque<>(); + speedAdjustedTimeAsyncInputTimeUs = C.TIME_UNSET; resetInternalState(/* shouldResetSpeed= */ true); } @@ -96,10 +120,10 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { public static long getSampleCountAfterProcessorApplied( SpeedProvider speedProvider, @IntRange(from = 1) int inputSampleRateHz, - @IntRange(from = 0) long inputSamples) { + @IntRange(from = 1) long inputSamples) { checkArgument(speedProvider != null); checkArgument(inputSampleRateHz > 0); - checkArgument(inputSamples >= 0); + checkArgument(inputSamples > 0); long outputSamples = 0; long positionSamples = 0; @@ -147,22 +171,18 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { @Override public void queueInput(ByteBuffer inputBuffer) { - AudioFormat format; - synchronized (lock) { - format = inputAudioFormat; - } - - float newSpeed = getSampleAlignedSpeed(speedProvider, framesRead, format.sampleRate); + long currentTimeUs = sampleCountToDurationUs(framesRead, inputAudioFormat.sampleRate); + float newSpeed = getSampleAlignedSpeed(speedProvider, framesRead, inputAudioFormat.sampleRate); long nextSpeedChangeSamplePosition = - getNextSpeedChangeSamplePosition(speedProvider, framesRead, format.sampleRate); + getNextSpeedChangeSamplePosition(speedProvider, framesRead, inputAudioFormat.sampleRate); - updateSpeed(newSpeed); + updateSpeed(newSpeed, currentTimeUs); int inputBufferLimit = inputBuffer.limit(); int bytesToNextSpeedChange; if (nextSpeedChangeSamplePosition != C.INDEX_UNSET) { bytesToNextSpeedChange = - (int) ((nextSpeedChangeSamplePosition - framesRead) * format.bytesPerFrame); + (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 { @@ -177,8 +197,10 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { endOfStreamQueuedToSonic = true; } long bytesRead = inputBuffer.position() - startPosition; - checkState(bytesRead % format.bytesPerFrame == 0, "A frame was not queued completely."); - framesRead += bytesRead / format.bytesPerFrame; + checkState( + bytesRead % inputAudioFormat.bytesPerFrame == 0, "A frame was not queued completely."); + framesRead += bytesRead / inputAudioFormat.bytesPerFrame; + updateLastProcessedInputTime(); inputBuffer.limit(inputBufferLimit); } @@ -193,7 +215,9 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { @Override public ByteBuffer getOutput() { - return sonicAudioProcessor.getOutput(); + ByteBuffer output = sonicAudioProcessor.getOutput(); + processPendingCallbacks(); + return output; } @Override @@ -204,12 +228,9 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { @Override public void flush() { inputEnded = false; + inputAudioFormat = pendingInputAudioFormat; resetInternalState(/* shouldResetSpeed= */ false); - synchronized (lock) { - inputAudioFormat = pendingInputAudioFormat; - sonicAudioProcessor.flush(); - processPendingCallbacks(); - } + sonicAudioProcessor.flush(); } @Override @@ -217,11 +238,7 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { flush(); pendingInputAudioFormat = AudioFormat.NOT_SET; pendingOutputAudioFormat = AudioFormat.NOT_SET; - synchronized (lock) { - inputAudioFormat = AudioFormat.NOT_SET; - pendingCallbackInputTimesUs.clear(); - pendingCallbacks.clear(); - } + inputAudioFormat = AudioFormat.NOT_SET; resetInternalState(/* shouldResetSpeed= */ true); sonicAudioProcessor.reset(); } @@ -244,125 +261,154 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { * @param callback The callback called with the output time. May be called on a different thread * from the caller of this method. */ - // TODO(b/381553948): Accept an executor on which to dispatch the callback. public void getSpeedAdjustedTimeAsync(long inputTimeUs, TimestampConsumer callback) { - int sampleRate; synchronized (lock) { - sampleRate = inputAudioFormat.sampleRate; - - if (sampleRate == Format.NO_VALUE) { - pendingCallbackInputTimesUs.add(inputTimeUs); - pendingCallbacks.add(callback); + checkArgument(speedAdjustedTimeAsyncInputTimeUs < inputTimeUs); + speedAdjustedTimeAsyncInputTimeUs = inputTimeUs; + if ((inputTimeUs <= lastProcessedInputTimeUs && pendingCallbackInputTimesUs.isEmpty()) + || isEnded()) { + callback.onTimestamp(calculateSpeedAdjustedTime(inputTimeUs)); return; } + pendingCallbackInputTimesUs.add(inputTimeUs); + pendingCallbacks.add(callback); } - // TODO(b/381553948): Use an executor to invoke callback. - callback.onTimestamp( - getDurationUsAfterProcessorApplied(speedProvider, sampleRate, inputTimeUs)); } /** - * Returns the input media duration in microseconds for the given playout duration. + * Returns the input media duration for the given playout duration. * - *

This method returns the inverse of {@link #getSpeedAdjustedTimeAsync} when the instance has - * been configured and flushed. Otherwise, it returns {@code playoutDurationUs}. + *

Both durations are counted from the last {@link #reset()} or {@link #flush()} of the audio + * processor. + * + *

The {@code playoutDurationUs} must be less than last processed buffer output time. * * @param playoutDurationUs The playout duration in microseconds. + * @return The corresponding input duration in microseconds. */ public long getMediaDurationUs(long playoutDurationUs) { - int sampleRate; synchronized (lock) { - sampleRate = inputAudioFormat.sampleRate; + int floorIndex = outputSegmentStartTimesUs.size() - 1; + while (floorIndex > 0 && outputSegmentStartTimesUs.get(floorIndex) > playoutDurationUs) { + floorIndex--; + } + long lastSegmentOutputDurationUs = + playoutDurationUs - outputSegmentStartTimesUs.get(floorIndex); + long lastSegmentInputDurationUs; + if (floorIndex == outputSegmentStartTimesUs.size() - 1) { + lastSegmentInputDurationUs = getMediaDurationUsAtCurrentSpeed(lastSegmentOutputDurationUs); + + } else { + lastSegmentInputDurationUs = + round( + lastSegmentOutputDurationUs + * divide( + inputSegmentStartTimesUs.get(floorIndex + 1) + - inputSegmentStartTimesUs.get(floorIndex), + outputSegmentStartTimesUs.get(floorIndex + 1) + - outputSegmentStartTimesUs.get(floorIndex))); + } + return inputSegmentStartTimesUs.get(floorIndex) + lastSegmentInputDurationUs; } - if (sampleRate == Format.NO_VALUE) { - return playoutDurationUs; - } - long outputSamples = - scaleLargeValue(playoutDurationUs, sampleRate, C.MICROS_PER_SECOND, RoundingMode.HALF_EVEN); - long inputSamples = getInputFrameCountForOutput(speedProvider, sampleRate, outputSamples); - return sampleCountToDurationUs(inputSamples, sampleRate); } /** - * Returns the number of input frames needed to output a specific number of frames, given a speed - * provider, input sample rate, and number of output frames. - * - *

This is the inverse operation of {@link #getSampleCountAfterProcessorApplied}. + * Assuming enough audio has been processed, calculates the time at which the {@code inputTimeUs} + * is outputted at after the speed changes has been applied. */ - @VisibleForTesting - /* package */ static long getInputFrameCountForOutput( - SpeedProvider speedProvider, - @IntRange(from = 1) int inputSampleRate, - @IntRange(from = 0) long outputFrameCount) { - checkArgument(inputSampleRate > 0); - checkArgument(outputFrameCount >= 0); - - long inputSampleCount = 0; - while (outputFrameCount > 0) { - long boundarySamples = - getNextSpeedChangeSamplePosition(speedProvider, inputSampleCount, inputSampleRate); - float speed = getSampleAlignedSpeed(speedProvider, inputSampleCount, inputSampleRate); - - long outputSamplesForSection = - Sonic.getExpectedFrameCountAfterProcessorApplied( - /* inputSampleRateHz= */ inputSampleRate, - /* outputSampleRateHz= */ inputSampleRate, - /* speed= */ speed, - /* pitch= */ speed, - /* inputFrameCount= */ boundarySamples - inputSampleCount); - - if (boundarySamples == C.INDEX_UNSET || outputSamplesForSection > outputFrameCount) { - inputSampleCount += - Sonic.getExpectedInputFrameCountForOutputFrameCount( - /* inputSampleRateHz= */ inputSampleRate, - /* outputSampleRateHz= */ inputSampleRate, - /* speed= */ speed, - /* pitch= */ speed, - outputFrameCount); - outputFrameCount = 0; - } else { - outputFrameCount -= outputSamplesForSection; - inputSampleCount = boundarySamples; - } + @SuppressWarnings("GuardedBy") // All call sites are guarded. + private long calculateSpeedAdjustedTime(long inputTimeUs) { + int floorIndex = inputSegmentStartTimesUs.size() - 1; + while (floorIndex > 0 && inputSegmentStartTimesUs.get(floorIndex) > inputTimeUs) { + floorIndex--; } - - return inputSampleCount; + long lastSegmentOutputDurationUs; + if (floorIndex == inputSegmentStartTimesUs.size() - 1) { + if (lastSpeedAdjustedInputTimeUs < inputSegmentStartTimesUs.get(floorIndex)) { + lastSpeedAdjustedInputTimeUs = inputSegmentStartTimesUs.get(floorIndex); + lastSpeedAdjustedOutputTimeUs = outputSegmentStartTimesUs.get(floorIndex); + } + long lastSegmentInputDurationUs = inputTimeUs - lastSpeedAdjustedInputTimeUs; + lastSegmentOutputDurationUs = getPlayoutDurationUsAtCurrentSpeed(lastSegmentInputDurationUs); + } else { + long lastSegmentInputDurationUs = inputTimeUs - lastSpeedAdjustedInputTimeUs; + lastSegmentOutputDurationUs = + round( + lastSegmentInputDurationUs + * divide( + outputSegmentStartTimesUs.get(floorIndex + 1) + - outputSegmentStartTimesUs.get(floorIndex), + inputSegmentStartTimesUs.get(floorIndex + 1) + - inputSegmentStartTimesUs.get(floorIndex))); + } + lastSpeedAdjustedInputTimeUs = inputTimeUs; + lastSpeedAdjustedOutputTimeUs += lastSegmentOutputDurationUs; + return lastSpeedAdjustedOutputTimeUs; } - private static long getDurationUsAfterProcessorApplied( - SpeedProvider speedProvider, int sampleRate, long inputDurationUs) { - long inputSamples = - scaleLargeValue(inputDurationUs, sampleRate, C.MICROS_PER_SECOND, RoundingMode.HALF_EVEN); - long outputSamples = - getSampleCountAfterProcessorApplied(speedProvider, sampleRate, inputSamples); - return sampleCountToDurationUs(outputSamples, sampleRate); + private static double divide(long dividend, long divisor) { + return ((double) dividend) / divisor; } private void processPendingCallbacks() { synchronized (lock) { - if (inputAudioFormat.sampleRate == Format.NO_VALUE) { - return; - } - - while (!pendingCallbacks.isEmpty()) { - long inputTimeUs = pendingCallbackInputTimesUs.remove(); - TimestampConsumer consumer = pendingCallbacks.remove(); - // TODO(b/381553948): Use an executor to invoke callback. - consumer.onTimestamp( - getDurationUsAfterProcessorApplied( - speedProvider, inputAudioFormat.sampleRate, inputTimeUs)); + while (!pendingCallbacks.isEmpty() + && (pendingCallbackInputTimesUs.element() <= lastProcessedInputTimeUs || isEnded())) { + pendingCallbacks + .remove() + .onTimestamp(calculateSpeedAdjustedTime(pendingCallbackInputTimesUs.remove())); } } } - private void updateSpeed(float newSpeed) { - if (newSpeed != currentSpeed) { - currentSpeed = newSpeed; - sonicAudioProcessor.setSpeed(newSpeed); - sonicAudioProcessor.setPitch(newSpeed); - // Invalidate any previously created buffers in SonicAudioProcessor and the base class. - sonicAudioProcessor.flush(); - endOfStreamQueuedToSonic = false; + private void updateSpeed(float newSpeed, long timeUs) { + synchronized (lock) { + if (newSpeed != currentSpeed) { + updateSpeedChangeArrays(timeUs); + currentSpeed = newSpeed; + sonicAudioProcessor.setSpeed(newSpeed); + sonicAudioProcessor.setPitch(newSpeed); + // Invalidate any previously created buffers in SonicAudioProcessor and the base class. + sonicAudioProcessor.flush(); + endOfStreamQueuedToSonic = false; + } + } + } + + @SuppressWarnings("GuardedBy") // All call sites are guarded. + private void updateSpeedChangeArrays(long currentSpeedChangeInputTimeUs) { + long lastSpeedChangeOutputTimeUs = + outputSegmentStartTimesUs.get(outputSegmentStartTimesUs.size() - 1); + long lastSpeedChangeInputTimeUs = + inputSegmentStartTimesUs.get(inputSegmentStartTimesUs.size() - 1); + long lastSpeedSegmentMediaDurationUs = + currentSpeedChangeInputTimeUs - lastSpeedChangeInputTimeUs; + inputSegmentStartTimesUs.add(currentSpeedChangeInputTimeUs); + outputSegmentStartTimesUs.add( + lastSpeedChangeOutputTimeUs + + getPlayoutDurationUsAtCurrentSpeed(lastSpeedSegmentMediaDurationUs)); + } + + private long getPlayoutDurationUsAtCurrentSpeed(long mediaDurationUs) { + return sonicAudioProcessor.getPlayoutDuration(mediaDurationUs); + } + + private long getMediaDurationUsAtCurrentSpeed(long playoutDurationUs) { + return sonicAudioProcessor.getMediaDuration(playoutDurationUs); + } + + private void updateLastProcessedInputTime() { + synchronized (lock) { + // TODO - b/320242819: Investigate whether bytesRead can be used here rather than + // sonicAudioProcessor.getProcessedInputBytes(). + long currentProcessedInputDurationUs = + Util.scaleLargeTimestamp( + /* timestamp= */ sonicAudioProcessor.getProcessedInputBytes(), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame); + lastProcessedInputTimeUs = + inputSegmentStartTimesUs.get(inputSegmentStartTimesUs.size() - 1) + + currentProcessedInputDurationUs; } } @@ -374,12 +420,28 @@ public final class SpeedChangingAudioProcessor implements AudioProcessor { * * @param shouldResetSpeed Whether {@link #currentSpeed} should be reset to its default value. */ + @EnsuresNonNull({"inputSegmentStartTimesUs", "outputSegmentStartTimesUs"}) + @RequiresNonNull("lock") private void resetInternalState( @UnknownInitialization SpeedChangingAudioProcessor this, boolean shouldResetSpeed) { - if (shouldResetSpeed) { - currentSpeed = 1f; + synchronized (lock) { + inputSegmentStartTimesUs = new LongArray(); + outputSegmentStartTimesUs = new LongArray(); + inputSegmentStartTimesUs.add(0); + outputSegmentStartTimesUs.add(0); + lastProcessedInputTimeUs = 0; + lastSpeedAdjustedInputTimeUs = 0; + lastSpeedAdjustedOutputTimeUs = 0; + if (shouldResetSpeed) { + currentSpeed = 1f; + } } + framesRead = 0; endOfStreamQueuedToSonic = false; + // TODO: b/339842724 - This should ideally also reset speedAdjustedTimeAsyncInputTimeUs and + // clear pendingCallbacks and pendingCallbacksInputTimes. We can't do this at the moment + // because some clients register callbacks with getSpeedAdjustedTimeAsync before this audio + // processor is flushed. } } 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 05785447ad..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 @@ -16,7 +16,7 @@ package androidx.media3.common.audio; import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER; -import static androidx.media3.common.audio.SpeedChangingAudioProcessor.getInputFrameCountForOutput; +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; @@ -36,59 +36,53 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class SpeedChangingAudioProcessorTest { - private static final AudioFormat AUDIO_FORMAT_44_100HZ = + private static final AudioFormat AUDIO_FORMAT = new AudioFormat( - /* sampleRate= */ 44_100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); - - private static final AudioFormat AUDIO_FORMAT_50_000HZ = - new AudioFormat( - /* sampleRate= */ 50_000, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); + /* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); @Test public void queueInput_noSpeedChange_doesNotOverwriteInput() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); assertThat(inputBuffer) - .isEqualTo( - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame)); + .isEqualTo(getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame)); } @Test public void queueInput_speedChange_doesNotOverwriteInput() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); assertThat(inputBuffer) - .isEqualTo( - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame)); + .isEqualTo(getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame)); } @Test public void queueInput_noSpeedChange_copiesSamples() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -102,11 +96,11 @@ public class SpeedChangingAudioProcessorTest { public void queueInput_speedChange_modifiesSamples() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -121,13 +115,11 @@ public class SpeedChangingAudioProcessorTest { public void queueInput_noSpeedChangeAfterSpeedChange_copiesSamples() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {2, 1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -144,13 +136,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {1, 2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -160,7 +150,7 @@ public class SpeedChangingAudioProcessorTest { speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); inputBuffer.rewind(); speedChangingAudioProcessor.queueInput(inputBuffer); @@ -175,13 +165,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {3, 2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {3, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -191,7 +179,7 @@ public class SpeedChangingAudioProcessorTest { speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); inputBuffer.rewind(); speedChangingAudioProcessor.queueInput(inputBuffer); @@ -206,20 +194,18 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {2, 3}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 3}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); ByteBuffer outputBuffer = getAudioProcessorOutput(speedChangingAudioProcessor); speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); inputBuffer.rewind(); speedChangingAudioProcessor.queueInput(inputBuffer); @@ -232,7 +218,7 @@ public class SpeedChangingAudioProcessorTest { @Test public void queueInput_multipleSpeedsInBufferWithLimitAtFrameBoundary_readsDataUntilSpeedLimit() throws Exception { - long speedChangeTimeUs = 4 * C.MICROS_PER_SECOND / AUDIO_FORMAT_44_100HZ.sampleRate; + long speedChangeTimeUs = 4 * C.MICROS_PER_SECOND / AUDIO_FORMAT.sampleRate; SpeedProvider speedProvider = TestSpeedProvider.createWithStartTimes( /* startTimesUs= */ new long[] {0L, speedChangeTimeUs}, @@ -240,19 +226,19 @@ public class SpeedChangingAudioProcessorTest { SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); int inputBufferLimit = inputBuffer.limit(); speedChangingAudioProcessor.queueInput(inputBuffer); - assertThat(inputBuffer.position()).isEqualTo(4 * AUDIO_FORMAT_44_100HZ.bytesPerFrame); + assertThat(inputBuffer.position()).isEqualTo(4 * AUDIO_FORMAT.bytesPerFrame); assertThat(inputBuffer.limit()).isEqualTo(inputBufferLimit); } @Test public void queueInput_multipleSpeedsInBufferWithLimitInsideFrame_readsDataUntilSpeedLimit() throws Exception { - long speedChangeTimeUs = (long) (3.5 * C.MICROS_PER_SECOND / AUDIO_FORMAT_44_100HZ.sampleRate); + long speedChangeTimeUs = (long) (3.5 * C.MICROS_PER_SECOND / AUDIO_FORMAT.sampleRate); SpeedProvider speedProvider = TestSpeedProvider.createWithStartTimes( /* startTimesUs= */ new long[] {0L, speedChangeTimeUs}, @@ -260,12 +246,12 @@ public class SpeedChangingAudioProcessorTest { SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); int inputBufferLimit = inputBuffer.limit(); speedChangingAudioProcessor.queueInput(inputBuffer); - assertThat(inputBuffer.position()).isEqualTo(4 * AUDIO_FORMAT_44_100HZ.bytesPerFrame); + assertThat(inputBuffer.position()).isEqualTo(4 * AUDIO_FORMAT.bytesPerFrame); assertThat(inputBuffer.limit()).isEqualTo(inputBufferLimit); } @@ -280,18 +266,18 @@ public class SpeedChangingAudioProcessorTest { SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); // SpeedChangingAudioProcessor only queues samples until the next speed change. while (inputBuffer.hasRemaining()) { speedChangingAudioProcessor.queueInput(inputBuffer); outputFrames += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.queueEndOfStream(); outputFrames += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; // We allow 1 sample of tolerance per speed change. assertThat(outputFrames).isWithin(1).of(3); } @@ -301,13 +287,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {2, 1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -323,13 +307,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {5, 5}, - /* speeds= */ new float[] {1, 2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {1, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); inputBuffer.rewind(); @@ -345,11 +327,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -362,11 +344,11 @@ public class SpeedChangingAudioProcessorTest { throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueEndOfStream(); @@ -378,7 +360,7 @@ public class SpeedChangingAudioProcessorTest { public void queueEndOfStream_noInputQueued_endsProcessor() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); @@ -391,11 +373,11 @@ public class SpeedChangingAudioProcessorTest { public void isEnded_afterNoSpeedChangeAndOutputRetrieved_isFalse() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); @@ -407,11 +389,11 @@ public class SpeedChangingAudioProcessorTest { public void isEnded_afterSpeedChangeAndOutputRetrieved_isFalse() throws Exception { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5}, /* speeds= */ new float[] {2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); ByteBuffer inputBuffer = - getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(inputBuffer); getAudioProcessorOutput(speedChangingAudioProcessor); @@ -420,89 +402,147 @@ public class SpeedChangingAudioProcessorTest { } @Test - public void getSpeedAdjustedTimeAsync_beforeFlush_callbacksCalledWithCorrectParametersAfterFlush() - throws Exception { + public void getSpeedAdjustedTimeAsync_callbacksCalledWithCorrectParameters() throws Exception { ArrayList outputTimesUs = new ArrayList<>(); - // Sample period = 20us. + // The speed change is at 113Us (5*MICROS_PER_SECOND/sampleRate). SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_50_000HZ, - /* frameCounts= */ new int[] {6, 6}, - /* speeds= */ new float[] {2, 1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = - new SpeedChangingAudioProcessor(speedProvider); - speedChangingAudioProcessor.configure(AUDIO_FORMAT_50_000HZ); + getConfiguredSpeedChangingAudioProcessor(speedProvider); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 40L, outputTimesUs::add); + /* inputTimeUs= */ 50L, outputTimesUs::add); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 80L, outputTimesUs::add); + /* inputTimeUs= */ 100L, outputTimesUs::add); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 160L, outputTimesUs::add); + /* inputTimeUs= */ 150L, outputTimesUs::add); - assertThat(outputTimesUs).isEmpty(); - speedChangingAudioProcessor.flush(); - assertThat(outputTimesUs).containsExactly(20L, 40L, 100L); + // 150 is after the speed change so floor(113 / 2 + (150 - 113)*1) -> 93 + assertThat(outputTimesUs).containsExactly(25L, 50L, 93L); } @Test - public void getSpeedAdjustedTimeAsync_afterCallToFlush_callbacksCalledWithCorrectParameters() + public void getSpeedAdjustedTimeAsync_afterFlush_callbacksCalledWithCorrectParameters() throws Exception { ArrayList outputTimesUs = new ArrayList<>(); - // Sample period = 20us. + // The speed change is at 113Us (5*MICROS_PER_SECOND/sampleRate). Also add another speed change + // to 3x at a later point that should not be used if the flush is handled correctly. SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_50_000HZ, - /* frameCounts= */ new int[] {6, 6}, - /* speeds= */ new float[] {2, 1}); + AUDIO_FORMAT, + /* frameCounts= */ new int[] {5, 5, 5}, + /* speeds= */ new float[] {2, 1, 3}); SpeedChangingAudioProcessor speedChangingAudioProcessor = - new SpeedChangingAudioProcessor(speedProvider); - speedChangingAudioProcessor.configure(AUDIO_FORMAT_50_000HZ); + getConfiguredSpeedChangingAudioProcessor(speedProvider); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); + // Use the audio processor before a flush + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + + // Flush and use it again. speedChangingAudioProcessor.flush(); + speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( + /* inputTimeUs= */ 50L, outputTimesUs::add); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( + /* inputTimeUs= */ 100L, outputTimesUs::add); + speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( + /* inputTimeUs= */ 150L, outputTimesUs::add); - speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 40L, outputTimesUs::add); - speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 80L, outputTimesUs::add); - speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( - /* inputTimeUs= */ 160L, outputTimesUs::add); - - assertThat(outputTimesUs).containsExactly(20L, 40L, 100L); + // 150 is after the speed change so floor(113 / 2 + (150 - 113)*1) -> 93 + assertThat(outputTimesUs).containsExactly(25L, 50L, 93L); } @Test public void getSpeedAdjustedTimeAsync_timeAfterEndTime_callbacksCalledWithCorrectParameters() throws Exception { ArrayList outputTimesUs = new ArrayList<>(); - // The speed change is at 120Us (6*MICROS_PER_SECOND/sampleRate). + // The speed change is at 113Us (5*MICROS_PER_SECOND/sampleRate). SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_50_000HZ, - /* frameCounts= */ new int[] {6, 6}, - /* speeds= */ new float[] {2, 1}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); SpeedChangingAudioProcessor speedChangingAudioProcessor = - new SpeedChangingAudioProcessor(speedProvider); - speedChangingAudioProcessor.configure(AUDIO_FORMAT_50_000HZ); - speedChangingAudioProcessor.flush(); + getConfiguredSpeedChangingAudioProcessor(speedProvider); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 3, AUDIO_FORMAT.bytesPerFrame); + + speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( + /* inputTimeUs= */ 300L, outputTimesUs::add); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + speedChangingAudioProcessor.queueEndOfStream(); + getAudioProcessorOutput(speedChangingAudioProcessor); + + // 150 is after the speed change so floor(113 / 2 + (300 - 113)*1) -> 243 + assertThat(outputTimesUs).containsExactly(243L); + } + + @Test + public void + getSpeedAdjustedTimeAsync_timeAfterEndTimeAfterProcessorEnded_callbacksCalledWithCorrectParameters() + throws Exception { + ArrayList outputTimesUs = new ArrayList<>(); + // The speed change is at 113Us (5*MICROS_PER_SECOND/sampleRate). + SpeedProvider speedProvider = + TestSpeedProvider.createWithFrameCounts( + AUDIO_FORMAT, /* frameCounts= */ new int[] {5, 5}, /* speeds= */ new float[] {2, 1}); + SpeedChangingAudioProcessor speedChangingAudioProcessor = + getConfiguredSpeedChangingAudioProcessor(speedProvider); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 5, AUDIO_FORMAT.bytesPerFrame); + speedChangingAudioProcessor.queueInput(inputBuffer); + getAudioProcessorOutput(speedChangingAudioProcessor); + inputBuffer.rewind(); + speedChangingAudioProcessor.queueInput(inputBuffer); + speedChangingAudioProcessor.queueEndOfStream(); + getAudioProcessorOutput(speedChangingAudioProcessor); + checkState(speedChangingAudioProcessor.isEnded()); speedChangingAudioProcessor.getSpeedAdjustedTimeAsync( /* inputTimeUs= */ 300L, outputTimesUs::add); - assertThat(outputTimesUs).containsExactly(240L); + // 150 is after the speed change so floor(113 / 2 + (300 - 113)*1) -> 243 + assertThat(outputTimesUs).containsExactly(243L); } @Test public void getMediaDurationUs_returnsCorrectValues() throws Exception { - // The speed changes happen every 10ms (500 samples @ 50.KHz) + // The speed changes happen every 10ms (441 samples @ 441.KHz) SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_50_000HZ, - /* frameCounts= */ new int[] {500, 500, 500, 500}, + AUDIO_FORMAT, + /* frameCounts= */ new int[] {441, 441, 441, 441}, /* speeds= */ new float[] {2, 1, 5, 2}); SpeedChangingAudioProcessor speedChangingAudioProcessor = - new SpeedChangingAudioProcessor(speedProvider); - speedChangingAudioProcessor.configure(AUDIO_FORMAT_50_000HZ); - speedChangingAudioProcessor.flush(); + getConfiguredSpeedChangingAudioProcessor(speedProvider); + ByteBuffer inputBuffer = + getNonRandomByteBuffer(/* frameCount= */ 441 * 4, AUDIO_FORMAT.bytesPerFrame); + while (inputBuffer.position() < inputBuffer.limit()) { + speedChangingAudioProcessor.queueInput(inputBuffer); + } + getAudioProcessorOutput(speedChangingAudioProcessor); // input (in ms) (0, 10, 20, 30, 40) -> // output (in ms) (0, 10/2, 10/2 + 10, 10/2 + 10 + 10/5, 10/2 + 10 + 10/5 + 10/2) @@ -532,30 +572,30 @@ public class SpeedChangingAudioProcessorTest { int outputFrameCount = 0; SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 1000, 1000}, /* speeds= */ new float[] {2, 4, 2}); // 500, 250, 500 = 1250 SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer input = getNonRandomByteBuffer(1000, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + ByteBuffer input = getNonRandomByteBuffer(1000, AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; input.rewind(); speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; input.rewind(); speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; speedChangingAudioProcessor.queueEndOfStream(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; assertThat(outputFrameCount).isWithin(2).of(1250); } @@ -572,17 +612,17 @@ public class SpeedChangingAudioProcessorTest { /* speeds= */ new float[] {2, 3, 8, 4}); SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); - ByteBuffer input = getNonRandomByteBuffer(12, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + ByteBuffer input = getNonRandomByteBuffer(12, AUDIO_FORMAT.bytesPerFrame); while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.queueEndOfStream(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; // Allow one sample of tolerance per effectively applied speed change. assertThat(outputFrameCount).isWithin(1).of(4); @@ -593,23 +633,23 @@ public class SpeedChangingAudioProcessorTest { throws AudioProcessor.UnhandledAudioFormatException { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 1000}, /* speeds= */ new float[] {1, 2}); // 1000, 500. SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); // 1500 input frames falls in the middle of the 2x region. - ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT.bytesPerFrame); int outputFrameCount = 0; while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.flush(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; assertThat(outputFrameCount).isEqualTo(1250); input.rewind(); @@ -619,11 +659,11 @@ public class SpeedChangingAudioProcessorTest { while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.queueEndOfStream(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; assertThat(outputFrameCount).isWithin(1).of(2500); // 1250 * 2. } @@ -632,23 +672,23 @@ public class SpeedChangingAudioProcessorTest { throws AudioProcessor.UnhandledAudioFormatException { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 1000}, /* speeds= */ new float[] {2, 4}); // 500, 250. SpeedChangingAudioProcessor speedChangingAudioProcessor = getConfiguredSpeedChangingAudioProcessor(speedProvider); // 1500 input frames falls in the middle of the 2x region. - ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT_44_100HZ.bytesPerFrame); + ByteBuffer input = getNonRandomByteBuffer(1500, AUDIO_FORMAT.bytesPerFrame); int outputFrameCount = 0; while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.flush(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; assertThat(outputFrameCount).isWithin(1).of(625); input.rewind(); @@ -658,11 +698,11 @@ public class SpeedChangingAudioProcessorTest { while (input.hasRemaining()) { speedChangingAudioProcessor.queueInput(input); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; } speedChangingAudioProcessor.queueEndOfStream(); outputFrameCount += - speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT_44_100HZ.bytesPerFrame; + speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame; assertThat(outputFrameCount).isWithin(2).of(1250); // 625 * 2. } @@ -676,7 +716,7 @@ public class SpeedChangingAudioProcessorTest { long sampleCountAfterProcessorApplied = SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ 100); + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 100); assertThat(sampleCountAfterProcessorApplied).isEqualTo(50); } @@ -684,13 +724,13 @@ public class SpeedChangingAudioProcessorTest { public void getSampleCountAfterProcessorApplied_withMultipleSpeeds_outputsExpectedSamples() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {100, 400, 50}, /* speeds= */ new float[] {2.f, 4f, 0.5f}); long sampleCountAfterProcessorApplied = SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ 550); + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 550); assertThat(sampleCountAfterProcessorApplied).isEqualTo(250); } @@ -699,13 +739,13 @@ public class SpeedChangingAudioProcessorTest { getSampleCountAfterProcessorApplied_beyondLastSpeedRegion_stillAppliesLastSpeedValue() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {100, 400, 50}, /* speeds= */ new float[] {2.f, 4f, 0.5f}); long sampleCountAfterProcessorApplied = SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ 3000); + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3000); assertThat(sampleCountAfterProcessorApplied).isEqualTo(5150); } @@ -714,38 +754,38 @@ public class SpeedChangingAudioProcessorTest { getSampleCountAfterProcessorApplied_withInputCountBeyondIntRange_outputsExpectedSamples() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 10000, 8200}, /* speeds= */ new float[] {0.2f, 8f, 0.5f}); long sampleCountAfterProcessorApplied = SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ 3_000_000_000L); - assertThat(sampleCountAfterProcessorApplied).isEqualTo(5_999_984_250L); + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3_000_000_000L); + assertThat(sampleCountAfterProcessorApplied).isEqualTo(5999984250L); } // Testing range validation. @SuppressLint("Range") @Test - public void getSampleCountAfterProcessorApplied_withNegativeFrameCount_throws() { + public void getSampleCountAfterProcessorApplied_withNegativeSampleCount_throws() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 10000, 8200}, /* speeds= */ new float[] {0.2f, 8f, 0.5f}); assertThrows( IllegalArgumentException.class, () -> SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ -2L)); + speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ -2L)); } // Testing range validation. @SuppressLint("Range") @Test - public void getSampleCountAfterProcessorApplied_withZeroFrameRate_throws() { + public void getSampleCountAfterProcessorApplied_withZeroSampleRate_throws() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000, 10000, 8200}, /* speeds= */ new float[] {0.2f, 8f, 0.5f}); assertThrows( @@ -761,32 +801,14 @@ public class SpeedChangingAudioProcessorTest { IllegalArgumentException.class, () -> SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - /* speedProvider= */ null, - AUDIO_FORMAT_44_100HZ.sampleRate, - /* inputSamples= */ 1000L)); - } - - @Test - public void getSampleCountAfterProcessorApplied_withZeroInputFrames_returnsZero() { - SpeedProvider speedProvider = - TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {1000, 10000, 8200}, - /* speeds= */ new float[] {0.2f, 8f, 0.5f}); - - long sampleCountAfterProcessorApplied = - SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* inputSamples= */ 0L); - assertThat(sampleCountAfterProcessorApplied).isEqualTo(0L); + /* speedProvider= */ null, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 1000L)); } @Test public void isActive_beforeConfigure_returnsFalse() { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {1000}, - /* speeds= */ new float[] {2f}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000}, /* speeds= */ new float[] {2f}); SpeedChangingAudioProcessor processor = new SpeedChangingAudioProcessor(speedProvider); assertThat(processor.isActive()).isFalse(); @@ -797,34 +819,18 @@ public class SpeedChangingAudioProcessorTest { throws AudioProcessor.UnhandledAudioFormatException { SpeedProvider speedProvider = TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {1000}, - /* speeds= */ new float[] {2f}); + AUDIO_FORMAT, /* frameCounts= */ new int[] {1000}, /* speeds= */ new float[] {2f}); SpeedChangingAudioProcessor processor = new SpeedChangingAudioProcessor(speedProvider); - processor.configure(AUDIO_FORMAT_44_100HZ); + processor.configure(AUDIO_FORMAT); assertThat(processor.isActive()).isTrue(); } - @Test - public void getInputFrameCountForOutput_withZeroOutputFrames_returnsZero() { - SpeedProvider speedProvider = - TestSpeedProvider.createWithFrameCounts( - AUDIO_FORMAT_44_100HZ, - /* frameCounts= */ new int[] {1000, 10000, 8200}, - /* speeds= */ new float[] {0.2f, 8f, 0.5f}); - - long inputFrames = - getInputFrameCountForOutput( - speedProvider, AUDIO_FORMAT_44_100HZ.sampleRate, /* outputFrameCount= */ 0L); - assertThat(inputFrames).isEqualTo(0L); - } - private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor( SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException { SpeedChangingAudioProcessor speedChangingAudioProcessor = new SpeedChangingAudioProcessor(speedProvider); - speedChangingAudioProcessor.configure(AUDIO_FORMAT_44_100HZ); + speedChangingAudioProcessor.configure(AUDIO_FORMAT); speedChangingAudioProcessor.flush(); return speedChangingAudioProcessor; }