From b61e7cdbc42b5f8fb6f84eac26c2673f3cb0c279 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 May 2016 02:33:15 -0700 Subject: [PATCH] Support 8-bit and 24-bit PCM with re-sampling. This is a cleaned up version of the pull request below, which will be merged with this change then applied on top. https://github.com/google/ExoPlayer/pull/1490 Issue: #1246 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=121926682 --- .../exoplayer/ext/flac/FlacExtractor.java | 4 +- .../java/com/google/android/exoplayer/C.java | 20 ++ .../com/google/android/exoplayer/Format.java | 72 ++++--- .../exoplayer/FrameworkSampleSource.java | 5 +- .../MediaCodecAudioTrackRenderer.java | 12 +- .../android/exoplayer/audio/AudioTrack.java | 192 +++++++++++++----- .../extractor/mkv/MatroskaExtractor.java | 11 +- .../exoplayer/extractor/mp3/Mp3Extractor.java | 2 +- .../exoplayer/extractor/mp4/AtomParsers.java | 11 +- .../exoplayer/extractor/wav/WavExtractor.java | 2 +- .../exoplayer/extractor/wav/WavHeader.java | 18 +- .../extractor/wav/WavHeaderReader.java | 11 +- .../android/exoplayer/util/Ac3Util.java | 8 +- .../android/exoplayer/util/DtsUtil.java | 2 +- .../google/android/exoplayer/util/Util.java | 21 ++ .../extensions/AudioDecoderTrackRenderer.java | 3 +- 16 files changed, 284 insertions(+), 110 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java index b30c4aa4ae..76cc026722 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java @@ -105,8 +105,8 @@ public final class FlacExtractor implements Extractor { }); Format mediaFormat = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, - streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, null, - null, null); + streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, + C.ENCODING_PCM_16BIT, null, null, null); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 2de2cc4937..8fafb9c0f5 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -59,6 +59,26 @@ public final class C { @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; + /** + * @see AudioFormat#ENCODING_INVALID + */ + public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; + + /** + * @see AudioFormat#ENCODING_PCM_8BIT + */ + public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; + + /** + * @see AudioFormat#ENCODING_PCM_16BIT + */ + public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + + /** + * PCM encoding with 24 bits per sample. + */ + public static final int ENCODING_PCM_24BIT = 0x80000000; + /** * @see AudioFormat#ENCODING_AC3 */ diff --git a/library/src/main/java/com/google/android/exoplayer/Format.java b/library/src/main/java/com/google/android/exoplayer/Format.java index aecf065853..ca0c73501c 100644 --- a/library/src/main/java/com/google/android/exoplayer/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/Format.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; @@ -131,6 +132,12 @@ public final class Format { * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */ public final int sampleRate; + /** + * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} + * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT} and + * {@link C#ENCODING_PCM_24BIT}. Set to {@link #NO_VALUE} for other media types. + */ + public final int pcmEncoding; /** * The number of samples to trim from the start of the decoded audio stream. */ @@ -167,7 +174,7 @@ public final class Format { String sampleMimeType, int bitrate, int width, int height, float frameRate, List initializationData) { return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, width, height, - frameRate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, + frameRate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, initializationData, null, false); } @@ -182,8 +189,8 @@ public final class Format { int maxInputSize, int width, int height, float frameRate, List initializationData, int rotationDegrees, float pixelWidthHeightRatio, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, - rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, - OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); + rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, + null, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); } // Audio. @@ -192,23 +199,31 @@ public final class Format { String sampleMimeType, int bitrate, int channelCount, int sampleRate, List initializationData, String language) { return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, language, - OFFSET_SAMPLE_RELATIVE, initializationData, null, false); + NO_VALUE, NO_VALUE, NO_VALUE, channelCount, sampleRate, NO_VALUE, NO_VALUE, NO_VALUE, + language, OFFSET_SAMPLE_RELATIVE, initializationData, null, false); } public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate, int maxInputSize, int channelCount, int sampleRate, List initializationData, - String language, DrmInitData drmInitData) { + DrmInitData drmInitData, String language) { return createAudioSampleFormat(id, sampleMimeType, bitrate, maxInputSize, channelCount, - sampleRate, NO_VALUE, NO_VALUE, initializationData, language, drmInitData); + sampleRate, NO_VALUE, initializationData, drmInitData, language); } public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate, - int maxInputSize, int channelCount, int sampleRate, int encoderDelay, int encoderPadding, - List initializationData, String language, DrmInitData drmInitData) { + int maxInputSize, int channelCount, int sampleRate, int pcmEncoding, + List initializationData, DrmInitData drmInitData, String language) { + return createAudioSampleFormat(id, sampleMimeType, bitrate, maxInputSize, channelCount, + sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData, language); + } + + public static Format createAudioSampleFormat(String id, String sampleMimeType, int bitrate, + int maxInputSize, int channelCount, int sampleRate, int pcmEncoding, int encoderDelay, + int encoderPadding, List initializationData, DrmInitData drmInitData, + String language) { return new Format(id, null, sampleMimeType, bitrate, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, channelCount, sampleRate, encoderDelay, encoderPadding, language, - OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); + NO_VALUE, NO_VALUE, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); } // Text. @@ -216,7 +231,7 @@ public final class Format { public static Format createTextContainerFormat(String id, String containerMimeType, String sampleMimeType, int bitrate, String language) { return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, OFFSET_SAMPLE_RELATIVE, null, null, false); } @@ -229,7 +244,7 @@ public final class Format { public static Format createTextSampleFormat(String id, String sampleMimeType, int bitrate, String language, DrmInitData drmInitData, long subsampleOffsetUs) { return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, subsampleOffsetUs, null, drmInitData, false); } @@ -238,7 +253,7 @@ public final class Format { public static Format createImageSampleFormat(String id, String sampleMimeType, int bitrate, List initializationData, String language, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); } @@ -247,22 +262,22 @@ public final class Format { public static Format createContainerFormat(String id, String containerMimeType, String sampleMimeType, int bitrate) { return new Format(id, containerMimeType, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, null, false); } public static Format createSampleFormat(String id, String sampleMimeType, int bitrate, DrmInitData drmInitData) { return new Format(id, null, sampleMimeType, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, - null, drmInitData, false); + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, + OFFSET_SAMPLE_RELATIVE, null, drmInitData, false); } /* package */ Format(String id, String containerMimeType, String sampleMimeType, int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees, - float pixelWidthHeightRatio, int channelCount, int sampleRate, int encoderDelay, - int encoderPadding, String language, long subsampleOffsetUs, List initializationData, - DrmInitData drmInitData, boolean requiresSecureDecryption) { + float pixelWidthHeightRatio, int channelCount, int sampleRate, int pcmEncoding, + int encoderDelay, int encoderPadding, String language, long subsampleOffsetUs, + List initializationData, DrmInitData drmInitData, boolean requiresSecureDecryption) { this.id = id; this.containerMimeType = containerMimeType; this.sampleMimeType = sampleMimeType; @@ -275,6 +290,7 @@ public final class Format { this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.channelCount = channelCount; this.sampleRate = sampleRate; + this.pcmEncoding = pcmEncoding; this.encoderDelay = encoderDelay; this.encoderPadding = encoderPadding; this.language = language; @@ -288,14 +304,14 @@ public final class Format { public Format copyWithMaxInputSize(int maxInputSize) { return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, - encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, + pcmEncoding, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, drmInitData, requiresSecureDecryption); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, - encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, + pcmEncoding, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, drmInitData, requiresSecureDecryption); } @@ -303,21 +319,21 @@ public final class Format { String language) { return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, - encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, + pcmEncoding, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, drmInitData, requiresSecureDecryption); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, - encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, + pcmEncoding, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, drmInitData, requiresSecureDecryption); } public Format copyWithDrmInitData(DrmInitData drmInitData) { return new Format(id, containerMimeType, sampleMimeType, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, - encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, + pcmEncoding, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, drmInitData, requiresSecureDecryption); } @@ -379,8 +395,6 @@ public final class Format { result = 31 * result + height; result = 31 * result + channelCount; result = 31 * result + sampleRate; - result = 31 * result + encoderDelay; - result = 31 * result + encoderPadding; result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode()); hashCode = result; @@ -403,8 +417,8 @@ public final class Format { || rotationDegrees != other.rotationDegrees || pixelWidthHeightRatio != other.pixelWidthHeightRatio || channelCount != other.channelCount || sampleRate != other.sampleRate - || encoderDelay != other.encoderDelay || encoderPadding != other.encoderPadding - || subsampleOffsetUs != other.subsampleOffsetUs + || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay + || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs || !Util.areEqual(id, other.id) || !Util.areEqual(language, other.language) || !Util.areEqual(containerMimeType, other.containerMimeType) || !Util.areEqual(sampleMimeType, other.sampleMimeType) diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 515373ab0d..c0a6fef2b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -333,10 +333,11 @@ public final class FrameworkSampleSource implements SampleSource { initializationData.add(data); buffer.flip(); } + int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; Format format = new Format(Integer.toString(index), null, mimeType, Format.NO_VALUE, maxInputSize, width, height, frameRate, rotationDegrees, Format.NO_VALUE, channelCount, - sampleRate, encoderDelay, encoderPadding, language, Format.OFFSET_SAMPLE_RELATIVE, - initializationData, drmInitData, false); + sampleRate, pcmEncoding, encoderDelay, encoderPadding, language, + Format.OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, false); format.setFrameworkMediaFormatV16(mediaFormat); return format; } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 6dbf528d9e..c9e4c4ddb3 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -70,6 +70,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem private boolean passthroughEnabled; private android.media.MediaFormat passthroughMediaFormat; + private int pcmEncoding; private int audioSessionId; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -234,6 +235,15 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem return this; } + @Override + protected void onInputFormatChanged(FormatHolder holder) throws ExoPlaybackException { + super.onInputFormatChanged(holder); + // 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(holder.format.sampleMimeType) + ? holder.format.pcmEncoding : C.ENCODING_PCM_16BIT; + } + @Override protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { boolean passthrough = passthroughMediaFormat != null; @@ -243,7 +253,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem android.media.MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(android.media.MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate); + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 7e6b931633..6ad0f0cfb5 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -199,7 +199,8 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; - private int encoding; + private int sourceEncoding; + private int targetEncoding; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -224,7 +225,10 @@ public final class AudioTrack { private byte[] temporaryBuffer; private int temporaryBufferOffset; - private ByteBuffer currentBuffer; + private ByteBuffer currentSourceBuffer; + + private ByteBuffer resampledBuffer; + private boolean useResampledBuffer; /** * Creates an audio track with default audio capabilities (no encoded audio passthrough support). @@ -336,9 +340,11 @@ public final class AudioTrack { * @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} and {@link C#ENCODING_PCM_24BIT}. */ - public void configure(String mimeType, int channelCount, int sampleRate) { - configure(mimeType, channelCount, sampleRate, 0); + public void configure(String mimeType, int channelCount, int sampleRate, int pcmEncoding) { + configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); } /** @@ -347,10 +353,12 @@ public final class AudioTrack { * @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} and {@link C#ENCODING_PCM_24BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. */ - public void configure(String mimeType, int channelCount, int sampleRate, + public void configure(String mimeType, int channelCount, int sampleRate, int pcmEncoding, int specifiedBufferSize) { int channelConfig; switch (channelCount) { @@ -381,37 +389,48 @@ public final class AudioTrack { default: throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - int encoding = passthrough ? getEncodingForMimeType(mimeType) : AudioFormat.ENCODING_PCM_16BIT; - if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig - && this.encoding == encoding) { - // We already have an audio track with the correct sample rate, encoding and channel config. + int sourceEncoding; + if (passthrough) { + sourceEncoding = getEncodingForMimeType(mimeType); + } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT + || pcmEncoding == C.ENCODING_PCM_24BIT) { + sourceEncoding = pcmEncoding; + } else { + throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); + } + + if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + && this.channelConfig == channelConfig) { + // We already have an audio track with the correct sample rate, channel config and encoding. return; } reset(); - this.encoding = encoding; + this.sourceEncoding = sourceEncoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - pcmFrameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels. + targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; + pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3) { + if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else { // encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD + } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); Assertions.checkState(minBufferSize != android.media.AudioTrack.ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; @@ -449,12 +468,12 @@ public final class AudioTrack { releasingConditionVariable.block(); if (sessionId == SESSION_ID_NOT_SET) { - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, encoding, - bufferSize, android.media.AudioTrack.MODE_STREAM); + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM); } else { // Re-attach to the same audio session. - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, encoding, - bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); } checkAudioTrackInitialized(); @@ -470,7 +489,7 @@ public final class AudioTrack { if (keepSessionIdAudioTrack == null) { int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. int channelConfig = AudioFormat.CHANNEL_OUT_MONO; - int encoding = AudioFormat.ENCODING_PCM_16BIT; + int encoding = C.ENCODING_PCM_16BIT; int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC, sessionId); @@ -551,15 +570,9 @@ public final class AudioTrack { * @throws WriteException If an error occurs writing the audio data. */ public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewBuffer = currentBuffer == null; - Assertions.checkState(isNewBuffer || currentBuffer == buffer); - currentBuffer = buffer; - - int bytesRemaining = buffer.remaining(); - if (bytesRemaining == 0) { - currentBuffer = null; - return RESULT_BUFFER_CONSUMED; - } + boolean isNewSourceBuffer = currentSourceBuffer == null; + Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); + currentSourceBuffer = buffer; if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its @@ -578,11 +591,26 @@ public final class AudioTrack { } int result = 0; - if (isNewBuffer) { + if (isNewSourceBuffer) { // We're seeing this buffer for the first time. + + if (!currentSourceBuffer.hasRemaining()) { + // The buffer is empty. + currentSourceBuffer = null; + return RESULT_BUFFER_CONSUMED; + } + + useResampledBuffer = targetEncoding != sourceEncoding; + if (useResampledBuffer) { + Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); + // Resample the buffer to get the data in the target encoding. + resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); + buffer = resampledBuffer; + } + if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(encoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); } if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); @@ -607,6 +635,7 @@ public final class AudioTrack { } if (Util.SDK_INT < 21) { // Copy {@code buffer} into {@code temporaryBuffer}. + int bytesRemaining = buffer.remaining(); if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { temporaryBuffer = new byte[bytesRemaining]; } @@ -617,6 +646,8 @@ public final class AudioTrack { } } + buffer = useResampledBuffer ? resampledBuffer : buffer; + int bytesRemaining = buffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. @@ -646,7 +677,7 @@ public final class AudioTrack { if (passthrough) { submittedEncodedFrames += framesPerEncodedSample; } - currentBuffer = null; + currentSourceBuffer = null; result |= RESULT_BUFFER_CONSUMED; } return result; @@ -661,12 +692,6 @@ public final class AudioTrack { } } - @TargetApi(21) - private static int writeNonBlockingV21( - android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { - return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); - } - /** * Returns whether the audio track has more data pending that will be played back. */ @@ -707,16 +732,6 @@ public final class AudioTrack { } } - @TargetApi(21) - private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { - audioTrack.setVolume(volume); - } - - @SuppressWarnings("deprecation") - private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { - audioTrack.setStereoVolume(volume, volume); - } - /** * Pauses playback. */ @@ -737,7 +752,7 @@ public final class AudioTrack { submittedPcmBytes = 0; submittedEncodedFrames = 0; framesPerEncodedSample = 0; - currentBuffer = null; + currentSourceBuffer = null; startMediaTimeState = START_NOT_SET; latencyUs = 0; resetSyncParams(); @@ -937,7 +952,8 @@ public final class AudioTrack { * See [Internal: b/18899620, b/19187573, b/21145353]. */ private boolean needsPassthroughWorkarounds() { - return Util.SDK_INT < 23 && (encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3); + return Util.SDK_INT < 23 + && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); } /** @@ -952,6 +968,66 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } + /** + * Converts the provided buffer into 16-bit PCM. + * + * @param buffer The buffer containing the data to convert. + * @param sourceEncoding The data encoding. + * @param out A buffer into which the output should be written, if its capacity is sufficient. + * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the + * capacity was insufficient for the output. + */ + private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, int sourceEncoding, + ByteBuffer out) { + int offset = buffer.position(); + int limit = buffer.limit(); + int size = limit - offset; + + int resampledSize; + switch (sourceEncoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + default: + // Never happens. + throw new IllegalStateException(); + } + + ByteBuffer resampledBuffer = out; + if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { + resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + } + resampledBuffer.position(0); + resampledBuffer.limit(resampledSize); + + // Samples are little endian. + switch (sourceEncoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = offset; i < limit; i++) { + resampledBuffer.put((byte) 0); + resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = offset; i < limit; i += 3) { + resampledBuffer.put(buffer.get(i + 1)); + resampledBuffer.put(buffer.get(i + 2)); + } + break; + default: + // Never happens. + throw new IllegalStateException(); + } + + resampledBuffer.position(0); + return resampledBuffer; + } + private static int getEncodingForMimeType(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AC3: @@ -963,7 +1039,7 @@ public final class AudioTrack { case MimeTypes.AUDIO_DTS_HD: return C.ENCODING_DTS_HD; default: - return AudioFormat.ENCODING_INVALID; + return C.ENCODING_INVALID; } } @@ -979,6 +1055,22 @@ public final class AudioTrack { } } + @TargetApi(21) + private static int writeNonBlockingV21( + android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); + } + + @TargetApi(21) + private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + @SuppressWarnings("deprecation") + private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + /** * Wraps an {@link android.media.AudioTrack} to expose useful utility methods. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java index 47fe64a4ce..9415804a98 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mkv/MatroskaExtractor.java @@ -1207,6 +1207,7 @@ public final class MatroskaExtractor implements Extractor { public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { String mimeType; int maxInputSize = Format.NO_VALUE; + int pcmEncoding = Format.NO_VALUE; List initializationData = null; switch (codecId) { case CODEC_ID_VP8: @@ -1291,13 +1292,15 @@ public final class MatroskaExtractor implements Extractor { if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { throw new ParserException("Non-PCM MS/ACM is unsupported"); } - if (audioBitDepth != 16) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); } break; case CODEC_ID_PCM_INT_LIT: mimeType = MimeTypes.AUDIO_RAW; - if (audioBitDepth != 16) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); } break; @@ -1320,8 +1323,8 @@ public final class MatroskaExtractor implements Extractor { // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, - Format.NO_VALUE, maxInputSize, channelCount, sampleRate, initializationData, language, - drmInitData); + Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, + initializationData, drmInitData, language); } else if (MimeTypes.isVideo(mimeType)) { if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index ee605bc5cd..69c64356a5 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -127,7 +127,7 @@ public final class Mp3Extractor implements Extractor { extractorOutput.seekMap(seeker); trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, - synchronizedHeader.sampleRate, gaplessInfoHolder.encoderDelay, + synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding, null, null, null)); } return readSample(input); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 2369f43369..2fa8106eb3 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -939,8 +939,8 @@ import java.util.List; || atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) && childAtomType == Atom.TYPE_ddts) { out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, language, - drmInitData); + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, + language); return; } childAtomPosition += childAtomSize; @@ -951,10 +951,13 @@ import java.util.List; return; } + // TODO: Determine the correct PCM encoding. + int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; + out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), - language, drmInitData); + drmInitData, language); } /** Returns the position of the esds box within a parent, or -1 if no esds box is found */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavExtractor.java index cb7fe9aab0..5aeee0dc11 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavExtractor.java @@ -74,7 +74,7 @@ public final class WavExtractor implements Extractor, SeekMap { } Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(), - wavHeader.getSampleRateHz(), null, null, null); + wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, null); trackOutput.format(format); bytesPerFrame = wavHeader.getBytesPerFrame(); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeader.java b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeader.java index 24fb83ecd8..b8eac814a5 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeader.java @@ -30,22 +30,22 @@ import com.google.android.exoplayer.C; private final int blockAlignment; /** Bits per sample for the audio data. */ private final int bitsPerSample; + /** The pcm encoding */ + private final int encoding; + /** Offset to the start of sample data. */ private long dataStartPosition; /** Total size in bytes of the sample data. */ private long dataSize; - public WavHeader( - int numChannels, - int sampleRateHz, - int averageBytesPerSecond, - int blockAlignment, - int bitsPerSample) { + public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, + int bitsPerSample, int encoding) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; + this.encoding = encoding; } /** Returns the duration in microseconds of this WAV. */ @@ -110,4 +110,10 @@ import com.google.android.exoplayer.C; this.dataStartPosition = dataStartPosition; this.dataSize = dataSize; } + + /** Returns the PCM encoding. **/ + public int getEncoding() { + return encoding; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeaderReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeaderReader.java index c3c8599d4c..b61dcfffd2 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeaderReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/wav/WavHeaderReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.extractor.wav; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.util.Assertions; @@ -87,8 +88,10 @@ import java.io.IOException; throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " + blockAlignment); } - if (bitsPerSample != 16) { - Log.e(TAG, "Only 16-bit WAVs are supported; got: " + bitsPerSample); + + int encoding = Util.getPcmEncoding(bitsPerSample); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); return null; } @@ -100,8 +103,8 @@ import java.io.IOException; // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader( - numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample); + return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, + bitsPerSample, encoding); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java b/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java index a32ed5929c..cc53b69d90 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Ac3Util.java @@ -80,7 +80,7 @@ public final class Ac3Util { channelCount++; } return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, language, drmInitData); + Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, language); } /** @@ -107,7 +107,7 @@ public final class Ac3Util { channelCount++; } return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, null, language, drmInitData); + Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, language); } /** @@ -138,7 +138,7 @@ public final class Ac3Util { boolean lfeon = data.readBit(); return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, Format.NO_VALUE, Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), - SAMPLE_RATE_BY_FSCOD[fscod], null, language, drmInitData); + SAMPLE_RATE_BY_FSCOD[fscod], null, drmInitData, language); } /** @@ -166,7 +166,7 @@ public final class Ac3Util { boolean lfeon = data.readBit(); return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, Format.NO_VALUE, Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), sampleRate, null, - language, drmInitData); + drmInitData, language); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java b/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java index e1aede50ec..2bd36daeb7 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/util/DtsUtil.java @@ -73,7 +73,7 @@ public final class DtsUtil { frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_DTS, bitrate, Format.NO_VALUE, - channelCount, sampleRate, null, language, drmInitData); + channelCount, sampleRate, null, drmInitData, language); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 889d90b06c..4389e9430e 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -777,6 +777,27 @@ public final class Util { } } + /** + * Converts a sample bit depth to a corresponding PCM encoding constant. + * + * @param bitDepth The bit depth. Supported values are 8, 16 and 24. + * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT}, + * {@link C#ENCODING_PCM_16BIT} and {@link C#ENCODING_PCM_24BIT}. If the bit depth is + * unsupported then {@link C#ENCODING_INVALID} is returned. + */ + public static int getPcmEncoding(int bitDepth) { + switch (bitDepth) { + case 8: + return C.ENCODING_PCM_8BIT; + case 16: + return C.ENCODING_PCM_16BIT; + case 24: + return C.ENCODING_PCM_24BIT; + default: + return C.ENCODING_INVALID; + } + } + /** * Makes a best guess to infer the type from a file name. * diff --git a/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java index baec2090af..263c840374 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java @@ -292,7 +292,8 @@ public abstract class AudioDecoderTrackRenderer extends TrackRenderer implements int result = readSource(formatHolder, null); if (result == TrackStream.FORMAT_READ) { format = formatHolder.format; - audioTrack.configure(MimeTypes.AUDIO_RAW, format.channelCount, format.sampleRate); + audioTrack.configure(MimeTypes.AUDIO_RAW, format.channelCount, format.sampleRate, + C.ENCODING_PCM_16BIT); return true; } return false;