Implement offload AudioTrack in DefaultAudioSink.

#exo-offload

PiperOrigin-RevId: 314288300
This commit is contained in:
krocard 2020-06-02 09:43:12 +01:00 committed by Oliver Woodman
parent 75e54a452a
commit a5067e6314
2 changed files with 168 additions and 81 deletions

View File

@ -35,15 +35,16 @@ import java.nio.ByteOrder;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
* position smoothing, non-blocking writes and reconfiguration. * position smoothing, non-blocking writes and reconfiguration.
* <p> *
* If tunneling mode is enabled, care must be taken that audio processors do not output buffers with * <p>If tunneling mode is enabled, care must be taken that audio processors do not output buffers
* a different duration than their input, and buffer processors must produce output corresponding to * with a different duration than their input, and buffer processors must produce output
* their last input immediately after that input is queued. This means that, for example, speed * corresponding to their last input immediately after that input is queued. This means that, for
* adjustment is not possible while using tunneling. * example, speed adjustment is not possible while using tunneling.
*/ */
public final class DefaultAudioSink implements AudioSink { public final class DefaultAudioSink implements AudioSink {
@ -204,6 +205,9 @@ public final class DefaultAudioSink implements AudioSink {
private static final long MAX_BUFFER_DURATION_US = 750_000; private static final long MAX_BUFFER_DURATION_US = 750_000;
/** The length for passthrough {@link AudioTrack} buffers, in microseconds. */ /** The length for passthrough {@link AudioTrack} buffers, in microseconds. */
private static final long PASSTHROUGH_BUFFER_DURATION_US = 250_000; 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 * A multiplication factor to apply to the minimum buffer size requested by the underlying
* {@link AudioTrack}. * {@link AudioTrack}.
@ -269,14 +273,15 @@ public final class DefaultAudioSink implements AudioSink {
private final ConditionVariable releasingConditionVariable; private final ConditionVariable releasingConditionVariable;
private final AudioTrackPositionTracker audioTrackPositionTracker; private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints; private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
private final boolean enableOffload;
@Nullable private Listener listener; @Nullable private Listener listener;
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */
@Nullable private AudioTrack keepSessionIdAudioTrack; @Nullable private AudioTrack keepSessionIdAudioTrack;
@Nullable private Configuration pendingConfiguration; @Nullable private Configuration pendingConfiguration;
private Configuration configuration; @MonotonicNonNull private Configuration configuration;
private AudioTrack audioTrack; @Nullable private AudioTrack audioTrack;
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
@Nullable private MediaPositionParameters afterDrainParameters; @Nullable private MediaPositionParameters afterDrainParameters;
@ -340,7 +345,11 @@ public final class DefaultAudioSink implements AudioSink {
@Nullable AudioCapabilities audioCapabilities, @Nullable AudioCapabilities audioCapabilities,
AudioProcessor[] audioProcessors, AudioProcessor[] audioProcessors,
boolean enableFloatOutput) { boolean enableFloatOutput) {
this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); this(
audioCapabilities,
new DefaultAudioProcessorChain(audioProcessors),
enableFloatOutput,
/* enableOffload= */ false);
} }
/** /**
@ -355,14 +364,19 @@ public final class DefaultAudioSink implements AudioSink {
* output will be used if the input is 32-bit float, and also if the input is high resolution * output will be used if the input is 32-bit float, and also if the input is high resolution
* (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not
* be available when float output is in use. * be available when float output is in use.
* @param enableOffload Whether audio offloading is enabled. If an audio format can be both played
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
* supported starting with API 29 ({@link android.os.Build.VERSION_CODES#Q}).
*/ */
public DefaultAudioSink( public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities, @Nullable AudioCapabilities audioCapabilities,
AudioProcessorChain audioProcessorChain, AudioProcessorChain audioProcessorChain,
boolean enableFloatOutput) { boolean enableFloatOutput,
boolean enableOffload) {
this.audioCapabilities = audioCapabilities; this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableFloatOutput = enableFloatOutput; this.enableFloatOutput = enableFloatOutput;
this.enableOffload = Util.SDK_INT >= 29 && enableOffload;
releasingConditionVariable = new ConditionVariable(true); releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
@ -410,9 +424,12 @@ public final class DefaultAudioSink implements AudioSink {
// sink to 16-bit PCM. We assume that the audio framework will downsample any number of // sink to 16-bit PCM. We assume that the audio framework will downsample any number of
// channels to the output device's required number of channels. // channels to the output device's required number of channels.
return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21;
} else {
return isPassthroughPlaybackSupported(encoding, channelCount);
} }
if (enableOffload
&& isOffloadedPlaybackSupported(channelCount, sampleRateHz, encoding, audioAttributes)) {
return true;
}
return isPassthroughPlaybackSupported(encoding, channelCount);
} }
@Override @Override
@ -485,6 +502,11 @@ public final class DefaultAudioSink implements AudioSink {
int outputPcmFrameSize = int outputPcmFrameSize =
isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET;
boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput;
boolean useOffload =
enableOffload
&& !isInputPcm
&& isOffloadedPlaybackSupported(channelCount, sampleRate, encoding, audioAttributes);
Configuration pendingConfiguration = Configuration pendingConfiguration =
new Configuration( new Configuration(
isInputPcm, isInputPcm,
@ -497,7 +519,8 @@ public final class DefaultAudioSink implements AudioSink {
specifiedBufferSize, specifiedBufferSize,
processingEnabled, processingEnabled,
canApplyPlaybackParameters, canApplyPlaybackParameters,
availableAudioProcessors); availableAudioProcessors,
useOffload);
if (isInitialized()) { if (isInitialized()) {
this.pendingConfiguration = pendingConfiguration; this.pendingConfiguration = pendingConfiguration;
} else { } else {
@ -786,8 +809,9 @@ public final class DefaultAudioSink implements AudioSink {
} }
} else if (tunneling) { } else if (tunneling) {
Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, bytesWritten =
avSyncPresentationTimeUs); writeNonBlockingWithAvSyncV21(
audioTrack, buffer, bytesRemaining, avSyncPresentationTimeUs);
} else { } else {
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
} }
@ -1191,6 +1215,20 @@ public final class DefaultAudioSink implements AudioSink {
|| channelCount <= audioCapabilities.getMaxChannelCount()); || channelCount <= audioCapabilities.getMaxChannelCount());
} }
private static boolean isOffloadedPlaybackSupported(
int channelCount,
int sampleRateHz,
@C.Encoding int encoding,
AudioAttributes audioAttributes) {
if (Util.SDK_INT < 29) {
return false;
}
int channelMask = getChannelConfig(channelCount, /* isInputPcm= */ false);
AudioFormat audioFormat = getAudioFormat(sampleRateHz, channelMask, encoding);
return AudioManager.isOffloadedPlaybackSupported(
audioFormat, audioAttributes.getAudioAttributesV21());
}
private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {
int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE. int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO; int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
@ -1386,6 +1424,15 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
@RequiresApi(21)
private static AudioFormat getAudioFormat(int sampleRate, int channelConfig, int encoding) {
return new AudioFormat.Builder()
.setSampleRate(sampleRate)
.setChannelMask(channelConfig)
.setEncoding(encoding)
.build();
}
private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener {
@Override @Override
@ -1466,6 +1513,7 @@ public final class DefaultAudioSink implements AudioSink {
public final boolean processingEnabled; public final boolean processingEnabled;
public final boolean canApplyPlaybackParameters; public final boolean canApplyPlaybackParameters;
public final AudioProcessor[] availableAudioProcessors; public final AudioProcessor[] availableAudioProcessors;
public final boolean useOffload;
public Configuration( public Configuration(
boolean isInputPcm, boolean isInputPcm,
@ -1478,7 +1526,8 @@ public final class DefaultAudioSink implements AudioSink {
int specifiedBufferSize, int specifiedBufferSize,
boolean processingEnabled, boolean processingEnabled,
boolean canApplyPlaybackParameters, boolean canApplyPlaybackParameters,
AudioProcessor[] availableAudioProcessors) { AudioProcessor[] availableAudioProcessors,
boolean useOffload) {
this.isInputPcm = isInputPcm; this.isInputPcm = isInputPcm;
this.inputPcmFrameSize = inputPcmFrameSize; this.inputPcmFrameSize = inputPcmFrameSize;
this.inputSampleRate = inputSampleRate; this.inputSampleRate = inputSampleRate;
@ -1486,16 +1535,22 @@ 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 = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize();
this.processingEnabled = processingEnabled; this.processingEnabled = processingEnabled;
this.canApplyPlaybackParameters = canApplyPlaybackParameters; this.canApplyPlaybackParameters = canApplyPlaybackParameters;
this.availableAudioProcessors = availableAudioProcessors; this.availableAudioProcessors = availableAudioProcessors;
this.useOffload = useOffload;
// Call computeBufferSize() last as it depends on the other configuration values.
this.bufferSize = computeBufferSize(specifiedBufferSize);
} }
/** Returns if the configurations are sufficiently compatible to reuse the audio track. */
public boolean canReuseAudioTrack(Configuration audioTrackConfiguration) { public boolean canReuseAudioTrack(Configuration audioTrackConfiguration) {
return audioTrackConfiguration.outputEncoding == outputEncoding return audioTrackConfiguration.outputEncoding == outputEncoding
&& audioTrackConfiguration.outputSampleRate == outputSampleRate && audioTrackConfiguration.outputSampleRate == outputSampleRate
&& audioTrackConfiguration.outputChannelConfig == outputChannelConfig; && audioTrackConfiguration.outputChannelConfig == outputChannelConfig
&& audioTrackConfiguration.outputPcmFrameSize == outputPcmFrameSize
&& audioTrackConfiguration.useOffload == useOffload;
} }
public long inputFramesToDurationUs(long frameCount) { public long inputFramesToDurationUs(long frameCount) {
@ -1514,31 +1569,12 @@ public final class DefaultAudioSink implements AudioSink {
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) boolean tunneling, AudioAttributes audioAttributes, int audioSessionId)
throws InitializationException { throws InitializationException {
AudioTrack audioTrack; AudioTrack audioTrack;
if (Util.SDK_INT >= 21) { if (Util.SDK_INT >= 29) {
audioTrack = createAudioTrackV29(tunneling, audioAttributes, audioSessionId);
} else if (Util.SDK_INT >= 21) {
audioTrack = createAudioTrackV21(tunneling, audioAttributes, audioSessionId); audioTrack = createAudioTrackV21(tunneling, audioAttributes, audioSessionId);
} else { } else {
int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); audioTrack = createAudioTrack(audioAttributes, audioSessionId);
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioTrack =
new AudioTrack(
streamType,
outputSampleRate,
outputChannelConfig,
outputEncoding,
bufferSize,
MODE_STREAM);
} else {
// Re-attach to the same audio session.
audioTrack =
new AudioTrack(
streamType,
outputSampleRate,
outputChannelConfig,
outputEncoding,
bufferSize,
MODE_STREAM,
audioSessionId);
}
} }
int state = audioTrack.getState(); int state = audioTrack.getState();
@ -1554,56 +1590,106 @@ public final class DefaultAudioSink implements AudioSink {
return audioTrack; return audioTrack;
} }
@RequiresApi(29)
private AudioTrack createAudioTrackV29(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {
AudioFormat audioFormat =
getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding);
android.media.AudioAttributes audioTrackAttributes =
getAudioTrackAttributesV21(audioAttributes, tunneling);
return new AudioTrack.Builder()
.setAudioAttributes(audioTrackAttributes)
.setAudioFormat(audioFormat)
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize)
.setSessionId(audioSessionId)
.setOffloadedPlayback(useOffload)
.build();
}
@RequiresApi(21) @RequiresApi(21)
private AudioTrack createAudioTrackV21( private AudioTrack createAudioTrackV21(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) { boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {
android.media.AudioAttributes attributes;
if (tunneling) {
attributes =
new android.media.AudioAttributes.Builder()
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC)
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.build();
} else {
attributes = audioAttributes.getAudioAttributesV21();
}
AudioFormat format =
new AudioFormat.Builder()
.setChannelMask(outputChannelConfig)
.setEncoding(outputEncoding)
.setSampleRate(outputSampleRate)
.build();
return new AudioTrack( return new AudioTrack(
attributes, getAudioTrackAttributesV21(audioAttributes, tunneling),
format, getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding),
bufferSize, bufferSize,
MODE_STREAM, MODE_STREAM,
audioSessionId != C.AUDIO_SESSION_ID_UNSET audioSessionId);
? audioSessionId
: AudioManager.AUDIO_SESSION_ID_GENERATE);
} }
private int getDefaultBufferSize() { private AudioTrack createAudioTrack(AudioAttributes audioAttributes, int audioSessionId) {
if (isInputPcm) { int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);
int minBufferSize = if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding); return new AudioTrack(
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); streamType,
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; outputSampleRate,
int minAppBufferSize = outputChannelConfig,
(int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; outputEncoding,
int maxAppBufferSize = bufferSize,
(int) MODE_STREAM);
Math.max(
minBufferSize, durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
} else { } else {
// Re-attach to the same audio session.
return new AudioTrack(
streamType,
outputSampleRate,
outputChannelConfig,
outputEncoding,
bufferSize,
MODE_STREAM,
audioSessionId);
}
}
private int computeBufferSize(int specifiedBufferSize) {
if (specifiedBufferSize != 0) {
return specifiedBufferSize;
} else if (isInputPcm) {
return getPcmDefaultBufferSize();
} else if (useOffload) {
return getEncodedDefaultBufferSize(OFFLOAD_BUFFER_DURATION_US);
} else { // Passthrough
return getEncodedDefaultBufferSize(PASSTHROUGH_BUFFER_DURATION_US);
}
}
private int getEncodedDefaultBufferSize(long bufferDurationUs) {
int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding); int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding);
if (outputEncoding == C.ENCODING_AC3) { if (outputEncoding == C.ENCODING_AC3) {
rate *= AC3_BUFFER_MULTIPLICATION_FACTOR; rate *= AC3_BUFFER_MULTIPLICATION_FACTOR;
} }
return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND); return (int) (bufferDurationUs * rate / C.MICROS_PER_SECOND);
} }
private int getPcmDefaultBufferSize() {
int minBufferSize =
AudioTrack.getMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding);
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize =
Math.max(
minBufferSize, (int) durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
}
@RequiresApi(21)
private static android.media.AudioAttributes getAudioTrackAttributesV21(
AudioAttributes audioAttributes, boolean tunneling) {
if (tunneling) {
return getAudioTrackTunnelingAttributesV21();
} else {
return audioAttributes.getAudioAttributesV21();
}
}
@RequiresApi(21)
private static android.media.AudioAttributes getAudioTrackTunnelingAttributesV21() {
return new android.media.AudioAttributes.Builder()
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC)
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.build();
} }
} }
} }

View File

@ -63,7 +63,8 @@ public final class DefaultAudioSinkTest {
new DefaultAudioSink( new DefaultAudioSink(
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor), new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor),
/* enableConvertHighResIntPcmToFloat= */ false); /* enableFloatOutput= */ false,
/* enableOffload= */ false);
} }
@Test @Test