diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java | 1474 |
1 files changed, 1474 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java new file mode 100644 index 0000000000..9fc3fbbfd8 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -0,0 +1,1474 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.os.ConditionVariable; +import android.os.SystemClock; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback + * 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 + * a different duration than their input, and buffer processors must produce output corresponding to + * their last input immediately after that input is queued. This means that, for example, speed + * adjustment is not possible while using tunneling. + */ +public final class DefaultAudioSink implements AudioSink { + + /** + * Thrown when the audio track has provided a spurious timestamp, if {@link + * #failOnSpuriousAudioTimestamp} is set. + */ + public static final class InvalidAudioTrackTimestampException extends RuntimeException { + + /** + * Creates a new invalid timestamp exception with the specified message. + * + * @param message The detail message for this exception. + */ + private InvalidAudioTrackTimestampException(String message) { + super(message); + } + + } + + /** + * Provides a chain of audio processors, which are used for any user-defined processing and + * applying playback parameters (if supported). Because applying playback parameters can skip and + * stretch/compress audio, the sink will query the chain for information on how to transform its + * output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link + * #getSkippedOutputFrameCount()}. + */ + public interface AudioProcessorChain { + + /** + * Returns the fixed chain of audio processors that will process audio. This method is called + * once during initialization, but audio processors may change state to become active/inactive + * during playback. + */ + AudioProcessor[] getAudioProcessors(); + + /** + * Configures audio processors to apply the specified playback parameters immediately, returning + * the new parameters, which may differ from those passed in. Only called when processors have + * no input pending. + * + * @param playbackParameters The playback parameters to try to apply. + * @return The playback parameters that were actually applied. + */ + PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Scales the specified playout duration to take into account speedup due to audio processing, + * returning an input media duration, in arbitrary units. + */ + long getMediaDuration(long playoutDuration); + + /** + * Returns the number of output audio frames skipped since the audio processors were last + * flushed. + */ + long getSkippedOutputFrameCount(); + } + + /** + * The default audio processor chain, which applies a (possibly empty) chain of user-defined audio + * processors followed by {@link SilenceSkippingAudioProcessor} and {@link SonicAudioProcessor}. + */ + public static class DefaultAudioProcessorChain implements AudioProcessorChain { + + private final AudioProcessor[] audioProcessors; + private final SilenceSkippingAudioProcessor silenceSkippingAudioProcessor; + private final SonicAudioProcessor sonicAudioProcessor; + + /** + * Creates a new default chain of audio processors, with the user-defined {@code + * audioProcessors} applied before silence skipping and playback parameters. + */ + public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { + // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array + // rather than using Arrays.copyOf. + this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; + System.arraycopy( + /* src= */ audioProcessors, + /* srcPos= */ 0, + /* dest= */ this.audioProcessors, + /* destPos= */ 0, + /* length= */ audioProcessors.length); + silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); + sonicAudioProcessor = new SonicAudioProcessor(); + this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; + this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor; + } + + @Override + public AudioProcessor[] getAudioProcessors() { + return audioProcessors; + } + + @Override + public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) { + silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence); + return new PlaybackParameters( + sonicAudioProcessor.setSpeed(playbackParameters.speed), + sonicAudioProcessor.setPitch(playbackParameters.pitch), + playbackParameters.skipSilence); + } + + @Override + public long getMediaDuration(long playoutDuration) { + return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration); + } + + @Override + public long getSkippedOutputFrameCount() { + return silenceSkippingAudioProcessor.getSkippedFrames(); + } + } + + /** + * A minimum length for the {@link AudioTrack} buffer, in microseconds. + */ + private static final long MIN_BUFFER_DURATION_US = 250000; + /** + * A maximum length for the {@link AudioTrack} buffer, in microseconds. + */ + private static final long MAX_BUFFER_DURATION_US = 750000; + /** + * The length for passthrough {@link AudioTrack} buffers, in microseconds. + */ + private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000; + /** + * 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; + + /** + * @see AudioTrack#ERROR_BAD_VALUE + */ + private static final int ERROR_BAD_VALUE = AudioTrack.ERROR_BAD_VALUE; + /** + * @see AudioTrack#MODE_STATIC + */ + private static final int MODE_STATIC = AudioTrack.MODE_STATIC; + /** + * @see AudioTrack#MODE_STREAM + */ + private static final int MODE_STREAM = AudioTrack.MODE_STREAM; + /** + * @see AudioTrack#STATE_INITIALIZED + */ + private static final int STATE_INITIALIZED = AudioTrack.STATE_INITIALIZED; + /** + * @see AudioTrack#WRITE_NON_BLOCKING + */ + @SuppressLint("InlinedApi") + private static final int WRITE_NON_BLOCKING = AudioTrack.WRITE_NON_BLOCKING; + + private static final String TAG = "AudioTrack"; + + /** Represents states of the {@link #startMediaTimeUs} value. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({START_NOT_SET, START_IN_SYNC, START_NEED_SYNC}) + private @interface StartMediaTimeState {} + + private static final int START_NOT_SET = 0; + private static final int START_IN_SYNC = 1; + private static final int START_NEED_SYNC = 2; + + /** + * Whether to enable a workaround for an issue where an audio effect does not keep its session + * active across releasing/initializing a new audio track, on platform builds where + * {@link Util#SDK_INT} < 21. + * <p> + * The flag must be set before creating a player. + */ + public static boolean enablePreV21AudioSessionWorkaround = false; + + /** + * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is + * reported from {@link AudioTrack#getTimestamp}. + * <p> + * The flag must be set before creating a player. Should be set to {@code true} for testing and + * debugging purposes only. + */ + public static boolean failOnSpuriousAudioTimestamp = false; + + @Nullable private final AudioCapabilities audioCapabilities; + private final AudioProcessorChain audioProcessorChain; + private final boolean enableFloatOutput; + private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final TrimmingAudioProcessor trimmingAudioProcessor; + private final AudioProcessor[] toIntPcmAvailableAudioProcessors; + private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; + private final ConditionVariable releasingConditionVariable; + private final AudioTrackPositionTracker audioTrackPositionTracker; + private final ArrayDeque<PlaybackParametersCheckpoint> playbackParametersCheckpoints; + + @Nullable private Listener listener; + /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ + @Nullable private AudioTrack keepSessionIdAudioTrack; + + @Nullable private Configuration pendingConfiguration; + private Configuration configuration; + private AudioTrack audioTrack; + + private AudioAttributes audioAttributes; + @Nullable private PlaybackParameters afterDrainPlaybackParameters; + private PlaybackParameters playbackParameters; + private long playbackParametersOffsetUs; + private long playbackParametersPositionUs; + + @Nullable private ByteBuffer avSyncHeader; + private int bytesUntilNextAvSync; + + private long submittedPcmBytes; + private long submittedEncodedFrames; + private long writtenPcmBytes; + private long writtenEncodedFrames; + private int framesPerEncodedSample; + private @StartMediaTimeState int startMediaTimeState; + private long startMediaTimeUs; + private float volume; + + private AudioProcessor[] activeAudioProcessors; + private ByteBuffer[] outputBuffers; + @Nullable private ByteBuffer inputBuffer; + @Nullable private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; + private int drainingAudioProcessorIndex; + private boolean handledEndOfStream; + private boolean stoppedAudioTrack; + + private boolean playing; + private int audioSessionId; + private AuxEffectInfo auxEffectInfo; + private boolean tunneling; + private long lastFeedElapsedRealtimeMs; + + /** + * Creates a new default audio sink. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { + this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false); + } + + /** + * Creates a new default audio sink, optionally using float output for high resolution PCM. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * 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 + * be available when float output is in use. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors, + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); + } + + /** + * Creates a new default audio sink, optionally using float output for high resolution PCM and + * with the specified {@code audioProcessorChain}. + * + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback + * parameters adjustments. The instance passed in must not be reused in other sinks. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * 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 + * be available when float output is in use. + */ + public DefaultAudioSink( + @Nullable AudioCapabilities audioCapabilities, + AudioProcessorChain audioProcessorChain, + boolean enableFloatOutput) { + this.audioCapabilities = audioCapabilities; + this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); + this.enableFloatOutput = enableFloatOutput; + releasingConditionVariable = new ConditionVariable(true); + audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); + channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + trimmingAudioProcessor = new TrimmingAudioProcessor(); + ArrayList<AudioProcessor> toIntPcmAudioProcessors = new ArrayList<>(); + Collections.addAll( + toIntPcmAudioProcessors, + new ResamplingAudioProcessor(), + channelMappingAudioProcessor, + trimmingAudioProcessor); + Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors()); + toIntPcmAvailableAudioProcessors = toIntPcmAudioProcessors.toArray(new AudioProcessor[0]); + toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; + volume = 1.0f; + startMediaTimeState = START_NOT_SET; + audioAttributes = AudioAttributes.DEFAULT; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + auxEffectInfo = new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, 0f); + playbackParameters = PlaybackParameters.DEFAULT; + drainingAudioProcessorIndex = C.INDEX_UNSET; + activeAudioProcessors = new AudioProcessor[0]; + outputBuffers = new ByteBuffer[0]; + playbackParametersCheckpoints = new ArrayDeque<>(); + } + + // AudioSink implementation. + + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override + public boolean supportsOutput(int channelCount, @C.Encoding int encoding) { + if (Util.isEncodingLinearPcm(encoding)) { + // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float + // output from platform API version 21 only. Other integer PCM encodings are resampled by this + // 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. + return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; + } else { + return audioCapabilities != null + && audioCapabilities.supportsEncoding(encoding) + && (channelCount == Format.NO_VALUE + || channelCount <= audioCapabilities.getMaxChannelCount()); + } + } + + @Override + public long getCurrentPositionUs(boolean sourceEnded) { + if (!isInitialized() || startMediaTimeState == START_NOT_SET) { + return CURRENT_POSITION_NOT_SET; + } + long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); + positionUs = Math.min(positionUs, configuration.framesToDurationUs(getWrittenFrames())); + return startMediaTimeUs + applySkipping(applySpeedup(positionUs)); + } + + @Override + public void configure( + @C.Encoding int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { + if (Util.SDK_INT < 21 && inputChannelCount == 8 && outputChannels == null) { + // AudioTrack doesn't support 8 channel output before Android L. Discard the last two (side) + // channels to give a 6 channel stream that is supported. + outputChannels = new int[6]; + for (int i = 0; i < outputChannels.length; i++) { + outputChannels[i] = i; + } + } + + boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); + boolean processingEnabled = isInputPcm; + int sampleRate = inputSampleRate; + int channelCount = inputChannelCount; + @C.Encoding int encoding = inputEncoding; + boolean useFloatOutput = + enableFloatOutput + && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) + && Util.isEncodingHighResolutionPcm(inputEncoding); + AudioProcessor[] availableAudioProcessors = + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; + if (processingEnabled) { + trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); + channelMappingAudioProcessor.setChannelMap(outputChannels); + AudioProcessor.AudioFormat outputFormat = + new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); + for (AudioProcessor audioProcessor : availableAudioProcessors) { + try { + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } + } catch (UnhandledAudioFormatException e) { + throw new ConfigurationException(e); + } + } + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; + } + + int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); + if (outputChannelConfig == AudioFormat.CHANNEL_INVALID) { + throw new ConfigurationException("Unsupported channel count: " + channelCount); + } + + int inputPcmFrameSize = + isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; + int outputPcmFrameSize = + isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; + Configuration pendingConfiguration = + new Configuration( + isInputPcm, + inputPcmFrameSize, + inputSampleRate, + outputPcmFrameSize, + sampleRate, + outputChannelConfig, + encoding, + specifiedBufferSize, + processingEnabled, + canApplyPlaybackParameters, + availableAudioProcessors); + if (isInitialized()) { + this.pendingConfiguration = pendingConfiguration; + } else { + configuration = pendingConfiguration; + } + } + + private void setupAudioProcessors() { + AudioProcessor[] audioProcessors = configuration.availableAudioProcessors; + ArrayList<AudioProcessor> newAudioProcessors = new ArrayList<>(); + for (AudioProcessor audioProcessor : audioProcessors) { + if (audioProcessor.isActive()) { + newAudioProcessors.add(audioProcessor); + } else { + audioProcessor.flush(); + } + } + int count = newAudioProcessors.size(); + activeAudioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]); + outputBuffers = new ByteBuffer[count]; + flushAudioProcessors(); + } + + private void flushAudioProcessors() { + for (int i = 0; i < activeAudioProcessors.length; i++) { + AudioProcessor audioProcessor = activeAudioProcessors[i]; + audioProcessor.flush(); + outputBuffers[i] = audioProcessor.getOutput(); + } + } + + private void initialize(long presentationTimeUs) throws InitializationException { + // If we're asynchronously releasing a previous audio track then we block until it has been + // released. This guarantees that we cannot end up in a state where we have multiple audio + // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust + // the shared memory that's available for audio track buffers. This would in turn cause the + // initialization of the audio track to fail. + releasingConditionVariable.block(); + + audioTrack = + Assertions.checkNotNull(configuration) + .buildAudioTrack(tunneling, audioAttributes, audioSessionId); + int audioSessionId = audioTrack.getAudioSessionId(); + if (enablePreV21AudioSessionWorkaround) { + if (Util.SDK_INT < 21) { + // The workaround creates an audio track with a two byte buffer on the same session, and + // does not release it until this object is released, which keeps the session active. + if (keepSessionIdAudioTrack != null + && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) { + releaseKeepSessionIdAudioTrack(); + } + if (keepSessionIdAudioTrack == null) { + keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId); + } + } + } + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + if (listener != null) { + listener.onAudioSessionId(audioSessionId); + } + } + + applyPlaybackParameters(playbackParameters, presentationTimeUs); + + audioTrackPositionTracker.setAudioTrack( + audioTrack, + configuration.outputEncoding, + configuration.outputPcmFrameSize, + configuration.bufferSize); + setVolumeInternal(); + + if (auxEffectInfo.effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.attachAuxEffect(auxEffectInfo.effectId); + audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel); + } + } + + @Override + public void play() { + playing = true; + if (isInitialized()) { + audioTrackPositionTracker.start(); + audioTrack.play(); + } + } + + @Override + public void handleDiscontinuity() { + // Force resynchronization after a skipped buffer. + if (startMediaTimeState == START_IN_SYNC) { + startMediaTimeState = START_NEED_SYNC; + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); + + if (pendingConfiguration != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // There's still pending data in audio processors to write to the track. + return false; + } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) { + playPendingData(); + if (hasPendingData()) { + // We're waiting for playout on the current audio track to finish. + return false; + } + flush(); + } else { + // The current audio track can be reused for the new configuration. + configuration = pendingConfiguration; + pendingConfiguration = null; + } + // Re-apply playback parameters. + applyPlaybackParameters(playbackParameters, presentationTimeUs); + } + + if (!isInitialized()) { + initialize(presentationTimeUs); + if (playing) { + play(); + } + } + + if (!audioTrackPositionTracker.mayHandleBuffer(getWrittenFrames())) { + return false; + } + + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { + // The buffer is empty. + return true; + } + + if (!configuration.isInputPcm && framesPerEncodedSample == 0) { + // If this is the first encoded sample, calculate the sample size in frames. + framesPerEncodedSample = getFramesPerEncodedSample(configuration.outputEncoding, buffer); + if (framesPerEncodedSample == 0) { + // We still don't know the number of frames per sample, so drop the buffer. + // For TrueHD this can occur after some seek operations, as not every sample starts with + // a syncframe header. If we chunked samples together so the extracted samples always + // started with a syncframe header, the chunks would be too large. + return true; + } + } + + if (afterDrainPlaybackParameters != null) { + if (!drainAudioProcessorsToEndOfStream()) { + // Don't process any more input until draining completes. + return false; + } + PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; + applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); + } + + if (startMediaTimeState == START_NOT_SET) { + startMediaTimeUs = Math.max(0, presentationTimeUs); + startMediaTimeState = START_IN_SYNC; + } else { + // Sanity check that presentationTimeUs is consistent with the expected value. + long expectedPresentationTimeUs = + startMediaTimeUs + + configuration.inputFramesToDurationUs( + getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); + if (startMediaTimeState == START_IN_SYNC + && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { + Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got " + + presentationTimeUs + "]"); + startMediaTimeState = START_NEED_SYNC; + } + if (startMediaTimeState == START_NEED_SYNC) { + // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the + // number of bytes submitted. + long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs; + startMediaTimeUs += adjustmentUs; + startMediaTimeState = START_IN_SYNC; + if (listener != null && adjustmentUs != 0) { + listener.onPositionDiscontinuity(); + } + } + } + + if (configuration.isInputPcm) { + submittedPcmBytes += buffer.remaining(); + } else { + submittedEncodedFrames += framesPerEncodedSample; + } + + inputBuffer = buffer; + } + + if (configuration.processingEnabled) { + processBuffers(presentationTimeUs); + } else { + writeBuffer(inputBuffer, presentationTimeUs); + } + + if (!inputBuffer.hasRemaining()) { + inputBuffer = null; + return true; + } + + if (audioTrackPositionTracker.isStalled(getWrittenFrames())) { + Log.w(TAG, "Resetting stalled audio track"); + flush(); + return true; + } + + return false; + } + + private void processBuffers(long avSyncPresentationTimeUs) throws WriteException { + int count = activeAudioProcessors.length; + int index = count; + while (index >= 0) { + ByteBuffer input = index > 0 ? outputBuffers[index - 1] + : (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER); + if (index == count) { + writeBuffer(input, avSyncPresentationTimeUs); + } else { + AudioProcessor audioProcessor = activeAudioProcessors[index]; + audioProcessor.queueInput(input); + ByteBuffer output = audioProcessor.getOutput(); + outputBuffers[index] = output; + if (output.hasRemaining()) { + // Handle the output as input to the next audio processor or the AudioTrack. + index++; + continue; + } + } + + if (input.hasRemaining()) { + // The input wasn't consumed and no output was produced, so give up for now. + return; + } + + // Get more input from upstream. + index--; + } + } + + @SuppressWarnings("ReferenceEquality") + private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException { + if (!buffer.hasRemaining()) { + return; + } + if (outputBuffer != null) { + Assertions.checkArgument(outputBuffer == buffer); + } else { + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = buffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = buffer.position(); + buffer.get(preV21OutputBuffer, 0, bytesRemaining); + buffer.position(originalPosition); + preV21OutputBufferOffset = 0; + } + } + int bytesRemaining = buffer.remaining(); + int bytesWritten = 0; + if (Util.SDK_INT < 21) { // isInputPcm == true + // Work out how many bytes we can write without the risk of blocking. + int bytesToWrite = audioTrackPositionTracker.getAvailableBufferSize(writtenPcmBytes); + if (bytesToWrite > 0) { + bytesToWrite = Math.min(bytesRemaining, bytesToWrite); + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + buffer.position(buffer.position() + bytesWritten); + } + } + } else if (tunneling) { + Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET); + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, + avSyncPresentationTimeUs); + } else { + bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + } + + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + + if (bytesWritten < 0) { + throw new WriteException(bytesWritten); + } + + if (configuration.isInputPcm) { + writtenPcmBytes += bytesWritten; + } + if (bytesWritten == bytesRemaining) { + if (!configuration.isInputPcm) { + writtenEncodedFrames += framesPerEncodedSample; + } + outputBuffer = null; + } + } + + @Override + public void playToEndOfStream() throws WriteException { + if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) { + playPendingData(); + handledEndOfStream = true; + } + } + + private boolean drainAudioProcessorsToEndOfStream() throws WriteException { + boolean audioProcessorNeedsEndOfStream = false; + if (drainingAudioProcessorIndex == C.INDEX_UNSET) { + drainingAudioProcessorIndex = + configuration.processingEnabled ? 0 : activeAudioProcessors.length; + audioProcessorNeedsEndOfStream = true; + } + while (drainingAudioProcessorIndex < activeAudioProcessors.length) { + AudioProcessor audioProcessor = activeAudioProcessors[drainingAudioProcessorIndex]; + if (audioProcessorNeedsEndOfStream) { + audioProcessor.queueEndOfStream(); + } + processBuffers(C.TIME_UNSET); + if (!audioProcessor.isEnded()) { + return false; + } + audioProcessorNeedsEndOfStream = true; + drainingAudioProcessorIndex++; + } + + // Finish writing any remaining output to the track. + if (outputBuffer != null) { + writeBuffer(outputBuffer, C.TIME_UNSET); + if (outputBuffer != null) { + return false; + } + } + drainingAudioProcessorIndex = C.INDEX_UNSET; + return true; + } + + @Override + public boolean isEnded() { + return !isInitialized() || (handledEndOfStream && !hasPendingData()); + } + + @Override + public boolean hasPendingData() { + return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + if (configuration != null && !configuration.canApplyPlaybackParameters) { + this.playbackParameters = PlaybackParameters.DEFAULT; + return; + } + PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); + if (!playbackParameters.equals(lastSetPlaybackParameters)) { + if (isInitialized()) { + // Drain the audio processors so we can determine the frame position at which the new + // parameters apply. + afterDrainPlaybackParameters = playbackParameters; + } else { + // Update the playback parameters now. They will be applied to the audio processors during + // initialization. + this.playbackParameters = playbackParameters; + } + } + } + + @Override + public PlaybackParameters getPlaybackParameters() { + // Mask the already set parameters. + return afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : playbackParameters; + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + if (this.audioAttributes.equals(audioAttributes)) { + return; + } + this.audioAttributes = audioAttributes; + if (tunneling) { + // The audio attributes are ignored in tunneling mode, so no need to reset. + return; + } + flush(); + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + @Override + public void setAudioSessionId(int audioSessionId) { + if (this.audioSessionId != audioSessionId) { + this.audioSessionId = audioSessionId; + flush(); + } + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + if (this.auxEffectInfo.equals(auxEffectInfo)) { + return; + } + int effectId = auxEffectInfo.effectId; + float sendLevel = auxEffectInfo.sendLevel; + if (audioTrack != null) { + if (this.auxEffectInfo.effectId != effectId) { + audioTrack.attachAuxEffect(effectId); + } + if (effectId != AuxEffectInfo.NO_AUX_EFFECT_ID) { + audioTrack.setAuxEffectSendLevel(sendLevel); + } + } + this.auxEffectInfo = auxEffectInfo; + } + + @Override + public void enableTunnelingV21(int tunnelingAudioSessionId) { + Assertions.checkState(Util.SDK_INT >= 21); + if (!tunneling || audioSessionId != tunnelingAudioSessionId) { + tunneling = true; + audioSessionId = tunnelingAudioSessionId; + flush(); + } + } + + @Override + public void disableTunneling() { + if (tunneling) { + tunneling = false; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + flush(); + } + } + + @Override + public void setVolume(float volume) { + if (this.volume != volume) { + this.volume = volume; + setVolumeInternal(); + } + } + + private void setVolumeInternal() { + if (!isInitialized()) { + // Do nothing. + } else if (Util.SDK_INT >= 21) { + setVolumeInternalV21(audioTrack, volume); + } else { + setVolumeInternalV3(audioTrack, volume); + } + } + + @Override + public void pause() { + playing = false; + if (isInitialized() && audioTrackPositionTracker.pause()) { + audioTrack.pause(); + } + } + + @Override + public void flush() { + if (isInitialized()) { + submittedPcmBytes = 0; + submittedEncodedFrames = 0; + writtenPcmBytes = 0; + writtenEncodedFrames = 0; + framesPerEncodedSample = 0; + if (afterDrainPlaybackParameters != null) { + playbackParameters = afterDrainPlaybackParameters; + afterDrainPlaybackParameters = null; + } else if (!playbackParametersCheckpoints.isEmpty()) { + playbackParameters = playbackParametersCheckpoints.getLast().playbackParameters; + } + playbackParametersCheckpoints.clear(); + playbackParametersOffsetUs = 0; + playbackParametersPositionUs = 0; + trimmingAudioProcessor.resetTrimmedFrameCount(); + flushAudioProcessors(); + inputBuffer = null; + outputBuffer = null; + stoppedAudioTrack = false; + handledEndOfStream = false; + drainingAudioProcessorIndex = C.INDEX_UNSET; + avSyncHeader = null; + bytesUntilNextAvSync = 0; + startMediaTimeState = START_NOT_SET; + if (audioTrackPositionTracker.isPlaying()) { + audioTrack.pause(); + } + // AudioTrack.release can take some time, so we call it on a background thread. + final AudioTrack toRelease = audioTrack; + audioTrack = null; + if (pendingConfiguration != null) { + configuration = pendingConfiguration; + pendingConfiguration = null; + } + audioTrackPositionTracker.reset(); + releasingConditionVariable.close(); + new Thread() { + @Override + public void run() { + try { + toRelease.flush(); + toRelease.release(); + } finally { + releasingConditionVariable.open(); + } + } + }.start(); + } + } + + @Override + public void reset() { + flush(); + releaseKeepSessionIdAudioTrack(); + for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { + audioProcessor.reset(); + } + for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) { + audioProcessor.reset(); + } + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + playing = false; + } + + /** + * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. + */ + private void releaseKeepSessionIdAudioTrack() { + if (keepSessionIdAudioTrack == null) { + return; + } + + // AudioTrack.release can take some time, so we call it on a background thread. + final AudioTrack toRelease = keepSessionIdAudioTrack; + keepSessionIdAudioTrack = null; + new Thread() { + @Override + public void run() { + toRelease.release(); + } + }.start(); + } + + private void applyPlaybackParameters( + PlaybackParameters playbackParameters, long presentationTimeUs) { + PlaybackParameters newPlaybackParameters = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + } + + private long applySpeedup(long positionUs) { + @Nullable PlaybackParametersCheckpoint checkpoint = null; + while (!playbackParametersCheckpoints.isEmpty() + && positionUs >= playbackParametersCheckpoints.getFirst().positionUs) { + checkpoint = playbackParametersCheckpoints.remove(); + } + if (checkpoint != null) { + // We are playing (or about to play) media with the new playback parameters, so update them. + playbackParameters = checkpoint.playbackParameters; + playbackParametersPositionUs = checkpoint.positionUs; + playbackParametersOffsetUs = checkpoint.mediaTimeUs - startMediaTimeUs; + } + + if (playbackParameters.speed == 1f) { + return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; + } + + if (playbackParametersCheckpoints.isEmpty()) { + return playbackParametersOffsetUs + + audioProcessorChain.getMediaDuration(positionUs - playbackParametersPositionUs); + } + + // We are playing data at a previous playback speed, so fall back to multiplying by the speed. + return playbackParametersOffsetUs + + Util.getMediaDurationForPlayoutDuration( + positionUs - playbackParametersPositionUs, playbackParameters.speed); + } + + private long applySkipping(long positionUs) { + return positionUs + + configuration.framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); + } + + private boolean isInitialized() { + return audioTrack != null; + } + + private long getSubmittedFrames() { + return configuration.isInputPcm + ? (submittedPcmBytes / configuration.inputPcmFrameSize) + : submittedEncodedFrames; + } + + private long getWrittenFrames() { + return configuration.isInputPcm + ? (writtenPcmBytes / configuration.outputPcmFrameSize) + : writtenEncodedFrames; + } + + private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { + int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE. + int channelConfig = AudioFormat.CHANNEL_OUT_MONO; + @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; + int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. + return new AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, bufferSize, + MODE_STATIC, audioSessionId); + } + + private static int getChannelConfig(int channelCount, boolean isInputPcm) { + if (Util.SDK_INT <= 28 && !isInputPcm) { + // In passthrough mode the channel count used to configure the audio track doesn't affect how + // the stream is handled, except that some devices do overly-strict channel configuration + // checks. Therefore we override the channel count so that a known-working channel + // configuration is chosen in all cases. See [Internal: b/29116190]. + if (channelCount == 7) { + channelCount = 8; + } else if (channelCount == 3 || channelCount == 4 || channelCount == 5) { + channelCount = 6; + } + } + + // Workaround for Nexus Player not reporting support for mono passthrough. + // (See [Internal: b/34268671].) + if (Util.SDK_INT <= 26 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { + channelCount = 2; + } + + return Util.getAudioTrackChannelConfig(channelCount); + } + + private static int getMaximumEncodedRateBytesPerSecond(@C.Encoding int encoding) { + switch (encoding) { + case C.ENCODING_AC3: + return 640 * 1000 / 8; + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return 6144 * 1000 / 8; + case C.ENCODING_AC4: + return 2688 * 1000 / 8; + case C.ENCODING_DTS: + // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. + return 1536 * 1000 / 8; + case C.ENCODING_DTS_HD: + return 18000 * 1000 / 8; + case C.ENCODING_DOLBY_TRUEHD: + return 24500 * 1000 / 8; + case C.ENCODING_INVALID: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_24BIT: + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_FLOAT: + case Format.NO_VALUE: + default: + throw new IllegalArgumentException(); + } + } + + private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { + switch (encoding) { + case C.ENCODING_MP3: + return MpegAudioHeader.getFrameSampleCount(buffer.get(buffer.position())); + case C.ENCODING_DTS: + case C.ENCODING_DTS_HD: + return DtsUtil.parseDtsAudioSampleCount(buffer); + case C.ENCODING_AC3: + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); + case C.ENCODING_AC4: + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); + case C.ENCODING_DOLBY_TRUEHD: + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + default: + throw new IllegalStateException("Unexpected audio encoding: " + encoding); + } + } + + @TargetApi(21) + private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); + } + + @TargetApi(21) + private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size, + long presentationTimeUs) { + if (Util.SDK_INT >= 26) { + // The underlying platform AudioTrack writes AV sync headers directly. + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + } + if (avSyncHeader == null) { + avSyncHeader = ByteBuffer.allocate(16); + avSyncHeader.order(ByteOrder.BIG_ENDIAN); + avSyncHeader.putInt(0x55550001); + } + if (bytesUntilNextAvSync == 0) { + avSyncHeader.putInt(4, size); + avSyncHeader.putLong(8, presentationTimeUs * 1000); + avSyncHeader.position(0); + bytesUntilNextAvSync = size; + } + int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); + if (avSyncHeaderBytesRemaining > 0) { + int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + if (result < avSyncHeaderBytesRemaining) { + return 0; + } + } + int result = writeNonBlockingV21(audioTrack, buffer, size); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + bytesUntilNextAvSync -= result; + return result; + } + + @TargetApi(21) + private static void setVolumeInternalV21(AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + + private void playPendingData() { + if (!stoppedAudioTrack) { + stoppedAudioTrack = true; + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); + bytesUntilNextAvSync = 0; + } + } + + /** Stores playback parameters with the position and media time at which they apply. */ + private static final class PlaybackParametersCheckpoint { + + private final PlaybackParameters playbackParameters; + private final long mediaTimeUs; + private final long positionUs; + + private PlaybackParametersCheckpoint(PlaybackParameters playbackParameters, long mediaTimeUs, + long positionUs) { + this.playbackParameters = playbackParameters; + this.mediaTimeUs = mediaTimeUs; + this.positionUs = positionUs; + } + + } + + private final class PositionTrackerListener implements AudioTrackPositionTracker.Listener { + + @Override + public void onPositionFramesMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (frame position mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onSystemTimeUsMismatch( + long audioTimestampPositionFrames, + long audioTimestampSystemTimeUs, + long systemTimeUs, + long playbackPositionUs) { + String message = + "Spurious audio timestamp (system clock mismatch): " + + audioTimestampPositionFrames + + ", " + + audioTimestampSystemTimeUs + + ", " + + systemTimeUs + + ", " + + playbackPositionUs + + ", " + + getSubmittedFrames() + + ", " + + getWrittenFrames(); + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + } + + @Override + public void onInvalidLatency(long latencyUs) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); + } + + @Override + public void onUnderrun(int bufferSize, long bufferSizeMs) { + if (listener != null) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + } + + /** Stores configuration relating to the audio format. */ + private static final class Configuration { + + public final boolean isInputPcm; + public final int inputPcmFrameSize; + public final int inputSampleRate; + public final int outputPcmFrameSize; + public final int outputSampleRate; + public final int outputChannelConfig; + @C.Encoding public final int outputEncoding; + public final int bufferSize; + public final boolean processingEnabled; + public final boolean canApplyPlaybackParameters; + public final AudioProcessor[] availableAudioProcessors; + + public Configuration( + boolean isInputPcm, + int inputPcmFrameSize, + int inputSampleRate, + int outputPcmFrameSize, + int outputSampleRate, + int outputChannelConfig, + int outputEncoding, + int specifiedBufferSize, + boolean processingEnabled, + boolean canApplyPlaybackParameters, + AudioProcessor[] availableAudioProcessors) { + this.isInputPcm = isInputPcm; + this.inputPcmFrameSize = inputPcmFrameSize; + this.inputSampleRate = inputSampleRate; + this.outputPcmFrameSize = outputPcmFrameSize; + this.outputSampleRate = outputSampleRate; + this.outputChannelConfig = outputChannelConfig; + this.outputEncoding = outputEncoding; + this.bufferSize = specifiedBufferSize != 0 ? specifiedBufferSize : getDefaultBufferSize(); + this.processingEnabled = processingEnabled; + this.canApplyPlaybackParameters = canApplyPlaybackParameters; + this.availableAudioProcessors = availableAudioProcessors; + } + + public boolean canReuseAudioTrack(Configuration audioTrackConfiguration) { + return audioTrackConfiguration.outputEncoding == outputEncoding + && audioTrackConfiguration.outputSampleRate == outputSampleRate + && audioTrackConfiguration.outputChannelConfig == outputChannelConfig; + } + + public long inputFramesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / inputSampleRate; + } + + public long framesToDurationUs(long frameCount) { + 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 { + AudioTrack audioTrack; + if (Util.SDK_INT >= 21) { + audioTrack = createAudioTrackV21(tunneling, audioAttributes, audioSessionId); + } else { + int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); + 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(); + if (state != STATE_INITIALIZED) { + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if + // release were to fail too. Swallow the exception. + } + throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize); + } + return audioTrack; + } + + @TargetApi(21) + private AudioTrack createAudioTrackV21( + 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( + attributes, + format, + bufferSize, + MODE_STREAM, + audioSessionId != C.AUDIO_SESSION_ID_UNSET + ? audioSessionId + : AudioManager.AUDIO_SESSION_ID_GENERATE); + } + + private int getDefaultBufferSize() { + if (isInputPcm) { + 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 = + (int) + Math.max( + minBufferSize, durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); + return Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + } else { + int rate = getMaximumEncodedRateBytesPerSecond(outputEncoding); + if (outputEncoding == C.ENCODING_AC3) { + rate *= AC3_BUFFER_MULTIPLICATION_FACTOR; + } + return (int) (PASSTHROUGH_BUFFER_DURATION_US * rate / C.MICROS_PER_SECOND); + } + } + } +} |