mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue