mirror of
https://github.com/androidx/media.git
synced 2025-05-18 04:59:54 +08:00
Detect underruns with AudioTrack#getUnderrunCount() in DefaultAudioSink
Prior to this change, DefaultAudioSink (via AudioTrackPositionTracker) would use best-effort logic to infer underruns in the underlying AudioTrack. This logic would miss underrun events (e.g. newly added test fails if run without any changes to AudioTrackPositionTracker). This change should help more accurately detect regressions in the audio pipeline. PiperOrigin-RevId: 753550187
This commit is contained in:
parent
8bcef5df6d
commit
e06e9b4cc9
@ -35,6 +35,9 @@
|
|||||||
* Make `ChannelMappingAudioProcessor`, `TrimmingAudioProcessor` and
|
* Make `ChannelMappingAudioProcessor`, `TrimmingAudioProcessor` and
|
||||||
`ToFloatPcmAudioProcessor` public
|
`ToFloatPcmAudioProcessor` public
|
||||||
([#2339](https://github.com/androidx/media/issues/2339)).
|
([#2339](https://github.com/androidx/media/issues/2339)).
|
||||||
|
* Use `AudioTrack#getUnderrunCount()` in `AudioTrackPositionTracker` to
|
||||||
|
detect underruns in `DefaultAudioSink` instead of best-effort
|
||||||
|
estimation.
|
||||||
* Video:
|
* Video:
|
||||||
* Add experimental `ExoPlayer` API to include the
|
* Add experimental `ExoPlayer` API to include the
|
||||||
`MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input
|
`MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input
|
||||||
|
@ -15,8 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.audio;
|
package androidx.media3.exoplayer.audio;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Util.sampleCountToDurationUs;
|
||||||
|
import static androidx.media3.common.util.Util.usToMs;
|
||||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||||
import static com.google.common.collect.Iterables.getLast;
|
import static com.google.common.collect.Iterables.getLast;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -28,6 +31,7 @@ import androidx.test.filters.SdkSuppress;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@ -73,6 +77,74 @@ public class DefaultAudioSinkTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SdkSuppress(minSdkVersion = 24) // The test depends on AudioTrack#getUnderrunCount() (API 24+).
|
||||||
|
public void audioTrackUnderruns_callsOnUnderrun() throws InterruptedException {
|
||||||
|
AtomicInteger underrunCount = new AtomicInteger();
|
||||||
|
DefaultAudioSink sink =
|
||||||
|
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
|
||||||
|
sink.setListener(
|
||||||
|
new AudioSink.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||||
|
underrunCount.addAndGet(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
|
||||||
|
});
|
||||||
|
Format format =
|
||||||
|
new Format.Builder()
|
||||||
|
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||||
|
.setPcmEncoding(C.ENCODING_PCM_16BIT)
|
||||||
|
.setChannelCount(1)
|
||||||
|
.setSampleRate(44_100)
|
||||||
|
.build();
|
||||||
|
// Create a big buffer to prime the sink's AudioTrack (~113ms).
|
||||||
|
long bigBufferDurationUs =
|
||||||
|
sampleCountToDurationUs(/* sampleCount= */ 5000, /* sampleRate= */ 44_100);
|
||||||
|
ByteBuffer bigBuffer = ByteBuffer.allocateDirect(5000 * 2).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
// Create a buffer smaller than sink buffer size to eventually cause an underrun (~567us).
|
||||||
|
long smallBufferDurationUs =
|
||||||
|
sampleCountToDurationUs(/* sampleCount= */ 25, /* sampleRate= */ 44_100);
|
||||||
|
ByteBuffer smallBuffer = ByteBuffer.allocateDirect(50).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
// Set buffer size of ~1.1ms. The tiny size helps cause an underrun.
|
||||||
|
sink.configure(format, /* specifiedBufferSize= */ 100, /* outputChannels= */ null);
|
||||||
|
|
||||||
|
// Prime AudioTrack with buffer larger than start threshold. Otherwise, AudioTrack
|
||||||
|
// won't start playing.
|
||||||
|
sink.handleBuffer(
|
||||||
|
bigBuffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
|
||||||
|
sink.play();
|
||||||
|
// Sleep until AudioTrack starts running out of queued samples.
|
||||||
|
Thread.sleep(usToMs(bigBufferDurationUs));
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
smallBuffer.rewind();
|
||||||
|
// Queue small buffer so that sink buffer is never filled up.
|
||||||
|
sink.handleBuffer(
|
||||||
|
smallBuffer,
|
||||||
|
/* presentationTimeUs= */ bigBufferDurationUs + smallBufferDurationUs * i,
|
||||||
|
/* encodedAccessUnitCount= */ 1);
|
||||||
|
// Add additional latency so loop can never fill up sink buffer quickly enough.
|
||||||
|
Thread.sleep(20);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThat(underrunCount.get()).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
private void configureAudioSinkAndFeedData(DefaultAudioSink audioSink) throws Exception {
|
private void configureAudioSinkAndFeedData(DefaultAudioSink audioSink) throws Exception {
|
||||||
Format format =
|
Format format =
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
|
@ -29,6 +29,7 @@ import android.media.AudioTimestamp;
|
|||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
@ -175,6 +176,7 @@ import java.lang.reflect.Method;
|
|||||||
private long bufferSizeUs;
|
private long bufferSizeUs;
|
||||||
private float audioTrackPlaybackSpeed;
|
private float audioTrackPlaybackSpeed;
|
||||||
private boolean notifiedPositionIncreasing;
|
private boolean notifiedPositionIncreasing;
|
||||||
|
private int lastUnderrunCount;
|
||||||
|
|
||||||
private long smoothedPlayheadOffsetUs;
|
private long smoothedPlayheadOffsetUs;
|
||||||
private long lastPlayheadSampleTimeUs;
|
private long lastPlayheadSampleTimeUs;
|
||||||
@ -273,6 +275,7 @@ import java.lang.reflect.Method;
|
|||||||
lastLatencySampleTimeUs = 0;
|
lastLatencySampleTimeUs = 0;
|
||||||
latencyUs = 0;
|
latencyUs = 0;
|
||||||
audioTrackPlaybackSpeed = 1f;
|
audioTrackPlaybackSpeed = 1f;
|
||||||
|
lastUnderrunCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAudioTrackPlaybackSpeed(float audioTrackPlaybackSpeed) {
|
public void setAudioTrackPlaybackSpeed(float audioTrackPlaybackSpeed) {
|
||||||
@ -408,9 +411,17 @@ import java.lang.reflect.Method;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hadData = hasData;
|
boolean emitUnderrun;
|
||||||
hasData = hasPendingData(writtenFrames);
|
if (SDK_INT >= 24) {
|
||||||
if (hadData && !hasData && playState != PLAYSTATE_STOPPED) {
|
emitUnderrun = hasPendingAudioTrackUnderruns();
|
||||||
|
} else {
|
||||||
|
boolean hadData = hasData;
|
||||||
|
hasData = hasPendingData(writtenFrames);
|
||||||
|
// For API 23- AudioTrack has no underrun API so we need to infer underruns heuristically.
|
||||||
|
emitUnderrun = hadData && !hasData && playState != PLAYSTATE_STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emitUnderrun) {
|
||||||
listener.onUnderrun(bufferSize, Util.usToMs(bufferSizeUs));
|
listener.onUnderrun(bufferSize, Util.usToMs(bufferSizeUs));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,6 +521,21 @@ import java.lang.reflect.Method;
|
|||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@link #audioTrack} has reported one or more underruns since the last call to
|
||||||
|
* this method.
|
||||||
|
*/
|
||||||
|
@RequiresApi(24)
|
||||||
|
private boolean hasPendingAudioTrackUnderruns() {
|
||||||
|
int underrunCount = checkNotNull(audioTrack).getUnderrunCount();
|
||||||
|
boolean result = underrunCount > lastUnderrunCount;
|
||||||
|
|
||||||
|
// If the AudioTrack unexpectedly resets the underrun count, we should update it silently.
|
||||||
|
lastUnderrunCount = underrunCount;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private void maybeSampleSyncParams() {
|
private void maybeSampleSyncParams() {
|
||||||
long systemTimeUs = clock.nanoTime() / 1000;
|
long systemTimeUs = clock.nanoTime() / 1000;
|
||||||
if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
|
if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user