Implement static method to return estimated output samples

This method will replace the current estimation mechanism in place on
`getSpeedAdjustedTimeAsync()`, which relies on heuristics from previous
output rates and depends on the state of the `AudioProcessor`. The new
static method should be considerably more accurate.

PiperOrigin-RevId: 694607264
This commit is contained in:
ivanbuper 2024-11-08 12:51:31 -08:00 committed by Copybara-Service
parent 53953dd377
commit f84f44bf6b
3 changed files with 145 additions and 11 deletions

View File

@ -25,6 +25,7 @@ import static java.lang.Math.min;
import static java.lang.Math.round; import static java.lang.Math.round;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.LongArray; import androidx.media3.common.util.LongArray;
import androidx.media3.common.util.LongArrayQueue; import androidx.media3.common.util.LongArrayQueue;
@ -105,6 +106,42 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
resetState(); resetState();
} }
/** Returns the estimated number of samples output given the provided parameters. */
public static long getSampleCountAfterProcessorApplied(
SpeedProvider speedProvider,
@IntRange(from = 1) int inputSampleRateHz,
@IntRange(from = 1) long inputSamples) {
checkArgument(speedProvider != null);
checkArgument(inputSampleRateHz > 0);
checkArgument(inputSamples > 0);
long outputSamples = 0;
long positionSamples = 0;
while (positionSamples < inputSamples) {
long boundarySamples =
getNextSpeedChangeSamplePosition(speedProvider, positionSamples, inputSampleRateHz);
if (boundarySamples == C.INDEX_UNSET || boundarySamples > inputSamples) {
boundarySamples = inputSamples;
}
float speed = getSampleAlignedSpeed(speedProvider, positionSamples, inputSampleRateHz);
// Input and output sample rates match because SpeedChangingAudioProcessor does not modify the
// output sample rate.
outputSamples +=
Sonic.getExpectedFrameCountAfterProcessorApplied(
/* inputSampleRateHz= */ inputSampleRateHz,
/* outputSampleRateHz= */ inputSampleRateHz,
/* speed= */ speed,
/* pitch= */ speed,
/* inputFrameCount= */ boundarySamples - positionSamples);
positionSamples = boundarySamples;
}
return outputSamples;
}
@Override @Override
public long getDurationAfterProcessorApplied(long durationUs) { public long getDurationAfterProcessorApplied(long durationUs) {
return SpeedProviderUtil.getDurationAfterSpeedProviderApplied(speedProvider, durationUs); return SpeedProviderUtil.getDurationAfterSpeedProviderApplied(speedProvider, durationUs);

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.common.audio; package androidx.media3.common.audio;
import static androidx.media3.common.audio.SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied;
import static androidx.media3.test.utils.TestUtil.buildTestData; import static androidx.media3.test.utils.TestUtil.buildTestData;
import static androidx.media3.test.utils.TestUtil.generateFloatInRange; import static androidx.media3.test.utils.TestUtil.generateFloatInRange;
import static androidx.media3.test.utils.TestUtil.generateLong; import static androidx.media3.test.utils.TestUtil.generateLong;
@ -104,18 +105,9 @@ public class RandomParameterizedSpeedChangingAudioProcessorTest {
ByteBuffer outBuffer; ByteBuffer outBuffer;
long outputFrameCount = 0; long outputFrameCount = 0;
long totalInputFrameCount = 0; long totalInputFrameCount = 0;
long expectedOutputFrames = 0;
for (int i = 0; i < frameCounts.size(); i++) { for (int i = 0; i < frameCounts.size(); i++) {
totalInputFrameCount += frameCounts.get(i); totalInputFrameCount += frameCounts.get(i);
float speed = speeds.get(i).floatValue();
expectedOutputFrames +=
Sonic.getExpectedFrameCountAfterProcessorApplied(
/* inputSampleRateHz= */ AUDIO_FORMAT.sampleRate,
/* outputSampleRateHz= */ AUDIO_FORMAT.sampleRate,
/* speed= */ speed,
/* pitch= */ speed,
/* inputFrameCount= */ frameCounts.get(i));
} }
SpeedProvider speedProvider = SpeedProvider speedProvider =
@ -124,6 +116,10 @@ public class RandomParameterizedSpeedChangingAudioProcessorTest {
/* frameCounts= */ Ints.toArray(frameCounts), /* frameCounts= */ Ints.toArray(frameCounts),
/* speeds= */ Floats.toArray(speeds)); /* speeds= */ Floats.toArray(speeds));
long expectedOutputFrames =
getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, totalInputFrameCount);
SpeedChangingAudioProcessor speedChangingAudioProcessor = SpeedChangingAudioProcessor speedChangingAudioProcessor =
new SpeedChangingAudioProcessor(speedProvider); new SpeedChangingAudioProcessor(speedProvider);
speedChangingAudioProcessor.configure(AUDIO_FORMAT); speedChangingAudioProcessor.configure(AUDIO_FORMAT);

View File

@ -18,8 +18,11 @@ package androidx.media3.common.audio;
import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER; import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.annotation.SuppressLint;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.audio.AudioProcessor.AudioFormat;
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;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -32,8 +35,8 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SpeedChangingAudioProcessorTest { public class SpeedChangingAudioProcessorTest {
private static final AudioProcessor.AudioFormat AUDIO_FORMAT = private static final AudioFormat AUDIO_FORMAT =
new AudioProcessor.AudioFormat( new AudioFormat(
/* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT); /* sampleRate= */ 44100, /* channelCount= */ 2, /* encoding= */ C.ENCODING_PCM_16BIT);
@Test @Test
@ -600,6 +603,104 @@ public class SpeedChangingAudioProcessorTest {
assertThat(outputFrameCount).isWithin(1).of(4); assertThat(outputFrameCount).isWithin(1).of(4);
} }
@Test
public void getSampleCountAfterProcessorApplied_withConstantSpeed_outputsExpectedSamples() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
new AudioFormat(/* sampleRate= */ 48000, /* channelCount= */ 1, C.ENCODING_PCM_16BIT),
/* frameCounts= */ new int[] {100},
/* speeds= */ new float[] {2.f});
long sampleCountAfterProcessorApplied =
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 100);
assertThat(sampleCountAfterProcessorApplied).isEqualTo(50);
}
@Test
public void getSampleCountAfterProcessorApplied_withMultipleSpeeds_outputsExpectedSamples() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {100, 400, 50},
/* speeds= */ new float[] {2.f, 4f, 0.5f});
long sampleCountAfterProcessorApplied =
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 550);
assertThat(sampleCountAfterProcessorApplied).isEqualTo(250);
}
@Test
public void
getSampleCountAfterProcessorApplied_beyondLastSpeedRegion_stillAppliesLastSpeedValue() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {100, 400, 50},
/* speeds= */ new float[] {2.f, 4f, 0.5f});
long sampleCountAfterProcessorApplied =
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3000);
assertThat(sampleCountAfterProcessorApplied).isEqualTo(5150);
}
@Test
public void
getSampleCountAfterProcessorApplied_withInputCountBeyondIntRange_outputsExpectedSamples() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {1000, 10000, 8200},
/* speeds= */ new float[] {0.2f, 8f, 0.5f});
long sampleCountAfterProcessorApplied =
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 3_000_000_000L);
assertThat(sampleCountAfterProcessorApplied).isEqualTo(5999984250L);
}
// Testing range validation.
@SuppressLint("Range")
@Test
public void getSampleCountAfterProcessorApplied_withNegativeSampleCount_throws() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {1000, 10000, 8200},
/* speeds= */ new float[] {0.2f, 8f, 0.5f});
assertThrows(
IllegalArgumentException.class,
() ->
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, AUDIO_FORMAT.sampleRate, /* inputSamples= */ -2L));
}
// Testing range validation.
@SuppressLint("Range")
@Test
public void getSampleCountAfterProcessorApplied_withZeroSampleRate_throws() {
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {1000, 10000, 8200},
/* speeds= */ new float[] {0.2f, 8f, 0.5f});
assertThrows(
IllegalArgumentException.class,
() ->
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
speedProvider, /* inputSampleRateHz= */ 0, /* inputSamples= */ 1000L));
}
@Test
public void getSampleCountAfterProcessorApplied_withNullSpeedProvider_throws() {
assertThrows(
IllegalArgumentException.class,
() ->
SpeedChangingAudioProcessor.getSampleCountAfterProcessorApplied(
/* speedProvider= */ null, AUDIO_FORMAT.sampleRate, /* inputSamples= */ 1000L));
}
private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor( private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor(
SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException { SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException {
SpeedChangingAudioProcessor speedChangingAudioProcessor = SpeedChangingAudioProcessor speedChangingAudioProcessor =