Make audio track min buffer size configurable.

Move the code in its own class as DefaultAudioTrack
is getting very big. It also help for testability.

The new class is easily configurable and highly tested.
Manual test was used to catch any regression.

https://github.com/google/ExoPlayer/issues/8891

PiperOrigin-RevId: 424602011
This commit is contained in:
krocard 2022-01-27 15:06:50 +00:00 committed by Andrew Lewis
parent fb8e99c778
commit 47f71cfe4d
3 changed files with 614 additions and 118 deletions

View File

@ -16,6 +16,7 @@
package androidx.media3.exoplayer.audio; package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.exoplayer.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES; import static androidx.media3.exoplayer.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES;
import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.Math.max; import static java.lang.Math.max;
@ -218,6 +219,39 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
/** Provides the buffer size to use when creating an {@link AudioTrack}. */
interface AudioTrackBufferSizeProvider {
/** Default instance. */
AudioTrackBufferSizeProvider DEFAULT =
new DefaultAudioTrackBufferSizeProvider.Builder().build();
/**
* Returns the buffer size to use when creating an {@link AudioTrack} for a specific format and
* output mode.
*
* @param minBufferSizeInBytes The minimum buffer size in bytes required to play this format.
* See {@link AudioTrack#getMinBufferSize}.
* @param encoding The {@link C.Encoding} of the format.
* @param outputMode How the audio will be played. One of the {@link OutputMode output modes}.
* @param pcmFrameSize The size of the PCM frames if the {@code encoding} is PCM, 1 otherwise,
* in bytes.
* @param sampleRate The sample rate of the format, in Hz.
* @param maxAudioTrackPlaybackSpeed The maximum speed the content will be played using {@link
* AudioTrack#setPlaybackParams}. 0.5 is 2x slow motion, 1 is real time, 2 is 2x fast
* forward, etc. This will be {@code 1} unless {@link
* Builder#setEnableAudioTrackPlaybackParams} is enabled.
* @return The computed buffer size in bytes. It should always be {@code >=
* minBufferSizeInBytes}. The computed buffer size must contain an integer number of frames:
* {@code bufferSizeInBytes % pcmFrameSize == 0}.
*/
int getBufferSizeInBytes(
int minBufferSizeInBytes,
@C.Encoding int encoding,
@OutputMode int outputMode,
int pcmFrameSize,
int sampleRate,
double maxAudioTrackPlaybackSpeed);
}
/** A builder to create {@link DefaultAudioSink} instances. */ /** A builder to create {@link DefaultAudioSink} instances. */
public static final class Builder { public static final class Builder {
@ -226,11 +260,13 @@ public final class DefaultAudioSink implements AudioSink {
private boolean enableFloatOutput; private boolean enableFloatOutput;
private boolean enableAudioTrackPlaybackParams; private boolean enableAudioTrackPlaybackParams;
private int offloadMode; private int offloadMode;
AudioTrackBufferSizeProvider audioTrackBufferSizeProvider;
/** Creates a new builder. */ /** Creates a new builder. */
public Builder() { public Builder() {
audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; audioCapabilities = DEFAULT_AUDIO_CAPABILITIES;
offloadMode = OFFLOAD_MODE_DISABLED; offloadMode = OFFLOAD_MODE_DISABLED;
audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT;
} }
/** /**
@ -311,6 +347,18 @@ public final class DefaultAudioSink implements AudioSink {
return this; return this;
} }
/**
* Sets an {@link AudioTrackBufferSizeProvider} to compute the buffer size when {@link
* #configure} is called with {@code specifiedBufferSize == 0}.
*
* <p>The default value is {@link AudioTrackBufferSizeProvider#DEFAULT}.
*/
public Builder setAudioTrackBufferSizeProvider(
AudioTrackBufferSizeProvider audioTrackBufferSizeProvider) {
this.audioTrackBufferSizeProvider = audioTrackBufferSizeProvider;
return this;
}
/** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */ /** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */
public DefaultAudioSink build() { public DefaultAudioSink build() {
if (audioProcessorChain == null) { if (audioProcessorChain == null) {
@ -371,31 +419,18 @@ public final class DefaultAudioSink implements AudioSink {
*/ */
public static final int OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED = 3; public static final int OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED = 3;
/** Output mode of the audio sink. */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH}) @IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH})
private @interface OutputMode {} public @interface OutputMode {}
private static final int OUTPUT_MODE_PCM = 0; /** The audio sink plays PCM audio. */
private static final int OUTPUT_MODE_OFFLOAD = 1; public static final int OUTPUT_MODE_PCM = 0;
private static final int OUTPUT_MODE_PASSTHROUGH = 2; /** The audio sink plays encoded audio in offload. */
public static final int OUTPUT_MODE_OFFLOAD = 1;
/** A minimum length for the {@link AudioTrack} buffer, in microseconds. */ /** The audio sink plays encoded audio in passthrough. */
private static final long MIN_BUFFER_DURATION_US = 250_000; public static final int OUTPUT_MODE_PASSTHROUGH = 2;
/** A maximum length for the {@link AudioTrack} buffer, in microseconds. */
private static final long MAX_BUFFER_DURATION_US = 750_000;
/** The length for passthrough {@link AudioTrack} buffers, in microseconds. */
private static final long PASSTHROUGH_BUFFER_DURATION_US = 250_000;
/** The length for offload {@link AudioTrack} buffers, in microseconds. */
private static final long OFFLOAD_BUFFER_DURATION_US = 50_000_000;
/**
* A multiplication factor to apply to the minimum buffer size requested by the underlying {@link
* AudioTrack}.
*/
private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
/** To avoid underruns on some devices (e.g., Broadcom 7271), scale up the AC3 buffer duration. */
private static final int AC3_BUFFER_MULTIPLICATION_FACTOR = 2;
/** /**
* Native error code equivalent of {@link AudioTrack#ERROR_DEAD_OBJECT} to workaround missing * Native error code equivalent of {@link AudioTrack#ERROR_DEAD_OBJECT} to workaround missing
@ -442,6 +477,7 @@ public final class DefaultAudioSink implements AudioSink {
private final PendingExceptionHolder<InitializationException> private final PendingExceptionHolder<InitializationException>
initializationExceptionPendingExceptionHolder; initializationExceptionPendingExceptionHolder;
private final PendingExceptionHolder<WriteException> writeExceptionPendingExceptionHolder; private final PendingExceptionHolder<WriteException> writeExceptionPendingExceptionHolder;
private final AudioTrackBufferSizeProvider audioTrackBufferSizeProvider;
@Nullable private PlayerId playerId; @Nullable private PlayerId playerId;
@Nullable private Listener listener; @Nullable private Listener listener;
@ -562,6 +598,7 @@ public final class DefaultAudioSink implements AudioSink {
enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput; enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput;
enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams; enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams;
offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED; offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED;
audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider;
releasingConditionVariable = new ConditionVariable(true); releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
@ -724,6 +761,16 @@ public final class DefaultAudioSink implements AudioSink {
outputChannelConfig = encodingAndChannelConfig.second; outputChannelConfig = encodingAndChannelConfig.second;
} }
} }
int bufferSize =
specifiedBufferSize != 0
? specifiedBufferSize
: audioTrackBufferSizeProvider.getBufferSizeInBytes(
getAudioTrackMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding),
outputEncoding,
outputMode,
outputPcmFrameSize,
outputSampleRate,
enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED);
if (outputEncoding == C.ENCODING_INVALID) { if (outputEncoding == C.ENCODING_INVALID) {
throw new ConfigurationException( throw new ConfigurationException(
@ -745,8 +792,7 @@ public final class DefaultAudioSink implements AudioSink {
outputSampleRate, outputSampleRate,
outputChannelConfig, outputChannelConfig,
outputEncoding, outputEncoding,
specifiedBufferSize, bufferSize,
enableAudioTrackPlaybackParams,
availableAudioProcessors); availableAudioProcessors);
if (isAudioTrackInitialized()) { if (isAudioTrackInitialized()) {
this.pendingConfiguration = pendingConfiguration; this.pendingConfiguration = pendingConfiguration;
@ -1207,8 +1253,8 @@ public final class DefaultAudioSink implements AudioSink {
public void setPlaybackParameters(PlaybackParameters playbackParameters) { public void setPlaybackParameters(PlaybackParameters playbackParameters) {
playbackParameters = playbackParameters =
new PlaybackParameters( new PlaybackParameters(
Util.constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED), constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED),
Util.constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH)); constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH));
if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
setAudioTrackPlaybackParametersV23(playbackParameters); setAudioTrackPlaybackParametersV23(playbackParameters);
} else { } else {
@ -1789,47 +1835,6 @@ public final class DefaultAudioSink implements AudioSink {
return Util.SDK_INT >= 29 && audioTrack.isOffloadedPlayback(); return Util.SDK_INT >= 29 && audioTrack.isOffloadedPlayback();
} }
private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
switch (encoding) {
case C.ENCODING_MP3:
return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_LC:
return AacUtil.AAC_LC_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V1:
return AacUtil.AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V2:
return AacUtil.AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_XHE:
return AacUtil.AAC_XHE_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_ELD:
return AacUtil.AAC_ELD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC3:
return Ac3Util.AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return Ac3Util.E_AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC4:
return Ac4Util.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS:
return DtsUtil.DTS_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS_HD:
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_AAC_ER_BSAC:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
}
}
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
switch (encoding) { switch (encoding) {
case C.ENCODING_MP3: case C.ENCODING_MP3:
@ -2019,6 +2024,13 @@ public final class DefaultAudioSink implements AudioSink {
.build(); .build();
} }
private static int getAudioTrackMinBufferSize(
int sampleRateInHz, int channelConfig, int encoding) {
int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, encoding);
Assertions.checkState(minBufferSize != AudioTrack.ERROR_BAD_VALUE);
return minBufferSize;
}
private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener {
@Override @Override
@ -2113,8 +2125,7 @@ public final class DefaultAudioSink implements AudioSink {
int outputSampleRate, int outputSampleRate,
int outputChannelConfig, int outputChannelConfig,
int outputEncoding, int outputEncoding,
int specifiedBufferSize, int bufferSize,
boolean enableAudioTrackPlaybackParams,
AudioProcessor[] availableAudioProcessors) { AudioProcessor[] availableAudioProcessors) {
this.inputFormat = inputFormat; this.inputFormat = inputFormat;
this.inputPcmFrameSize = inputPcmFrameSize; this.inputPcmFrameSize = inputPcmFrameSize;
@ -2123,10 +2134,8 @@ public final class DefaultAudioSink implements AudioSink {
this.outputSampleRate = outputSampleRate; this.outputSampleRate = outputSampleRate;
this.outputChannelConfig = outputChannelConfig; this.outputChannelConfig = outputChannelConfig;
this.outputEncoding = outputEncoding; this.outputEncoding = outputEncoding;
this.bufferSize = bufferSize;
this.availableAudioProcessors = availableAudioProcessors; this.availableAudioProcessors = availableAudioProcessors;
// Call computeBufferSize() last as it depends on the other configuration values.
this.bufferSize = computeBufferSize(specifiedBufferSize, enableAudioTrackPlaybackParams);
} }
/** Returns if the configurations are sufficiently compatible to reuse the audio track. */ /** Returns if the configurations are sufficiently compatible to reuse the audio track. */
@ -2146,10 +2155,6 @@ public final class DefaultAudioSink implements AudioSink {
return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate;
} }
public long durationUsToFrames(long durationUs) {
return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND;
}
public AudioTrack buildAudioTrack( public AudioTrack buildAudioTrack(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) boolean tunneling, AudioAttributes audioAttributes, int audioSessionId)
throws InitializationException { throws InitializationException {
@ -2250,49 +2255,6 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
private int computeBufferSize(
int specifiedBufferSize, boolean enableAudioTrackPlaybackParameters) {
if (specifiedBufferSize != 0) {
return specifiedBufferSize;
}
switch (outputMode) {
case OUTPUT_MODE_PCM:
return getPcmDefaultBufferSize(
enableAudioTrackPlaybackParameters ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED);
case OUTPUT_MODE_OFFLOAD:
return getEncodedDefaultBufferSize(OFFLOAD_BUFFER_DURATION_US);
case OUTPUT_MODE_PASSTHROUGH:
return getEncodedDefaultBufferSize(PASSTHROUGH_BUFFER_DURATION_US);
default:
throw new IllegalStateException();
}
}
private int getEncodedDefaultBufferSize(long bufferDurationUs) {
int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding);
if (outputEncoding == C.ENCODING_AC3) {
rate *= AC3_BUFFER_MULTIPLICATION_FACTOR;
}
return (int) (bufferDurationUs * rate / C.MICROS_PER_SECOND);
}
private int getPcmDefaultBufferSize(float maxAudioTrackPlaybackSpeed) {
int minBufferSize =
AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding);
Assertions.checkState(minBufferSize != AudioTrack.ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize =
max(minBufferSize, (int) durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
int bufferSize =
Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
if (maxAudioTrackPlaybackSpeed != 1f) {
// Maintain the buffer duration by scaling the size accordingly.
bufferSize = Math.round(bufferSize * maxAudioTrackPlaybackSpeed);
}
return bufferSize;
}
@RequiresApi(21) @RequiresApi(21)
private static android.media.AudioAttributes getAudioTrackAttributesV21( private static android.media.AudioAttributes getAudioTrackAttributesV21(
AudioAttributes audioAttributes, boolean tunneling) { AudioAttributes audioAttributes, boolean tunneling) {

View File

@ -0,0 +1,264 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_OFFLOAD;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PCM;
import static com.google.common.primitives.Ints.checkedCast;
import static java.lang.Math.max;
import android.media.AudioTrack;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.audio.DefaultAudioSink.OutputMode;
import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil;
/** Provide the buffer size to use when creating an {@link AudioTrack}. */
@UnstableApi
public class DefaultAudioTrackBufferSizeProvider
implements DefaultAudioSink.AudioTrackBufferSizeProvider {
/** Default minimum length for the {@link AudioTrack} buffer, in microseconds. */
private static final int MIN_PCM_BUFFER_DURATION_US = 250_000;
/** Default maximum length for the {@link AudioTrack} buffer, in microseconds. */
private static final int MAX_PCM_BUFFER_DURATION_US = 750_000;
/** Default multiplication factor to apply to the minimum buffer size requested. */
private static final int PCM_BUFFER_MULTIPLICATION_FACTOR = 4;
/** Default length for passthrough {@link AudioTrack} buffers, in microseconds. */
private static final int PASSTHROUGH_BUFFER_DURATION_US = 250_000;
/** Default length for offload {@link AudioTrack} buffers, in microseconds. */
private static final int OFFLOAD_BUFFER_DURATION_US = 50_000_000;
/**
* Default multiplication factor to apply to AC3 passthrough buffer to avoid underruns on some
* devices (e.g., Broadcom 7271).
*/
private static final int AC3_BUFFER_MULTIPLICATION_FACTOR = 2;
/** A builder to create {@link DefaultAudioTrackBufferSizeProvider} instances. */
public static class Builder {
private int minPcmBufferDurationUs;
private int maxPcmBufferDurationUs;
private int pcmBufferMultiplicationFactor;
private int passthroughBufferDurationUs;
private int offloadBufferDurationUs;
private int ac3BufferMultiplicationFactor;
/** Creates a new builder. */
public Builder() {
minPcmBufferDurationUs = MIN_PCM_BUFFER_DURATION_US;
maxPcmBufferDurationUs = MAX_PCM_BUFFER_DURATION_US;
pcmBufferMultiplicationFactor = PCM_BUFFER_MULTIPLICATION_FACTOR;
passthroughBufferDurationUs = PASSTHROUGH_BUFFER_DURATION_US;
offloadBufferDurationUs = OFFLOAD_BUFFER_DURATION_US;
ac3BufferMultiplicationFactor = AC3_BUFFER_MULTIPLICATION_FACTOR;
}
/**
* Sets the minimum length for PCM {@link AudioTrack} buffers, in microseconds. Default is
* {@value #MIN_PCM_BUFFER_DURATION_US}.
*/
public Builder setMinPcmBufferDurationUs(int minPcmBufferDurationUs) {
this.minPcmBufferDurationUs = minPcmBufferDurationUs;
return this;
}
/**
* Sets the maximum length for PCM {@link AudioTrack} buffers, in microseconds. Default is
* {@value #MAX_PCM_BUFFER_DURATION_US}.
*/
public Builder setMaxPcmBufferDurationUs(int maxPcmBufferDurationUs) {
this.maxPcmBufferDurationUs = maxPcmBufferDurationUs;
return this;
}
/**
* Sets the multiplication factor to apply to the minimum buffer size requested. Default is
* {@value #PCM_BUFFER_MULTIPLICATION_FACTOR}.
*/
public Builder setPcmBufferMultiplicationFactor(int pcmBufferMultiplicationFactor) {
this.pcmBufferMultiplicationFactor = pcmBufferMultiplicationFactor;
return this;
}
/**
* Sets the length for passthrough {@link AudioTrack} buffers, in microseconds. Default is
* {@value #PASSTHROUGH_BUFFER_DURATION_US}.
*/
public Builder setPassthroughBufferDurationUs(int passthroughBufferDurationUs) {
this.passthroughBufferDurationUs = passthroughBufferDurationUs;
return this;
}
/**
* The length for offload {@link AudioTrack} buffers, in microseconds. Default is {@value
* #OFFLOAD_BUFFER_DURATION_US}.
*/
public Builder setOffloadBufferDurationUs(int offloadBufferDurationUs) {
this.offloadBufferDurationUs = offloadBufferDurationUs;
return this;
}
/**
* Sets the multiplication factor to apply to the passthrough buffer for AC3 to avoid underruns
* on some devices (e.g., Broadcom 7271). Default is {@value #AC3_BUFFER_MULTIPLICATION_FACTOR}.
*/
public Builder setAc3BufferMultiplicationFactor(int ac3BufferMultiplicationFactor) {
this.ac3BufferMultiplicationFactor = ac3BufferMultiplicationFactor;
return this;
}
/** Build the {@link DefaultAudioTrackBufferSizeProvider}. */
public DefaultAudioTrackBufferSizeProvider build() {
return new DefaultAudioTrackBufferSizeProvider(this);
}
}
/** The minimum length for PCM {@link AudioTrack} buffers, in microseconds. */
protected final int minPcmBufferDurationUs;
/** The maximum length for PCM {@link AudioTrack} buffers, in microseconds. */
protected final int maxPcmBufferDurationUs;
/** The multiplication factor to apply to the minimum buffer size requested. */
protected final int pcmBufferMultiplicationFactor;
/** The length for passthrough {@link AudioTrack} buffers, in microseconds. */
protected final int passthroughBufferDurationUs;
/** The length for offload {@link AudioTrack} buffers, in microseconds. */
protected final int offloadBufferDurationUs;
/**
* The multiplication factor to apply to AC3 passthrough buffer to avoid underruns on some devices
* (e.g., Broadcom 7271).
*/
public final int ac3BufferMultiplicationFactor;
protected DefaultAudioTrackBufferSizeProvider(Builder builder) {
minPcmBufferDurationUs = builder.minPcmBufferDurationUs;
maxPcmBufferDurationUs = builder.maxPcmBufferDurationUs;
pcmBufferMultiplicationFactor = builder.pcmBufferMultiplicationFactor;
passthroughBufferDurationUs = builder.passthroughBufferDurationUs;
offloadBufferDurationUs = builder.offloadBufferDurationUs;
ac3BufferMultiplicationFactor = builder.ac3BufferMultiplicationFactor;
}
@Override
public int getBufferSizeInBytes(
int minBufferSizeInBytes,
@C.Encoding int encoding,
@OutputMode int outputMode,
int pcmFrameSize,
int sampleRate,
double maxAudioTrackPlaybackSpeed) {
int bufferSize =
get1xBufferSizeInBytes(
minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate);
// Maintain the buffer duration by scaling the size accordingly.
bufferSize = (int) (bufferSize * maxAudioTrackPlaybackSpeed);
// Buffer size must not be lower than the AudioTrack min buffer size for this format.
bufferSize = max(minBufferSizeInBytes, bufferSize);
// Increase if needed to make sure the buffers contains an integer number of frames.
return (bufferSize + pcmFrameSize - 1) / pcmFrameSize * pcmFrameSize;
}
/** Returns the buffer size for playback at 1x speed. */
protected int get1xBufferSizeInBytes(
int minBufferSizeInBytes, int encoding, int outputMode, int pcmFrameSize, int sampleRate) {
switch (outputMode) {
case OUTPUT_MODE_PCM:
return getPcmBufferSizeInBytes(minBufferSizeInBytes, sampleRate, pcmFrameSize);
case OUTPUT_MODE_PASSTHROUGH:
return getPassthroughBufferSizeInBytes(encoding);
case OUTPUT_MODE_OFFLOAD:
return getOffloadBufferSizeInBytes(encoding);
default:
throw new IllegalArgumentException();
}
}
/** Returns the buffer size for PCM playback. */
protected int getPcmBufferSizeInBytes(int minBufferSizeInBytes, int samplingRate, int frameSize) {
int targetBufferSize = minBufferSizeInBytes * pcmBufferMultiplicationFactor;
int minAppBufferSize = durationUsToBytes(minPcmBufferDurationUs, samplingRate, frameSize);
int maxAppBufferSize = durationUsToBytes(maxPcmBufferDurationUs, samplingRate, frameSize);
return constrainValue(targetBufferSize, minAppBufferSize, maxAppBufferSize);
}
/** Returns the buffer size for passthrough playback. */
protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding) {
int bufferSizeUs = passthroughBufferDurationUs;
if (encoding == C.ENCODING_AC3) {
bufferSizeUs *= ac3BufferMultiplicationFactor;
}
int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) bufferSizeUs * maxByteRate / C.MICROS_PER_SECOND);
}
/** Returns the buffer size for offload playback. */
protected int getOffloadBufferSizeInBytes(@C.Encoding int encoding) {
int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) offloadBufferDurationUs * maxByteRate / C.MICROS_PER_SECOND);
}
protected static int durationUsToBytes(int durationUs, int samplingRate, int frameSize) {
return checkedCast((long) durationUs * samplingRate * frameSize / C.MICROS_PER_SECOND);
}
protected static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) {
switch (encoding) {
case C.ENCODING_MP3:
return MpegAudioUtil.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_LC:
return AacUtil.AAC_LC_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V1:
return AacUtil.AAC_HE_V1_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_HE_V2:
return AacUtil.AAC_HE_V2_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_XHE:
return AacUtil.AAC_XHE_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AAC_ELD:
return AacUtil.AAC_ELD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC3:
return Ac3Util.AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return Ac3Util.E_AC3_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_AC4:
return Ac4Util.MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS:
return DtsUtil.DTS_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DTS_HD:
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_AAC_ER_BSAC:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
}
}
}

View File

@ -0,0 +1,270 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PCM;
import static androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.junit.runners.Parameterized;
/** Tests for {@link DefaultAudioTrackBufferSizeProvider}. */
@RunWith(JUnit4.class)
public class DefaultAudioTrackBufferSizeProviderTest {
private static final DefaultAudioTrackBufferSizeProvider DEFAULT =
new DefaultAudioTrackBufferSizeProvider.Builder().build();
/** Tests for {@link DefaultAudioTrackBufferSizeProvider} for PCM audio. */
@RunWith(Parameterized.class)
public static class PcmTest {
@Parameterized.Parameter(0)
@C.PcmEncoding
public int encoding;
@Parameterized.Parameter(1)
public int channelCount;
@Parameterized.Parameter(2)
public int sampleRate;
@Parameterized.Parameters(name = "{index}: encoding={0}, channelCount={1}, sampleRate={2}")
public static List<Integer[]> data() {
return Sets.cartesianProduct(
ImmutableList.of(
/* encoding */ ImmutableSet.of(
C.ENCODING_PCM_8BIT,
C.ENCODING_PCM_16BIT,
C.ENCODING_PCM_16BIT_BIG_ENDIAN,
C.ENCODING_PCM_24BIT,
C.ENCODING_PCM_32BIT,
C.ENCODING_PCM_FLOAT),
/* channelCount */ ImmutableSet.of(1, 2, 3, 4, 6, 8),
/* sampleRate*/ ImmutableSet.of(
8000, 11025, 16000, 22050, 44100, 48000, 88200, 96000)))
.stream()
.map(s -> s.toArray(new Integer[0]))
.collect(Collectors.toList());
}
private int getPcmFrameSize() {
return Util.getPcmFrameSize(encoding, channelCount);
}
private int durationUsToBytes(int durationUs) {
return (int) (((long) durationUs * getPcmFrameSize() * sampleRate) / C.MICROS_PER_SECOND);
}
@Test
public void getBufferSizeInBytes_veryBigMinBufferSize_isMinBufferSize() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 123456789,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize).isEqualTo(123456789);
}
@Test
public void getBufferSizeInBytes_noMinBufferSize_isMinBufferDuration() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 0,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize).isEqualTo(durationUsToBytes(DEFAULT.minPcmBufferDurationUs));
assertThat(bufferSize % getPcmFrameSize()).isEqualTo(0);
}
@Test
public void getBufferSizeInBytes_tooSmallMinBufferSize_isMinBufferDuration() {
int minBufferSizeInBytes =
durationUsToBytes(DEFAULT.minPcmBufferDurationUs / DEFAULT.pcmBufferMultiplicationFactor)
- 1;
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ minBufferSizeInBytes,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize).isEqualTo(durationUsToBytes(DEFAULT.minPcmBufferDurationUs));
}
@Test
public void getBufferSizeInBytes_lowMinBufferSize_multipliesAudioTrackMinBuffer() {
int minBufferSizeInBytes =
durationUsToBytes(DEFAULT.minPcmBufferDurationUs / DEFAULT.pcmBufferMultiplicationFactor)
+ 1;
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ minBufferSizeInBytes,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize)
.isEqualTo(minBufferSizeInBytes * DEFAULT.pcmBufferMultiplicationFactor);
}
@Test
public void getBufferSizeInBytes_highMinBufferSize_multipliesAudioTrackMinBuffer() {
int minBufferSizeInBytes =
durationUsToBytes(DEFAULT.maxPcmBufferDurationUs / DEFAULT.pcmBufferMultiplicationFactor)
- 1;
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ minBufferSizeInBytes,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize)
.isEqualTo(minBufferSizeInBytes * DEFAULT.pcmBufferMultiplicationFactor);
}
@Test
public void getBufferSizeInBytes_tooHighMinBufferSize_isMaxBufferDuration() {
int minBufferSizeInBytes =
durationUsToBytes(DEFAULT.maxPcmBufferDurationUs / DEFAULT.pcmBufferMultiplicationFactor)
+ 1;
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ minBufferSizeInBytes,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize).isEqualTo(durationUsToBytes(DEFAULT.maxPcmBufferDurationUs));
assertThat(bufferSize % getPcmFrameSize()).isEqualTo(0);
}
@Test
public void getBufferSizeInBytes_lowPlaybackSpeed_isScaledByPlaybackSpeed() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 0,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 1 / 5F);
assertThat(bufferSize).isEqualTo(durationUsToBytes(DEFAULT.minPcmBufferDurationUs / 5));
}
@Test
public void getBufferSizeInBytes_highPlaybackSpeed_isScaledByPlaybackSpeed() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 0,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PCM,
/* pcmFrameSize= */ getPcmFrameSize(),
/* sampleRate= */ sampleRate,
/* maxAudioTrackPlaybackSpeed= */ 8F);
assertThat(bufferSize).isEqualTo(durationUsToBytes(DEFAULT.minPcmBufferDurationUs * 8));
}
}
/**
* Tests for {@link DefaultAudioTrackBufferSizeProvider} for encoded audio except {@link
* C#ENCODING_AC3}.
*/
@RunWith(Parameterized.class)
public static class EncodedTest {
@Parameterized.Parameter(0)
@C.Encoding
public int encoding;
@Parameterized.Parameters(name = "{index}: encoding={0}")
public static ImmutableList<Integer> data() {
return ImmutableList.of(
C.ENCODING_MP3,
C.ENCODING_AAC_LC,
C.ENCODING_AAC_HE_V1,
C.ENCODING_AC4,
C.ENCODING_DTS,
C.ENCODING_DOLBY_TRUEHD);
}
@Test
public void getBufferSizeInBytes_veryBigMinBufferSize_isMinBufferSize() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 123456789,
/* encoding= */ encoding,
/* outputMode= */ OUTPUT_MODE_PASSTHROUGH,
/* pcmFrameSize= */ 1,
/* sampleRate= */ 0,
/* maxAudioTrackPlaybackSpeed= */ 0);
assertThat(bufferSize).isEqualTo(123456789);
}
}
@Test
public void
getBufferSizeInBytes_passthroughAC3_isPassthroughBufferSizeTimesMultiplicationFactor() {
int bufferSize =
DEFAULT.getBufferSizeInBytes(
/* minBufferSizeInBytes= */ 0,
/* encoding= */ C.ENCODING_AC3,
/* outputMode= */ OUTPUT_MODE_PASSTHROUGH,
/* pcmFrameSize= */ 1,
/* sampleRate= */ 0,
/* maxAudioTrackPlaybackSpeed= */ 1);
assertThat(bufferSize)
.isEqualTo(
durationUsToAc3MaxBytes(DEFAULT.passthroughBufferDurationUs)
* DEFAULT.ac3BufferMultiplicationFactor);
}
private static int durationUsToAc3MaxBytes(long durationUs) {
return (int)
(durationUs * getMaximumEncodedRateBytesPerSecond(C.ENCODING_AC3) / C.MICROS_PER_SECOND);
}
}