Avoid dropped output frames on SpeedChangingAudioProcessor

Inconsistent rounding modes between `currentTimeUs` and
`bytesUntilNextSpeedChange` would cause `SpeedChangingAudioProcessor`
to miss calling `queueEndOfStream()` on `SonicAudioProcessor` on a speed
change, and thus the final output samples of that `SonicAudioProcessor`
"configuration" would be missed.

This change is also a partial revert of 971486f5f9, which fixed a hang
of `SpeedChangingAudioProcessor`, but introduced the dropped output
frames issue fixed in this CL (see b/372203420). To avoid reintroducing
the hang, we are now ignoring any mid-sample speed changes and will only
apply speed changes that are effective at a whole sample position.

PiperOrigin-RevId: 684824218
This commit is contained in:
ivanbuper 2024-10-11 06:59:43 -07:00 committed by Copybara-Service
parent 73f97c0371
commit 984b0bb31a
3 changed files with 100 additions and 12 deletions

View File

@ -67,6 +67,8 @@
* Fix pop sounds that may occur during seeks. * Fix pop sounds that may occur during seeks.
* Fix truncation error accumulation for Sonic's * Fix truncation error accumulation for Sonic's
time-stretching/pitch-shifting algorithm. time-stretching/pitch-shifting algorithm.
* Fix bug in `SpeedChangingAudioProcessor` that causes dropped output
frames.
* Video: * Video:
* Add workaround for a device issue on Galaxy Tab S7 FE that causes 60fps * Add workaround for a device issue on Galaxy Tab S7 FE that causes 60fps
secure H264 streams to be marked as unsupported secure H264 streams to be marked as unsupported

View File

@ -28,7 +28,6 @@ import androidx.media3.common.util.SpeedProviderUtil;
import androidx.media3.common.util.TimestampConsumer; import androidx.media3.common.util.TimestampConsumer;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import java.math.RoundingMode;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Queue; import java.util.Queue;
@ -121,21 +120,33 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor {
/* multiplier= */ C.MICROS_PER_SECOND, /* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame); /* divisor= */ (long) inputAudioFormat.sampleRate * inputAudioFormat.bytesPerFrame);
float newSpeed = speedProvider.getSpeed(currentTimeUs); 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 =
Util.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();
long nextSpeedChangeTimeUs = speedProvider.getNextSpeedChangeTimeUs(currentTimeUs);
int bytesToNextSpeedChange; int bytesToNextSpeedChange;
if (nextSpeedChangeTimeUs != C.TIME_UNSET) { if (nextSpeedChangeTimeUs != C.TIME_UNSET) {
bytesToNextSpeedChange = bytesToNextSpeedChange =
(int) (int)
Util.scaleLargeValue( Util.scaleLargeTimestamp(
/* timestamp */ nextSpeedChangeTimeUs - currentTimeUs, /* timestamp= */ nextSpeedChangeTimeUs - currentTimeUs,
/* multiplier= */ (long) inputAudioFormat.sampleRate /* multiplier= */ (long) inputAudioFormat.sampleRate
* inputAudioFormat.bytesPerFrame, * inputAudioFormat.bytesPerFrame,
/* divisor= */ C.MICROS_PER_SECOND, /* divisor= */ C.MICROS_PER_SECOND);
RoundingMode.CEILING);
int bytesToNextFrame = int bytesToNextFrame =
inputAudioFormat.bytesPerFrame - bytesToNextSpeedChange % inputAudioFormat.bytesPerFrame; inputAudioFormat.bytesPerFrame - bytesToNextSpeedChange % inputAudioFormat.bytesPerFrame;
if (bytesToNextFrame != inputAudioFormat.bytesPerFrame) { if (bytesToNextFrame != inputAudioFormat.bytesPerFrame) {
@ -410,4 +421,15 @@ 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);
}
} }

View File

