From ca0c090c1a98aa37bfaf6d85e1ae681f6d6f5236 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 23 Dec 2017 13:25:45 -0500 Subject: [PATCH] add support in mediacodecaudiorenderer for 24bit pcm to float --- .../audio/FloatResamplingAudioProcessor.java | 171 ++++++++++++++++++ .../audio/MediaCodecAudioRenderer.java | 108 +++++++++-- 2 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java 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 0000000000..28d2eca25f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -0,0 +1,171 @@ +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) { + 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; } + + @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: + 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: + 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.put((byte) (bits & 0xff)); + buffer.put((byte) ((bits >> 8) & 0xff)); + buffer.put((byte) ((bits >> 16) & 0xff)); + buffer.put((byte) ((bits >> 24) & 0xff)); + } + +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f73d63616b..d5e1b6ab03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -58,6 +58,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; + private final boolean dontDither24bitPCM; + private ByteBuffer resampledBuffer; + private FloatResamplingAudioProcessor floatResamplingAudioProcessor; /** * @param mediaCodecSelector A decoder selector. @@ -137,7 +140,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @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 dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, boolean dontDither24bitPCM, + AudioProcessor... audioProcessors) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + dontDither24bitPCM); } /** @@ -158,9 +191,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener, audioSink, false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioSink audioSink, + boolean dontDither24bitPCM) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; + this.dontDither24bitPCM = dontDither24bitPCM; audioSink.setListener(new AudioSinkListener()); } @@ -268,10 +326,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; + + // if the input is 24bit pcm audio and we explicitly said not to dither then convert it to float + if (dontDither24bitPCM && newFormat.pcmEncoding == C.ENCODING_PCM_24BIT) { + if (floatResamplingAudioProcessor == null) + floatResamplingAudioProcessor = new FloatResamplingAudioProcessor(); + pcmEncoding = floatResamplingAudioProcessor.getOutputEncoding(); + } else { + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding + : C.ENCODING_PCM_16BIT; + floatResamplingAudioProcessor = null; + } + channelCount = newFormat.channelCount; encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; @@ -302,9 +370,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { + if (floatResamplingAudioProcessor != null) + floatResamplingAudioProcessor.configure(sampleRate, channelCount, C.ENCODING_PCM_24BIT); audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioSink.ConfigurationException e) { + } catch (AudioSink.ConfigurationException | AudioProcessor.UnhandledFormatException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -420,19 +490,35 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codec.releaseOutputBuffer(bufferIndex, false); return true; } - if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); + resampledBuffer = null; return true; } try { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; + if (floatResamplingAudioProcessor != null) { + boolean draining = resampledBuffer != null; + if (!draining) { + floatResamplingAudioProcessor.queueInput(buffer); + resampledBuffer = floatResamplingAudioProcessor.getOutput(); + } + if (audioSink.handleBuffer(resampledBuffer, bufferPresentationTimeUs)) + resampledBuffer = null; + if (!draining) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } + } + else { + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex());