mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
0b1695124b
commit
7edbaa3f2c
4 changed files with 214 additions and 43 deletions
|
|
@ -17,6 +17,9 @@
|
||||||
package androidx.media3.common.audio;
|
package androidx.media3.common.audio;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
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 androidx.media3.common.util.Util.sampleCountToDurationUs;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
import static java.lang.Math.round;
|
import static java.lang.Math.round;
|
||||||
|
|
@ -116,39 +119,17 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||||
@Override
|
@Override
|
||||||
public void queueInput(ByteBuffer inputBuffer) {
|
public void queueInput(ByteBuffer inputBuffer) {
|
||||||
long currentTimeUs = sampleCountToDurationUs(framesRead, inputAudioFormat.sampleRate);
|
long currentTimeUs = sampleCountToDurationUs(framesRead, inputAudioFormat.sampleRate);
|
||||||
float newSpeed = speedProvider.getSpeed(currentTimeUs);
|
float newSpeed = getSampleAlignedSpeed(speedProvider, framesRead, inputAudioFormat.sampleRate);
|
||||||
long nextSpeedChangeTimeUs = speedProvider.getNextSpeedChangeTimeUs(currentTimeUs);
|
long nextSpeedChangeSamplePosition =
|
||||||
long sampleRateAlignedNextSpeedChangeTimeUs =
|
getNextSpeedChangeSamplePosition(speedProvider, framesRead, inputAudioFormat.sampleRate);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSpeed(newSpeed, currentTimeUs);
|
updateSpeed(newSpeed, currentTimeUs);
|
||||||
|
|
||||||
int inputBufferLimit = inputBuffer.limit();
|
int inputBufferLimit = inputBuffer.limit();
|
||||||
int bytesToNextSpeedChange;
|
int bytesToNextSpeedChange;
|
||||||
if (nextSpeedChangeTimeUs != C.TIME_UNSET) {
|
if (nextSpeedChangeSamplePosition != C.INDEX_UNSET) {
|
||||||
bytesToNextSpeedChange =
|
bytesToNextSpeedChange =
|
||||||
(int)
|
(int) ((nextSpeedChangeSamplePosition - framesRead) * inputAudioFormat.bytesPerFrame);
|
||||||
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;
|
|
||||||
}
|
|
||||||
// Update the input buffer limit to make sure that all samples processed have the same speed.
|
// Update the input buffer limit to make sure that all samples processed have the same speed.
|
||||||
inputBuffer.limit(min(inputBufferLimit, inputBuffer.position() + bytesToNextSpeedChange));
|
inputBuffer.limit(min(inputBufferLimit, inputBuffer.position() + bytesToNextSpeedChange));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,7 +151,10 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||||
}
|
}
|
||||||
buffer.flip();
|
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();
|
updateLastProcessedInputTime();
|
||||||
inputBuffer.limit(inputBufferLimit);
|
inputBuffer.limit(inputBufferLimit);
|
||||||
}
|
}
|
||||||
|
|
@ -414,15 +398,4 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
|
||||||
// because some clients register callbacks with getSpeedAdjustedTimeAsync before this audio
|
// because some clients register callbacks with getSpeedAdjustedTimeAsync before this audio
|
||||||
// processor is flushed.
|
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.common.util;
|
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.floor;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
|
@ -48,4 +51,42 @@ public class SpeedProviderUtil {
|
||||||
// Use floor to be consistent with Util#scaleLargeTimestamp().
|
// Use floor to be consistent with Util#scaleLargeTimestamp().
|
||||||
return (long) floor(outputDurationUs);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,13 +251,18 @@ public class SpeedChangingAudioProcessorTest {
|
||||||
getConfiguredSpeedChangingAudioProcessor(speedProvider);
|
getConfiguredSpeedChangingAudioProcessor(speedProvider);
|
||||||
ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5);
|
ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5);
|
||||||
|
|
||||||
|
// SpeedChangingAudioProcessor only queues samples until the next speed change.
|
||||||
|
while (inputBuffer.hasRemaining()) {
|
||||||
speedChangingAudioProcessor.queueInput(inputBuffer);
|
speedChangingAudioProcessor.queueInput(inputBuffer);
|
||||||
outputFrames +=
|
outputFrames +=
|
||||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
speedChangingAudioProcessor.queueEndOfStream();
|
speedChangingAudioProcessor.queueEndOfStream();
|
||||||
outputFrames +=
|
outputFrames +=
|
||||||
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
|
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
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@
|
||||||
package androidx.media3.common.util;
|
package androidx.media3.common.util;
|
||||||
|
|
||||||
import static androidx.media3.common.util.SpeedProviderUtil.getDurationAfterSpeedProviderApplied;
|
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 static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.audio.SpeedProvider;
|
import androidx.media3.common.audio.SpeedProvider;
|
||||||
import androidx.media3.test.utils.TestSpeedProvider;
|
import androidx.media3.test.utils.TestSpeedProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
@ -48,4 +51,153 @@ public class SpeedProviderUtilTest {
|
||||||
assertThat(getDurationAfterSpeedProviderApplied(speedProvider, /* durationUs= */ 113))
|
assertThat(getDurationAfterSpeedProviderApplied(speedProvider, /* durationUs= */ 113))
|
||||||
.isEqualTo(56);
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue