Use sample-aligned speed changes in SpeedChangingAudioProcessor

This CL adds utility methods to obtain sample-aligned timestamps from a
`SpeedProvider` to solve in a unified way the issues arising from
conversions between sample positions and microseconds.

The new utility methods will also help with the implementation of a
static method like `getDurationAfterProcessorApplied()` that will remove
the need for thread synchronization between the video and audio pipeline
for speed changing effects.

PiperOrigin-RevId: 692233318
This commit is contained in:
ivanbuper 2024-11-01 11:06:13 -07:00 committed by Copybara-Service
parent 0b1695124b
commit 7edbaa3f2c
4 changed files with 214 additions and 43 deletions

View file

@ -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);
}
}

View file

@ -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.
*
* <p>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.
*
* <p>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);
}
}

View file

@ -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

View file

@ -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);
}
}