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 e3bf72c541a..892fd428ff8 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 @@ -164,10 +164,12 @@ public InvalidAudioTrackTimestampException(String message) { public static boolean failOnSpuriousAudioTimestamp = false; @Nullable private final AudioCapabilities audioCapabilities; + private final boolean canConvertHiResPcmToFloat; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; - private final AudioProcessor[] availableAudioProcessors; + private final AudioProcessor[] toIntPcmAvailableAudioProcessors; + private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -180,12 +182,14 @@ public InvalidAudioTrackTimestampException(String message) { private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; private boolean isInputPcm; + private boolean shouldUpResPCMAudio; private int inputSampleRate; private int sampleRate; private int channelConfig; private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; private boolean processingEnabled; + private boolean canApplyPlaybackParams; private int bufferSize; private long bufferSizeUs; @@ -233,6 +237,8 @@ public InvalidAudioTrackTimestampException(String message) { private boolean hasData; private long lastFeedElapsedRealtimeMs; + + /** * @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. @@ -241,7 +247,23 @@ public InvalidAudioTrackTimestampException(String message) { */ public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { + this(audioCapabilities, audioProcessors, false); + } + + /** + * @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 canConvertHiResPcmToFloat Flag to convert > 16bit PCM Audio to 32bit Float PCM Audio to + * avoid dithering the input audio. If enabled other audio processors that expect 16bit PCM + * are disabled + */ + public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors, boolean canConvertHiResPcmToFloat) { + this.audioCapabilities = audioCapabilities; + this.canConvertHiResPcmToFloat = canConvertHiResPcmToFloat; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { @@ -259,12 +281,14 @@ public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); - availableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; - availableAudioProcessors[0] = new ResamplingAudioProcessor(); - availableAudioProcessors[1] = channelMappingAudioProcessor; - availableAudioProcessors[2] = trimmingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); - availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; + toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor(); + toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor; + toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor; + System.arraycopy(audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); + toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + toFloatPcmAvailableAudioProcessors = new AudioProcessor[1]; + toFloatPcmAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -342,12 +366,17 @@ public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int int channelCount = inputChannelCount; int sampleRate = inputSampleRate; isInputPcm = isEncodingPcm(inputEncoding); + shouldUpResPCMAudio = canConvertHiResPcmToFloat && + (inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); if (isInputPcm) { pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); } @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + canApplyPlaybackParams = processingEnabled && !shouldUpResPCMAudio; if (processingEnabled) { + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { @@ -460,6 +489,8 @@ public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int private void resetAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; for (AudioProcessor audioProcessor : availableAudioProcessors) { if (audioProcessor.isActive()) { newAudioProcessors.add(audioProcessor); @@ -808,7 +839,7 @@ public boolean hasPendingData() { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (isInitialized() && !processingEnabled) { + if (isInitialized() && !canApplyPlaybackParams) { // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; @@ -964,7 +995,10 @@ public void run() { public void release() { reset(); releaseKeepSessionIdAudioTrack(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { + audioProcessor.reset(); + } + for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) { audioProcessor.reset(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java new file mode 100644 index 00000000000..f7073f12759 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2018 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class FloatResamplingAudioProcessor implements AudioProcessor { + + private int sampleRateHz; + private static final double PCM_INT32_FLOAT = 1.0 / 0x7fffffff; + + private int channelCount; + @C.PcmEncoding + private int sourceEncoding; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ + public FloatResamplingAudioProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws AudioProcessor.UnhandledFormatException { + if (encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new AudioProcessor.UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.sourceEncoding == encoding) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.sourceEncoding = encoding; + + return true; + } + + @Override + public boolean isActive() { + return sourceEncoding == C.ENCODING_PCM_24BIT || sourceEncoding == C.ENCODING_PCM_32BIT; + } + + @Override + public int getOutputChannelCount() { return channelCount; } + + @Override + public int getOutputEncoding() { return C.ENCODING_PCM_FLOAT; } + + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int offset = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - offset; + + int resampledSize; + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 4; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size; + break; + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Samples are little endian. + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + // 24->32 bit resampling. + for (int i = offset; i < limit; i += 3) { + int val = (inputBuffer.get(i) << 8) & 0x0000ff00 | (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | + (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->32 bit conversion. + for (int i = offset; i < limit; i += 4) { + int val = inputBuffer.get(i) & 0x000000ff | (inputBuffer.get(i) << 8) & 0x0000ff00 | + (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + } + + /** + * Converts the provided value into 32-bit float PCM and writes to buffer. + * + * @param val 32-bit int value to convert to 32-bit float [-1.0, 1.0] + * @param buffer The output buffer. + */ + private static void writePcm32bitFloat(int val, ByteBuffer buffer) { + float convVal = (float) (PCM_INT32_FLOAT * val); + int bits = Float.floatToIntBits(convVal); + if (bits == 0x7fc00000) + bits = Float.floatToIntBits((float) 0.0); + buffer.putInt(bits); + } + +} \ No newline at end of file