diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 2f343ec40e..76b5ec72fe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -270,6 +270,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final ChannelMappingBufferProcessor channelMappingBufferProcessor; private final BufferProcessor[] availableBufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; @@ -343,9 +344,11 @@ public final class AudioTrack { public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; - availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + channelMappingBufferProcessor = new ChannelMappingBufferProcessor(); + availableBufferProcessors = new BufferProcessor[bufferProcessors.length + 2]; availableBufferProcessors[0] = new ResamplingBufferProcessor(); - System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 1, bufferProcessors.length); + availableBufferProcessors[1] = channelMappingBufferProcessor; + System.arraycopy(bufferProcessors, 0, availableBufferProcessors, 2, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -449,6 +452,30 @@ public final class AudioTrack { */ public void configure(String mimeType, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); + } + + /** + * Configures (or reconfigures) the audio track. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size automatically. + * @param outputChannels A mapping from input to output channels that is applied to this track's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the + * map is applied the audio data will have {@code outputChannels.length} channels. + * @throws ConfigurationException If an error occurs configuring the track. + */ + public void configure(String mimeType, int channelCount, int sampleRate, + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) + throws ConfigurationException { boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; boolean flush = false; @@ -456,17 +483,15 @@ public final class AudioTrack { pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); // Reconfigure the buffer processors. + channelMappingBufferProcessor.setChannelMap(outputChannels); ArrayList newBufferProcessors = new ArrayList<>(); for (BufferProcessor bufferProcessor : availableBufferProcessors) { - boolean wasActive = bufferProcessor.isActive(); try { flush |= bufferProcessor.configure(sampleRate, channelCount, encoding); } catch (BufferProcessor.UnhandledFormatException e) { throw new ConfigurationException(e); } - boolean isActive = bufferProcessor.isActive(); - flush |= isActive != wasActive; - if (isActive) { + if (bufferProcessor.isActive()) { newBufferProcessors.add(bufferProcessor); channelCount = bufferProcessor.getOutputChannelCount(); encoding = bufferProcessor.getOutputEncoding(); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index c31823fd3b..87d4e5fe7b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -42,16 +42,18 @@ public interface BufferProcessor { ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); /** - * Configures the processor to process input buffers with the specified format and returns whether - * the processor must be flushed. After calling this method, {@link #isActive()} returns whether - * the processor needs to handle buffers; if not, the processor will not accept any buffers until - * it is reconfigured. {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return - * the processor's output format. + * Configures the processor to process input buffers with the specified format. After calling this + * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the + * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the + * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a + * result of the call. If it's active, {@link #getOutputChannelCount()} and + * {@link #getOutputEncoding()} return the processor's output format. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. * @param encoding The encoding of input audio. - * @return Whether the processor must be flushed. + * @return {@code true} if the processor must be flushed or the value returned by + * {@link #isActive()} has changed as a result of the call. * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. */ boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java new file mode 100644 index 0000000000..8c23198925 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingBufferProcessor.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2017 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.C.Encoding; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Buffer processor that applies a mapping from input channels onto specified output channels. This + * can be used to reorder, duplicate or discard channels. + */ +/* package */ final class ChannelMappingBufferProcessor implements BufferProcessor { + + private int channelCount; + private int sampleRateHz; + private int[] pendingOutputChannels; + + private boolean active; + private int[] outputChannels; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new processor that applies a channel mapping. + */ + public ChannelMappingBufferProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + /** + * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} + * to start using the new channel map. + * + * @see AudioTrack#configure(String, int, int, int, int, int[]) + */ + public void setChannelMap(int[] outputChannels) { + pendingOutputChannels = outputChannels; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) + throws UnhandledFormatException { + boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); + outputChannels = pendingOutputChannels; + if (outputChannels == null) { + active = false; + return outputChannelsChanged; + } + if (encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz + && this.channelCount == channelCount) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + + active = channelCount != outputChannels.length; + for (int i = 0; i < outputChannels.length; i++) { + int channelIndex = outputChannels[i]; + if (channelIndex >= channelCount) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + active |= (channelIndex != i); + } + return true; + } + + @Override + public boolean isActive() { + return active; + } + + @Override + public int getOutputChannelCount() { + return outputChannels == null ? channelCount : outputChannels.length; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int frameCount = (limit - position) / (2 * channelCount); + int outputSize = frameCount * outputChannels.length * 2; + if (buffer.capacity() < outputSize) { + buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + while (position < limit) { + for (int channelIndex : outputChannels) { + buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); + } + position += channelCount * 2; + } + inputBuffer.position(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 release() { + flush(); + buffer = EMPTY_BUFFER; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 7ab9d9133a..76f7ac08bb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private final AudioTrack audioTrack; private boolean passthroughEnabled; + private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; + private int channelCount; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -188,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { + codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); @@ -219,6 +222,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // output 16-bit PCM. pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; + channelCount = newFormat.channelCount; } @Override @@ -230,8 +234,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + int[] channelMap; + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { + channelMap = new int[this.channelCount]; + for (int i = 0; i < this.channelCount; i++) { + channelMap[i] = i; + } + } else { + channelMap = null; + } + try { - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); } catch (AudioTrack.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -388,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns whether the decoder is known to output six audio channels when provided with input with + * fewer than six channels. + *

+ * See [Internal: b/35655036]. + */ + private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) { + // The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7. + return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte") + || Util.DEVICE.startsWith("heroqlte")); + } + private final class AudioTrackListener implements AudioTrack.Listener { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 9d06c4718f..370e54c58d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -25,6 +25,7 @@ import java.nio.ByteOrder; */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { + private int sampleRateHz; private int channelCount; @C.PcmEncoding private int encoding; @@ -36,6 +37,8 @@ import java.nio.ByteOrder; * Creates a new buffer processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. */ public ResamplingBufferProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; encoding = C.ENCODING_INVALID; buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; @@ -48,11 +51,12 @@ import java.nio.ByteOrder; && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - this.channelCount = channelCount; - if (this.encoding == encoding) { + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.encoding == encoding) { return false; } - + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; this.encoding = encoding; if (encoding == C.ENCODING_PCM_16BIT) { buffer = EMPTY_BUFFER;