Use low latency mode on Android O and later.
This CL makes it possible to use a low-latency mode on Android O and later. This should help to reduce the audio latency. The feature is disabled by default and needs to be enabled when creating the audio device module.
Bug: webrtc:12284
Change-Id: Idf41146aa0bc1206e9a2e28e4101d85c3e4eaefc
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/196741
Reviewed-by: Sami Kalliomäki <sakal@webrtc.org>
Reviewed-by: Henrik Andreassson <henrika@webrtc.org>
Commit-Queue: Ivo Creusen <ivoc@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#32854}
diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn
index 6adb8fa..ce5db77 100644
--- a/sdk/android/BUILD.gn
+++ b/sdk/android/BUILD.gn
@@ -425,6 +425,7 @@
visibility = [ "*" ]
sources = [
"api/org/webrtc/audio/JavaAudioDeviceModule.java",
+ "src/java/org/webrtc/audio/LowLatencyAudioBufferManager.java",
"src/java/org/webrtc/audio/VolumeLogger.java",
"src/java/org/webrtc/audio/WebRtcAudioEffects.java",
"src/java/org/webrtc/audio/WebRtcAudioManager.java",
@@ -1534,12 +1535,14 @@
"tests/src/org/webrtc/IceCandidateTest.java",
"tests/src/org/webrtc/RefCountDelegateTest.java",
"tests/src/org/webrtc/ScalingSettingsTest.java",
+ "tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java",
]
deps = [
":base_java",
":camera_java",
":hwcodecs_java",
+ ":java_audio_device_module_java",
":libjingle_peerconnection_java",
":peerconnection_java",
":video_api_java",
diff --git a/sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java b/sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java
index 43fce4f..4ca6466 100644
--- a/sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java
+++ b/sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java
@@ -49,12 +49,14 @@
private boolean useStereoInput;
private boolean useStereoOutput;
private AudioAttributes audioAttributes;
+ private boolean useLowLatency;
private Builder(Context context) {
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.inputSampleRate = WebRtcAudioManager.getSampleRate(audioManager);
this.outputSampleRate = WebRtcAudioManager.getSampleRate(audioManager);
+ this.useLowLatency = false;
}
public Builder setScheduler(ScheduledExecutorService scheduler) {
@@ -196,6 +198,14 @@
}
/**
+ * Control if the low-latency mode should be used. The default is disabled.
+ */
+ public Builder setUseLowLatency(boolean useLowLatency) {
+ this.useLowLatency = useLowLatency;
+ return this;
+ }
+
+ /**
* Set custom {@link AudioAttributes} to use.
*/
public Builder setAudioAttributes(AudioAttributes audioAttributes) {
@@ -225,6 +235,12 @@
}
Logging.d(TAG, "HW AEC will not be used.");
}
+ // Low-latency mode was introduced in API version 26, see
+ // https://developer.android.com/reference/android/media/AudioTrack#PERFORMANCE_MODE_LOW_LATENCY
+ final int MIN_LOW_LATENCY_SDK_VERSION = 26;
+ if (useLowLatency && Build.VERSION.SDK_INT >= MIN_LOW_LATENCY_SDK_VERSION) {
+ Logging.d(TAG, "Low latency mode will be used.");
+ }
ScheduledExecutorService executor = this.scheduler;
if (executor == null) {
executor = WebRtcAudioRecord.newDefaultScheduler();
@@ -232,8 +248,8 @@
final WebRtcAudioRecord audioInput = new WebRtcAudioRecord(context, executor, audioManager,
audioSource, audioFormat, audioRecordErrorCallback, audioRecordStateCallback,
samplesReadyCallback, useHardwareAcousticEchoCanceler, useHardwareNoiseSuppressor);
- final WebRtcAudioTrack audioOutput = new WebRtcAudioTrack(
- context, audioManager, audioAttributes, audioTrackErrorCallback, audioTrackStateCallback);
+ final WebRtcAudioTrack audioOutput = new WebRtcAudioTrack(context, audioManager,
+ audioAttributes, audioTrackErrorCallback, audioTrackStateCallback, useLowLatency);
return new JavaAudioDeviceModule(context, audioManager, audioInput, audioOutput,
inputSampleRate, outputSampleRate, useStereoInput, useStereoOutput);
}
diff --git a/sdk/android/src/java/org/webrtc/audio/LowLatencyAudioBufferManager.java b/sdk/android/src/java/org/webrtc/audio/LowLatencyAudioBufferManager.java
new file mode 100644
index 0000000..70c625a
--- /dev/null
+++ b/sdk/android/src/java/org/webrtc/audio/LowLatencyAudioBufferManager.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.audio;
+
+import android.media.AudioTrack;
+import android.os.Build;
+import org.webrtc.Logging;
+
+// Lowers the buffer size if no underruns are detected for 100 ms. Once an
+// underrun is detected, the buffer size is increased by 10 ms and it will not
+// be lowered further. The buffer size will never be increased more than
+// 5 times, to avoid the possibility of the buffer size increasing without
+// bounds.
+class LowLatencyAudioBufferManager {
+ private static final String TAG = "LowLatencyAudioBufferManager";
+ // The underrun count that was valid during the previous call to maybeAdjustBufferSize(). Used to
+ // detect increases in the value.
+ private int prevUnderrunCount;
+ // The number of ticks to wait without an underrun before decreasing the buffer size.
+ private int ticksUntilNextDecrease;
+ // Indicate if we should continue to decrease the buffer size.
+ private boolean keepLoweringBufferSize;
+ // How often the buffer size was increased.
+ private int bufferIncreaseCounter;
+
+ public LowLatencyAudioBufferManager() {
+ this.prevUnderrunCount = 0;
+ this.ticksUntilNextDecrease = 10;
+ this.keepLoweringBufferSize = true;
+ this.bufferIncreaseCounter = 0;
+ }
+
+ public void maybeAdjustBufferSize(AudioTrack audioTrack) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ final int underrunCount = audioTrack.getUnderrunCount();
+ if (underrunCount > prevUnderrunCount) {
+ // Don't increase buffer more than 5 times. Continuing to increase the buffer size
+ // could be harmful on low-power devices that regularly experience underruns under
+ // normal conditions.
+ if (bufferIncreaseCounter < 5) {
+ // Underrun detected, increase buffer size by 10ms.
+ final int currentBufferSize = audioTrack.getBufferSizeInFrames();
+ final int newBufferSize = currentBufferSize + audioTrack.getPlaybackRate() / 100;
+ Logging.d(TAG,
+ "Underrun detected! Increasing AudioTrack buffer size from " + currentBufferSize
+ + " to " + newBufferSize);
+ audioTrack.setBufferSizeInFrames(newBufferSize);
+ bufferIncreaseCounter++;
+ }
+ // Stop trying to lower the buffer size.
+ keepLoweringBufferSize = false;
+ prevUnderrunCount = underrunCount;
+ ticksUntilNextDecrease = 10;
+ } else if (keepLoweringBufferSize) {
+ ticksUntilNextDecrease--;
+ if (ticksUntilNextDecrease <= 0) {
+ // No underrun seen for 100 ms, try to lower the buffer size by 10ms.
+ final int bufferSize10ms = audioTrack.getPlaybackRate() / 100;
+ // Never go below a buffer size of 10ms.
+ final int currentBufferSize = audioTrack.getBufferSizeInFrames();
+ final int newBufferSize = Math.max(bufferSize10ms, currentBufferSize - bufferSize10ms);
+ if (newBufferSize != currentBufferSize) {
+ Logging.d(TAG,
+ "Lowering AudioTrack buffer size from " + currentBufferSize + " to "
+ + newBufferSize);
+ audioTrack.setBufferSizeInFrames(newBufferSize);
+ }
+ ticksUntilNextDecrease = 10;
+ }
+ }
+ }
+ }
+}
diff --git a/sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java b/sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java
index a752280..5e1201d 100644
--- a/sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java
+++ b/sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java
@@ -19,7 +19,6 @@
import android.os.Build;
import android.os.Process;
import android.support.annotation.Nullable;
-import java.lang.Thread;
import java.nio.ByteBuffer;
import org.webrtc.CalledByNative;
import org.webrtc.Logging;
@@ -27,6 +26,7 @@
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackErrorCallback;
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackStartErrorCode;
import org.webrtc.audio.JavaAudioDeviceModule.AudioTrackStateCallback;
+import org.webrtc.audio.LowLatencyAudioBufferManager;
class WebRtcAudioTrack {
private static final String TAG = "WebRtcAudioTrackExternal";
@@ -80,6 +80,8 @@
// Can be used to ensure that the speaker is fully muted.
private volatile boolean speakerMute;
private byte[] emptyBytes;
+ private boolean useLowLatency;
+ private int initialBufferSizeInFrames;
private final @Nullable AudioTrackErrorCallback errorCallback;
private final @Nullable AudioTrackStateCallback stateCallback;
@@ -92,9 +94,11 @@
*/
private class AudioTrackThread extends Thread {
private volatile boolean keepAlive = true;
+ private LowLatencyAudioBufferManager bufferManager;
public AudioTrackThread(String name) {
super(name);
+ bufferManager = new LowLatencyAudioBufferManager();
}
@Override
@@ -134,6 +138,9 @@
reportWebRtcAudioTrackError("AudioTrack.write failed: " + bytesWritten);
}
}
+ if (useLowLatency) {
+ bufferManager.maybeAdjustBufferSize(audioTrack);
+ }
// The byte buffer must be rewinded since byteBuffer.position() is
// increased at each call to AudioTrack.write(). If we don't do this,
// next call to AudioTrack.write() will fail.
@@ -164,12 +171,12 @@
@CalledByNative
WebRtcAudioTrack(Context context, AudioManager audioManager) {
this(context, audioManager, null /* audioAttributes */, null /* errorCallback */,
- null /* stateCallback */);
+ null /* stateCallback */, false /* useLowLatency */);
}
WebRtcAudioTrack(Context context, AudioManager audioManager,
@Nullable AudioAttributes audioAttributes, @Nullable AudioTrackErrorCallback errorCallback,
- @Nullable AudioTrackStateCallback stateCallback) {
+ @Nullable AudioTrackStateCallback stateCallback, boolean useLowLatency) {
threadChecker.detachThread();
this.context = context;
this.audioManager = audioManager;
@@ -177,6 +184,7 @@
this.errorCallback = errorCallback;
this.stateCallback = stateCallback;
this.volumeLogger = new VolumeLogger(audioManager);
+ this.useLowLatency = useLowLatency;
Logging.d(TAG, "ctor" + WebRtcAudioUtils.getThreadInfo());
}
@@ -218,6 +226,13 @@
return -1;
}
+ // Don't use low-latency mode when a bufferSizeFactor > 1 is used. When bufferSizeFactor > 1
+ // we want to use a larger buffer to prevent underruns. However, low-latency mode would
+ // decrease the buffer size, which makes the bufferSizeFactor have no effect.
+ if (bufferSizeFactor > 1.0) {
+ useLowLatency = false;
+ }
+
// Ensure that prevision audio session was stopped correctly before trying
// to create a new AudioTrack.
if (audioTrack != null) {
@@ -228,7 +243,11 @@
// Create an AudioTrack object and initialize its associated audio buffer.
// The size of this buffer determines how long an AudioTrack can play
// before running out of data.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (useLowLatency && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // On API level 26 or higher, we can use a low latency mode.
+ audioTrack = createAudioTrackOnOreoOrHigher(
+ sampleRate, channelConfig, minBufferSizeInBytes, audioAttributes);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// If we are on API level 21 or higher, it is possible to use a special AudioTrack
// constructor that uses AudioAttributes and AudioFormat as input. It allows us to
// supersede the notion of stream types for defining the behavior of audio playback,
@@ -255,6 +274,11 @@
releaseAudioResources();
return -1;
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ initialBufferSizeInFrames = audioTrack.getBufferSizeInFrames();
+ } else {
+ initialBufferSizeInFrames = -1;
+ }
logMainParameters();
logMainParametersExtended();
return minBufferSizeInBytes;
@@ -382,22 +406,16 @@
+ "max gain: " + AudioTrack.getMaxVolume());
}
- // Creates and AudioTrack instance using AudioAttributes and AudioFormat as input.
- // It allows certain platforms or routing policies to use this information for more
- // refined volume or routing decisions.
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static AudioTrack createAudioTrackOnLollipopOrHigher(int sampleRateInHz,
- int channelConfig, int bufferSizeInBytes, @Nullable AudioAttributes overrideAttributes) {
- Logging.d(TAG, "createAudioTrackOnLollipopOrHigher");
- // TODO(henrika): use setPerformanceMode(int) with PERFORMANCE_MODE_LOW_LATENCY to control
- // performance when Android O is supported. Add some logging in the mean time.
+ private static void logNativeOutputSampleRate(int requestedSampleRateInHz) {
final int nativeOutputSampleRate =
AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_VOICE_CALL);
Logging.d(TAG, "nativeOutputSampleRate: " + nativeOutputSampleRate);
- if (sampleRateInHz != nativeOutputSampleRate) {
+ if (requestedSampleRateInHz != nativeOutputSampleRate) {
Logging.w(TAG, "Unable to use fast mode since requested sample rate is not native");
}
+ }
+ private static AudioAttributes getAudioAttributes(@Nullable AudioAttributes overrideAttributes) {
AudioAttributes.Builder attributesBuilder =
new AudioAttributes.Builder()
.setUsage(DEFAULT_USAGE)
@@ -417,9 +435,20 @@
attributesBuilder = applyAttributesOnQOrHigher(attributesBuilder, overrideAttributes);
}
}
+ return attributesBuilder.build();
+ }
+
+ // Creates and AudioTrack instance using AudioAttributes and AudioFormat as input.
+ // It allows certain platforms or routing policies to use this information for more
+ // refined volume or routing decisions.
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static AudioTrack createAudioTrackOnLollipopOrHigher(int sampleRateInHz,
+ int channelConfig, int bufferSizeInBytes, @Nullable AudioAttributes overrideAttributes) {
+ Logging.d(TAG, "createAudioTrackOnLollipopOrHigher");
+ logNativeOutputSampleRate(sampleRateInHz);
// Create an audio track where the audio usage is for VoIP and the content type is speech.
- return new AudioTrack(attributesBuilder.build(),
+ return new AudioTrack(getAudioAttributes(overrideAttributes),
new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRateInHz)
@@ -428,6 +457,32 @@
bufferSizeInBytes, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
}
+ // Creates and AudioTrack instance using AudioAttributes and AudioFormat as input.
+ // Use the low-latency mode to improve audio latency. Note that the low-latency mode may
+ // prevent effects (such as AEC) from working. Assuming AEC is working, the delay changes
+ // that happen in low-latency mode during the call will cause the AEC to perform worse.
+ // The behavior of the low-latency mode may be device dependent, use at your own risk.
+ @TargetApi(Build.VERSION_CODES.O)
+ private static AudioTrack createAudioTrackOnOreoOrHigher(int sampleRateInHz, int channelConfig,
+ int bufferSizeInBytes, @Nullable AudioAttributes overrideAttributes) {
+ Logging.d(TAG, "createAudioTrackOnOreoOrHigher");
+ logNativeOutputSampleRate(sampleRateInHz);
+
+ // Create an audio track where the audio usage is for VoIP and the content type is speech.
+ return new AudioTrack.Builder()
+ .setAudioAttributes(getAudioAttributes(overrideAttributes))
+ .setAudioFormat(new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setSampleRate(sampleRateInHz)
+ .setChannelMask(channelConfig)
+ .build())
+ .setBufferSizeInBytes(bufferSizeInBytes)
+ .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
+ .setTransferMode(AudioTrack.MODE_STREAM)
+ .setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE)
+ .build();
+ }
+
@TargetApi(Build.VERSION_CODES.Q)
private static AudioAttributes.Builder applyAttributesOnQOrHigher(
AudioAttributes.Builder builder, AudioAttributes overrideAttributes) {
@@ -458,6 +513,11 @@
return -1;
}
+ @CalledByNative
+ private int getInitialBufferSizeInFrames() {
+ return initialBufferSizeInFrames;
+ }
+
private void logBufferCapacityInFrames() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Logging.d(TAG,
diff --git a/sdk/android/src/jni/audio_device/audio_track_jni.cc b/sdk/android/src/jni/audio_device/audio_track_jni.cc
index d5b880b..85adee2 100644
--- a/sdk/android/src/jni/audio_device/audio_track_jni.cc
+++ b/sdk/android/src/jni/audio_device/audio_track_jni.cc
@@ -151,6 +151,18 @@
if (!initialized_ || !playing_) {
return 0;
}
+ // Log the difference in initial and current buffer level.
+ const int current_buffer_size_frames =
+ Java_WebRtcAudioTrack_getBufferSizeInFrames(env_, j_audio_track_);
+ const int initial_buffer_size_frames =
+ Java_WebRtcAudioTrack_getInitialBufferSizeInFrames(env_, j_audio_track_);
+ const int sample_rate_hz = audio_parameters_.sample_rate();
+ RTC_HISTOGRAM_COUNTS(
+ "WebRTC.Audio.AndroidNativeAudioBufferSizeDifferenceFromInitialMs",
+ (current_buffer_size_frames - initial_buffer_size_frames) * 1000 /
+ sample_rate_hz,
+ -500, 100, 100);
+
if (!Java_WebRtcAudioTrack_stopPlayout(env_, j_audio_track_)) {
RTC_LOG(LS_ERROR) << "StopPlayout failed";
return -1;
diff --git a/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java b/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java
new file mode 100644
index 0000000..c76ee8d
--- /dev/null
+++ b/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 The WebRTC Project Authors. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.audio;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalMatchers.gt;
+import static org.mockito.AdditionalMatchers.lt;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioTrack;
+import android.os.Build;
+import org.chromium.testing.local.LocalRobolectricTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+import org.webrtc.audio.LowLatencyAudioBufferManager;
+
+/**
+ * Tests for LowLatencyAudioBufferManager.
+ */
+@RunWith(LocalRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O)
+public class LowLatencyAudioBufferManagerTest {
+ @Mock private AudioTrack mockAudioTrack;
+ private LowLatencyAudioBufferManager bufferManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ bufferManager = new LowLatencyAudioBufferManager();
+ }
+
+ @Test
+ public void testBufferSizeDecrease() {
+ when(mockAudioTrack.getUnderrunCount()).thenReturn(0);
+ when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100);
+ when(mockAudioTrack.getPlaybackRate()).thenReturn(1000);
+ for (int i = 0; i < 9; i++) {
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ }
+ // Check that the buffer size was not changed yet.
+ verify(mockAudioTrack, times(0)).setBufferSizeInFrames(anyInt());
+ // After the 10th call without underruns, we expect the buffer size to decrease.
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ // The expected size is 10ms below the existing size, which works out to 100 - (1000 / 100)
+ // = 90.
+ verify(mockAudioTrack, times(1)).setBufferSizeInFrames(90);
+ }
+
+ @Test
+ public void testBufferSizeNeverBelow10ms() {
+ when(mockAudioTrack.getUnderrunCount()).thenReturn(0);
+ when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(11);
+ when(mockAudioTrack.getPlaybackRate()).thenReturn(1000);
+ for (int i = 0; i < 10; i++) {
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ }
+ // Check that the buffer size was not set to a value below 10 ms.
+ verify(mockAudioTrack, times(0)).setBufferSizeInFrames(lt(10));
+ }
+
+ @Test
+ public void testUnderrunBehavior() {
+ when(mockAudioTrack.getUnderrunCount()).thenReturn(1);
+ when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100);
+ when(mockAudioTrack.getPlaybackRate()).thenReturn(1000);
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ // Check that the buffer size was increased after the underrrun.
+ verify(mockAudioTrack, times(1)).setBufferSizeInFrames(gt(100));
+ when(mockAudioTrack.getUnderrunCount()).thenReturn(0);
+ for (int i = 0; i < 10; i++) {
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ }
+ // Check that the buffer size was not changed again, even though there were no underruns for
+ // 10 calls.
+ verify(mockAudioTrack, times(1)).setBufferSizeInFrames(anyInt());
+ }
+
+ @Test
+ public void testBufferIncrease() {
+ when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100);
+ when(mockAudioTrack.getPlaybackRate()).thenReturn(1000);
+ for (int i = 1; i < 30; i++) {
+ when(mockAudioTrack.getUnderrunCount()).thenReturn(i);
+ bufferManager.maybeAdjustBufferSize(mockAudioTrack);
+ }
+ // Check that the buffer size was not increased more than 5 times.
+ verify(mockAudioTrack, times(5)).setBufferSizeInFrames(gt(100));
+ }
+}