@ -240,9 +240,9 @@ public class SpeedChangingAudioProcessorTest {
} }
@Test @Test
public void queueInput_multipleSpeedsInBufferWithLimitVeryClose_readsDataUntilSpeedLimit() public void queueInput_multipleSpeedsInBufferWithLimitVeryClose_doesNotHang() throws Exception {
throws Exception {
long speedChangeTimeUs = 1; // Change speed very close to current position at 1us. long speedChangeTimeUs = 1; // Change speed very close to current position at 1us.
int outputFrames = 0;
SpeedProvider speedProvider = SpeedProvider speedProvider =
TestSpeedProvider.createWithStartTimes( TestSpeedProvider.createWithStartTimes(
/* startTimesUs= */ new long[] {0L, speedChangeTimeUs}, /* startTimesUs= */ new long[] {0L, speedChangeTimeUs},
@ -250,12 +250,14 @@ public class SpeedChangingAudioProcessorTest {
SpeedChangingAudioProcessor speedChangingAudioProcessor = SpeedChangingAudioProcessor speedChangingAudioProcessor =
getConfiguredSpeedChangingAudioProcessor(speedProvider); getConfiguredSpeedChangingAudioProcessor(speedProvider);
ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5); ByteBuffer inputBuffer = getInputBuffer(/* frameCount= */ 5);
int inputBufferLimit = inputBuffer.limit();
speedChangingAudioProcessor.queueInput(inputBuffer); speedChangingAudioProcessor.queueInput(inputBuffer);
outputFrames +=
assertThat(inputBuffer.position()).isEqualTo(AUDIO_FORMAT.bytesPerFrame); speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
assertThat(inputBuffer.limit()).isEqualTo(inputBufferLimit); speedChangingAudioProcessor.queueEndOfStream();
outputFrames +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
assertThat(outputFrames).isEqualTo(3);
} }
@Test @Test
@ -531,6 +533,68 @@ public class SpeedChangingAudioProcessorTest {
.isEqualTo(40_000); .isEqualTo(40_000);
} }
@Test
public void queueInput_exactlyUpToSpeedBoundary_outputsExpectedNumberOfSamples()
throws AudioProcessor.UnhandledAudioFormatException {
int outputFrameCount = 0;
SpeedProvider speedProvider =
TestSpeedProvider.createWithFrameCounts(
AUDIO_FORMAT,
/* frameCounts= */ new int[] {1000, 1000, 1000},
/* speeds= */ new float[] {2, 4, 2}); // 500, 250, 500 = 1250
SpeedChangingAudioProcessor speedChangingAudioProcessor =
getConfiguredSpeedChangingAudioProcessor(speedProvider);
ByteBuffer input = getInputBuffer(1000);
speedChangingAudioProcessor.queueInput(input);
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
input.rewind();
speedChangingAudioProcessor.queueInput(input);
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
input.rewind();
speedChangingAudioProcessor.queueInput(input);
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
speedChangingAudioProcessor.queueEndOfStream();
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
assertThat(outputFrameCount).isWithin(2).of(1250);
}
@Test
public void queueInput_withUnalignedSpeedStartTimes_skipsMidSampleSpeedChanges()
throws AudioProcessor.UnhandledAudioFormatException {
int outputFrameCount = 0;
// Sample duration @44.1KHz is 22.67573696145125us. The last three speed changes fall between
// samples 4 and 5, so only the speed change at 105us should be used. We expect an output of
// 4 / 2 + 8 / 4 = 4 samples.
SpeedProvider speedProvider =
TestSpeedProvider.createWithStartTimes(
/* startTimesUs= */ new long[] {0, 95, 100, 105},
/* speeds= */ new float[] {2, 3, 8, 4});
SpeedChangingAudioProcessor speedChangingAudioProcessor =
getConfiguredSpeedChangingAudioProcessor(speedProvider);
ByteBuffer input = getInputBuffer(12);
while (input.hasRemaining()) {
speedChangingAudioProcessor.queueInput(input);
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
}
speedChangingAudioProcessor.queueEndOfStream();
outputFrameCount +=
speedChangingAudioProcessor.getOutput().remaining() / AUDIO_FORMAT.bytesPerFrame;
// Allow one sample of tolerance per effectively applied speed change.
assertThat(outputFrameCount).isWithin(1).of(4);
}
private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor( private static SpeedChangingAudioProcessor getConfiguredSpeedChangingAudioProcessor(
SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException { SpeedProvider speedProvider) throws AudioProcessor.UnhandledAudioFormatException {
SpeedChangingAudioProcessor speedChangingAudioProcessor = SpeedChangingAudioProcessor speedChangingAudioProcessor =