diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6ee2c0ebeb..fbb025afab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,6 +53,9 @@ constructors. * Change `AudioCapabilities` APIs to require passing explicitly `AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`. + * Allow customization of the `AudioTrack` buffer size calculation by + injecting an `AudioTrackBufferSizeProvider` to `DefaultAudioSink`. + ([#8891](https://github.com/google/ExoPlayer/issues/8891)). * Extractors: * Fix inconsistency with spec in H.265 SPS nal units parsing ([#9719](https://github.com/google/ExoPlayer/issues/9719)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 0c9b259536..c799a5d341 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import static com.google.android.exoplayer2.audio.AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Util.constrainValue; import static com.google.common.base.MoreObjects.firstNonNull; import static java.lang.Math.max; import static java.lang.Math.min; @@ -209,6 +210,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 { @@ -217,11 +251,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; } /** @@ -302,6 +338,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) { @@ -362,31 +410,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 @@ -433,6 +468,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; @@ -553,6 +589,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(); @@ -715,6 +752,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( @@ -736,8 +783,7 @@ public final class DefaultAudioSink implements AudioSink { outputSampleRate, outputChannelConfig, outputEncoding, - specifiedBufferSize, - enableAudioTrackPlaybackParams, + bufferSize, availableAudioProcessors); if (isAudioTrackInitialized()) { this.pendingConfiguration = pendingConfiguration; @@ -1198,8 +1244,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 { @@ -1780,47 +1826,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: @@ -2010,6 +2015,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 @@ -2104,8 +2116,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; @@ -2114,10 +2125,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. */ @@ -2137,10 +2146,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 { @@ -2241,49 +2246,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/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioTrackBufferSizeProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioTrackBufferSizeProvider.java new file mode 100644 index 0000000000..1f4cab21af --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioTrackBufferSizeProvider.java @@ -0,0 +1,257 @@ +/* + * 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 com.google.android.exoplayer2.audio; + +import static com.google.android.exoplayer2.audio.DefaultAudioSink.OUTPUT_MODE_OFFLOAD; +import static com.google.android.exoplayer2.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; +import static com.google.android.exoplayer2.audio.DefaultAudioSink.OUTPUT_MODE_PCM; +import static com.google.android.exoplayer2.util.Util.constrainValue; +import static com.google.common.primitives.Ints.checkedCast; +import static java.lang.Math.max; + +import android.media.AudioTrack; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.audio.DefaultAudioSink.OutputMode; + +/** Provide the buffer size to use when creating an {@link AudioTrack}. */ +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/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioTrackBufferSizeProviderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioTrackBufferSizeProviderTest.java new file mode 100644 index 0000000000..eea49bf5b8 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/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 com.google.android.exoplayer2.audio; + +import static com.google.android.exoplayer2.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; +import static com.google.android.exoplayer2.audio.DefaultAudioSink.OUTPUT_MODE_PCM; +import static com.google.android.exoplayer2.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond; +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.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); + } +}