Skip to content

Commit c8044ca

Browse files
toniheicopybara-github
authored andcommitted
Move AudioTrack interactions into separate class
And add an AudioOutput interface for the calls from DefaultAudioSink. The classes are package-private for now, but will allow other types of output paths in the future that don't rely on AudioTrack. PiperOrigin-RevId: 819753445
1 parent 6f94099 commit c8044ca

File tree

7 files changed

+1582
-821
lines changed

7 files changed

+1582
-821
lines changed

libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java

Lines changed: 103 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import java.nio.ByteBuffer;
3232
import java.nio.ByteOrder;
3333
import java.util.ArrayList;
34+
import java.util.concurrent.atomic.AtomicBoolean;
3435
import java.util.concurrent.atomic.AtomicInteger;
36+
import java.util.concurrent.atomic.AtomicReference;
3537
import org.junit.Test;
3638
import org.junit.runner.RunWith;
3739

@@ -43,43 +45,47 @@ public class DefaultAudioSinkTest {
4345
@SdkSuppress(minSdkVersion = 24) // TODO: b/399130330 - Debug why this fails on API 23.
4446
public void audioTrackExceedsSharedMemory_retriesUntilOngoingReleasesAreDone() throws Exception {
4547
Context context = ApplicationProvider.getApplicationContext();
46-
getInstrumentation()
47-
.runOnMainSync(
48-
() -> {
49-
// Create audio sinks in parallel until we exceed the device's shared audio memory.
50-
ArrayList<DefaultAudioSink> audioSinks = new ArrayList<>();
51-
while (true) {
52-
DefaultAudioSink audioSink = new DefaultAudioSink.Builder(context).build();
53-
audioSinks.add(audioSink);
54-
try {
55-
configureAudioSinkAndFeedData(audioSink);
56-
} catch (Exception e) {
57-
// Expected to happen once we reached the shared audio memory limit of the device.
58-
break;
59-
}
60-
}
61-
62-
// Trigger release of one sink and immediately try the failed sink again. This should
63-
// now succeed even if the sink is released asynchronously.
64-
audioSinks.get(0).flush();
65-
audioSinks.get(0).release();
66-
try {
67-
configureAudioSinkAndFeedData(getLast(audioSinks));
68-
} catch (Exception e) {
69-
throw new IllegalStateException(e);
70-
}
48+
// Create audio sinks in parallel until we exceed the device's shared audio memory.
49+
ArrayList<DefaultAudioSink> audioSinks = new ArrayList<>();
50+
while (true) {
51+
runOnMainSync(
52+
() -> {
53+
DefaultAudioSink audioSink = new DefaultAudioSink.Builder(context).build();
54+
audioSinks.add(audioSink);
55+
});
56+
try {
57+
configureAudioSinkAndFeedData(getLast(audioSinks));
58+
} catch (Exception e) {
59+
// Expected to happen once we reached the shared audio memory limit of the device.
60+
break;
61+
}
62+
}
63+
// Trigger release of one sink and immediately try the failed sink again. This should
64+
// now succeed even if the sink is released asynchronously.
65+
runOnMainSync(
66+
() -> {
67+
audioSinks.get(0).flush();
68+
audioSinks.get(0).release();
69+
});
70+
try {
71+
configureAudioSinkAndFeedData(getLast(audioSinks));
72+
} catch (Exception e) {
73+
throw new IllegalStateException(e);
74+
}
7175

72-
// Clean-up
73-
for (int i = 1; i < audioSinks.size(); i++) {
74-
audioSinks.get(i).flush();
75-
audioSinks.get(i).release();
76-
}
77-
});
76+
// Clean-up
77+
runOnMainSync(
78+
() -> {
79+
for (int i = 1; i < audioSinks.size(); i++) {
80+
audioSinks.get(i).flush();
81+
audioSinks.get(i).release();
82+
}
83+
});
7884
}
7985

8086
@Test
8187
@SdkSuppress(minSdkVersion = 24) // The test depends on AudioTrack#getUnderrunCount() (API 24+).
82-
public void audioTrackUnderruns_callsOnUnderrun() throws InterruptedException {
88+
public void audioTrackUnderruns_callsOnUnderrun() throws Exception {
8389
AtomicInteger underrunCount = new AtomicInteger();
8490
DefaultAudioSink sink =
8591
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
@@ -113,54 +119,79 @@ public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {}
113119
sampleCountToDurationUs(/* sampleCount= */ 25, /* sampleRate= */ 44_100);
114120
ByteBuffer smallBuffer = ByteBuffer.allocateDirect(50).order(ByteOrder.nativeOrder());
115121

122+
runOnMainSync(
123+
() -> {
124+
try {
125+
// Set buffer size of ~1.1ms. The tiny size helps cause an underrun.
126+
sink.configure(format, /* specifiedBufferSize= */ 100, /* outputChannels= */ null);
127+
128+
// Prime AudioTrack with buffer larger than start threshold. Otherwise, AudioTrack
129+
// won't start playing.
130+
sink.handleBuffer(
131+
bigBuffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
132+
sink.play();
133+
// Sleep until AudioTrack starts running out of queued samples.
134+
Thread.sleep(usToMs(bigBufferDurationUs));
135+
for (int i = 0; i < 5; i++) {
136+
smallBuffer.rewind();
137+
// Queue small buffer so that sink buffer is never filled up.
138+
sink.handleBuffer(
139+
smallBuffer,
140+
/* presentationTimeUs= */ bigBufferDurationUs + smallBufferDurationUs * i,
141+
/* encodedAccessUnitCount= */ 1);
142+
// Add additional latency so loop can never fill up sink buffer quickly enough.
143+
Thread.sleep(20);
144+
}
145+
} catch (Exception e) {
146+
throw new RuntimeException(e);
147+
}
148+
});
149+
150+
assertThat(underrunCount.get()).isGreaterThan(0);
151+
}
152+
153+
private void configureAudioSinkAndFeedData(DefaultAudioSink audioSink) throws Exception {
154+
ByteBuffer buffer = ByteBuffer.allocateDirect(8000).order(ByteOrder.nativeOrder());
155+
runOnMainSync(
156+
() -> {
157+
Format format =
158+
new Format.Builder()
159+
.setSampleMimeType(MimeTypes.AUDIO_RAW)
160+
.setPcmEncoding(C.ENCODING_PCM_16BIT)
161+
.setChannelCount(2)
162+
.setSampleRate(44_100)
163+
.build();
164+
audioSink.configure(
165+
format, /* specifiedBufferSize= */ 2_000_000, /* outputChannels= */ null);
166+
audioSink.play();
167+
});
168+
AtomicBoolean handledBuffer = new AtomicBoolean();
169+
while (!handledBuffer.get()) {
170+
runOnMainSync(
171+
() ->
172+
handledBuffer.set(
173+
audioSink.handleBuffer(
174+
buffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1)));
175+
}
176+
}
177+
178+
private static void runOnMainSync(ThrowingRunnable runnable) throws Exception {
179+
AtomicReference<Exception> exceptionOnMain = new AtomicReference<>();
116180
getInstrumentation()
117181
.runOnMainSync(
118182
() -> {
119183
try {
120-
// Set buffer size of ~1.1ms. The tiny size helps cause an underrun.
121-
sink.configure(format, /* specifiedBufferSize= */ 100, /* outputChannels= */ null);
122-
123-
// Prime AudioTrack with buffer larger than start threshold. Otherwise, AudioTrack
124-
// won't start playing.
125-
sink.handleBuffer(
126-
bigBuffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
127-
sink.play();
128-
// Sleep until AudioTrack starts running out of queued samples.
129-
Thread.sleep(usToMs(bigBufferDurationUs));
130-
for (int i = 0; i < 5; i++) {
131-
smallBuffer.rewind();
132-
// Queue small buffer so that sink buffer is never filled up.
133-
sink.handleBuffer(
134-
smallBuffer,
135-
/* presentationTimeUs= */ bigBufferDurationUs + smallBufferDurationUs * i,
136-
/* encodedAccessUnitCount= */ 1);
137-
// Add additional latency so loop can never fill up sink buffer quickly enough.
138-
Thread.sleep(20);
139-
}
184+
runnable.run();
140185
} catch (Exception e) {
141-
throw new RuntimeException(e);
186+
exceptionOnMain.set(e);
142187
}
143188
});
144-
145-
assertThat(underrunCount.get()).isGreaterThan(0);
189+
if (exceptionOnMain.get() != null) {
190+
throw exceptionOnMain.get();
191+
}
146192
}
147193

148-
private void configureAudioSinkAndFeedData(DefaultAudioSink audioSink) throws Exception {
149-
Format format =
150-
new Format.Builder()
151-
.setSampleMimeType(MimeTypes.AUDIO_RAW)
152-
.setPcmEncoding(C.ENCODING_PCM_16BIT)
153-
.setChannelCount(2)
154-
.setSampleRate(44_100)
155-
.build();
156-
audioSink.configure(format, /* specifiedBufferSize= */ 2_000_000, /* outputChannels= */ null);
157-
audioSink.play();
158-
ByteBuffer buffer = ByteBuffer.allocateDirect(8000).order(ByteOrder.nativeOrder());
159-
boolean handledBuffer = false;
160-
while (!handledBuffer) {
161-
handledBuffer =
162-
audioSink.handleBuffer(
163-
buffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
164-
}
194+
private interface ThrowingRunnable {
195+
void run() throws Exception;
165196
}
166197
}

0 commit comments

Comments
 (0)