diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java index 8762944cc8..9a55137524 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java @@ -141,6 +141,36 @@ public class SonicAudioProcessor implements AudioProcessor { } } + /** + * Returns the playout duration corresponding to the specified media duration, taking speed + * adjustment into account. + * + *
The scaling performed by this method will use the actual playback speed achieved by the
+ * audio processor, on average, since it was last flushed. This may differ very slightly from the
+ * target playback speed.
+ *
+ * @param mediaDuration The media duration to scale.
+ * @return The corresponding playout duration, in the same units as {@code mediaDuration}.
+ */
+ public final long getPlayoutDuration(long mediaDuration) {
+ if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
+ long processedInputBytes = inputBytes - checkNotNull(sonic).getPendingInputBytes();
+ return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
+ ? Util.scaleLargeTimestamp(mediaDuration, outputBytes, processedInputBytes)
+ : Util.scaleLargeTimestamp(
+ mediaDuration,
+ outputBytes * inputAudioFormat.sampleRate,
+ processedInputBytes * outputAudioFormat.sampleRate);
+ } else {
+ return (long) (mediaDuration / (double) speed);
+ }
+ }
+
+ /** Returns the number of bytes processed since last flush or reset. */
+ public final long getProcessedInputBytes() {
+ return inputBytes - checkNotNull(sonic).getPendingInputBytes();
+ }
+
@Override
public final AudioFormat configure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
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 fb2cb2472f..26852f38b2 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
@@ -19,9 +19,14 @@ package androidx.media3.common.audio;
import static java.lang.Math.min;
import androidx.media3.common.C;
+import androidx.media3.common.util.LongArray;
+import androidx.media3.common.util.LongArrayQueue;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.function.LongConsumer;
/**
* An {@link AudioProcessor} that changes the speed of audio samples depending on their timestamp.
@@ -41,13 +46,31 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
*/
private final SonicAudioProcessor sonicAudioProcessor;
+ private final Object pendingCallbacksLock;
+
+ // Elements in the same positions in the queues are associated.
+ private final LongArrayQueue pendingCallbackInputTimesUs;
+ private final Queue Calls {@linkplain LongConsumer#accept(long) the callback} with the output time as soon as
+ * enough audio has been processed to calculate it.
+ *
+ * Successive calls must have monotonically increasing {@code inputTimeUs}.
+ *
+ * Can be called from any thread.
+ *
+ * @param inputTimeUs The input time, in microseconds.
+ * @param callback The callback called with the output time. May be called on a different thread
+ * from the caller of this method.
+ */
+ public void getSpeedAdjustedTimeAsync(long inputTimeUs, LongConsumer callback) {
+ synchronized (pendingCallbacksLock) {
+ if (inputTimeUs <= lastProcessedInputTime) {
+ callback.accept(calculateSpeedAdjustedTime(inputTimeUs));
+ return;
+ }
+ pendingCallbackInputTimesUs.add(inputTimeUs);
+ pendingCallbacks.add(callback);
+ }
+ }
+
+ /**
+ * Assuming enough audio has been processed, calculates the time at which the {@code inputTimeUs}
+ * is outputted at after the speed changes has been applied.
+ */
+ private long calculateSpeedAdjustedTime(long inputTimeUs) {
+ int floorIndex = inputSegmentStartTimesUs.size() - 1;
+ while (floorIndex > 0 && inputSegmentStartTimesUs.get(floorIndex) > inputTimeUs) {
+ floorIndex--;
+ }
+ long lastSegmentInputDuration = inputTimeUs - inputSegmentStartTimesUs.get(floorIndex);
+ long lastSegmentOutputDuration =
+ floorIndex == inputSegmentStartTimesUs.size() - 1
+ ? getPlayoutDurationAtCurrentSpeed(lastSegmentInputDuration)
+ : lastSegmentInputDuration
+ * (inputSegmentStartTimesUs.get(floorIndex + 1)
+ - inputSegmentStartTimesUs.get(floorIndex))
+ / (outputSegmentStartTimesUs.get(floorIndex + 1)
+ - outputSegmentStartTimesUs.get(floorIndex));
+ return outputSegmentStartTimesUs.get(floorIndex) + lastSegmentOutputDuration;
+ }
+
+ private void processPendingCallbacks() {
+ synchronized (pendingCallbacksLock) {
+ while (!pendingCallbacks.isEmpty()
+ && pendingCallbackInputTimesUs.element() <= lastProcessedInputTime) {
+ pendingCallbacks
+ .remove()
+ .accept(calculateSpeedAdjustedTime(pendingCallbackInputTimesUs.remove()));
+ }
+ }
+ }
+
+ 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
+ + getPlayoutDurationAtCurrentSpeed(lastSpeedSegmentMediaDurationUs));
+ }
+
+ private long getPlayoutDurationAtCurrentSpeed(long mediaDuration) {
+ return isUsingSonic() ? sonicAudioProcessor.getPlayoutDuration(mediaDuration) : mediaDuration;
+ }
+
+ private long updateLastProcessedInputTime() {
+ if (isUsingSonic()) {
+ // 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);
+ return inputSegmentStartTimesUs.get(inputSegmentStartTimesUs.size() - 1)
+ + currentProcessedInputDurationUs;
+ }
+ return Util.scaleLargeTimestamp(
+ /* timestamp= */ bytesRead,
+ /* multiplier= */ C.MICROS_PER_SECOND,
+ /* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame);
+ }
+
private boolean isUsingSonic() {
return currentSpeed != 1f;
}
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 9dc725cc9e..74caffbe42 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
@@ -23,6 +23,7 @@ import androidx.media3.test.utils.TestSpeedProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -350,6 +351,32 @@ public class SpeedChangingAudioProcessorTest {
assertThat(speedChangingAudioProcessor.isEnded()).isFalse();
}
+ @Test
+ public void getSpeedAdjustedTimeAsync_callbacksCalledWithCorrectParameters() throws Exception {
+ ArrayList