From 47f71cfe4d78d209cdbdffd8bacc1e90045f0f72 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 27 Jan 2022 15:06:50 +0000 Subject: [PATCH] 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 --- .../exoplayer/audio/DefaultAudioSink.java | 198 ++++++------- .../DefaultAudioTrackBufferSizeProvider.java | 264 +++++++++++++++++ ...faultAudioTrackBufferSizeProviderTest.java | 270 ++++++++++++++++++ 3 files changed, 614 insertions(+), 118 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java create mode 100644 libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderTest.java diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 60e8fb04f9..588c3f74b9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -16,6 +16,7 @@ package androidx.media3.exoplayer.audio; 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 com.google.common.base.MoreObjects.firstNonNull; 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. */ public static final class Builder { @@ -226,11 +260,13 @@ public final class DefaultAudioSink implements AudioSink { private boolean enableFloatOutput; private boolean enableAudioTrackPlaybackParams; private int offloadMode; + AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; /** Creates a new builder. */ public Builder() { audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; offloadMode = OFFLOAD_MODE_DISABLED; + audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT; } /** @@ -311,6 +347,18 @@ public final class DefaultAudioSink implements AudioSink { return this; } + /** + * Sets an {@link AudioTrackBufferSizeProvider} to compute the buffer size when {@link + * #configure} is called with {@code specifiedBufferSize == 0}. + * + *

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. */ public DefaultAudioSink build() { if (audioProcessorChain == null) { @@ -371,31 +419,18 @@ public final class DefaultAudioSink implements AudioSink { */ public static final int OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED = 3; + /** Output mode of the audio sink. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({OUTPUT_MODE_PCM, OUTPUT_MODE_OFFLOAD, OUTPUT_MODE_PASSTHROUGH}) - private @interface OutputMode {} + public @interface OutputMode {} - private static final int OUTPUT_MODE_PCM = 0; - private static final int OUTPUT_MODE_OFFLOAD = 1; - private static final int OUTPUT_MODE_PASSTHROUGH = 2; - - /** A minimum length for the {@link AudioTrack} buffer, in microseconds. */ - private static final long MIN_BUFFER_DURATION_US = 250_000; - /** 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; + /** The audio sink plays PCM audio. */ + public static final int OUTPUT_MODE_PCM = 0; + /** The audio sink plays encoded audio in offload. */ + public static final int OUTPUT_MODE_OFFLOAD = 1; + /** The audio sink plays encoded audio in passthrough. */ + public static final int OUTPUT_MODE_PASSTHROUGH = 2; /** * 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 initializationExceptionPendingExceptionHolder; private final PendingExceptionHolder writeExceptionPendingExceptionHolder; + private final AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; @Nullable private PlayerId playerId; @Nullable private Listener listener; @@ -562,6 +598,7 @@ public final class DefaultAudioSink implements AudioSink { enableFloatOutput = Util.SDK_INT >= 21 && builder.enableFloatOutput; enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams; offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED; + audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -724,6 +761,16 @@ public final class DefaultAudioSink implements AudioSink { 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) { throw new ConfigurationException( @@ -745,8 +792,7 @@ public final class DefaultAudioSink implements AudioSink { outputSampleRate, outputChannelConfig, outputEncoding, - specifiedBufferSize, - enableAudioTrackPlaybackParams, + bufferSize, availableAudioProcessors); if (isAudioTrackInitialized()) { this.pendingConfiguration = pendingConfiguration; @@ -1207,8 +1253,8 @@ public final class DefaultAudioSink implements AudioSink { public void setPlaybackParameters(PlaybackParameters playbackParameters) { playbackParameters = new PlaybackParameters( - Util.constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED), - Util.constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH)); + constrainValue(playbackParameters.speed, MIN_PLAYBACK_SPEED, MAX_PLAYBACK_SPEED), + constrainValue(playbackParameters.pitch, MIN_PITCH, MAX_PITCH)); if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { setAudioTrackPlaybackParametersV23(playbackParameters); } else { @@ -1789,47 +1835,6 @@ public final class DefaultAudioSink implements AudioSink { 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) { switch (encoding) { case C.ENCODING_MP3: @@ -2019,6 +2024,13 @@ public final class DefaultAudioSink implements AudioSink { .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 { @Override @@ -2113,8 +2125,7 @@ public final class DefaultAudioSink implements AudioSink { int outputSampleRate, int outputChannelConfig, int outputEncoding, - int specifiedBufferSize, - boolean enableAudioTrackPlaybackParams, + int bufferSize, AudioProcessor[] availableAudioProcessors) { this.inputFormat = inputFormat; this.inputPcmFrameSize = inputPcmFrameSize; @@ -2123,10 +2134,8 @@ public final class DefaultAudioSink implements AudioSink { this.outputSampleRate = outputSampleRate; this.outputChannelConfig = outputChannelConfig; this.outputEncoding = outputEncoding; + this.bufferSize = bufferSize; 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. */ @@ -2146,10 +2155,6 @@ public final class DefaultAudioSink implements AudioSink { return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; } - public long durationUsToFrames(long durationUs) { - return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND; - } - public AudioTrack buildAudioTrack( boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) 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) private static android.media.AudioAttributes getAudioTrackAttributesV21( AudioAttributes audioAttributes, boolean tunneling) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java new file mode 100644 index 0000000000..5083d92896 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java @@ -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(); + } + } +} diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderTest.java new file mode 100644 index 0000000000..8916391b8b --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderTest.java @@ -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 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 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); + } +}