diff --git a/extensions/flac/README.md b/extensions/flac/README.md new file mode 100644 index 0000000000..7d6abc9c8b --- /dev/null +++ b/extensions/flac/README.md @@ -0,0 +1,65 @@ +# ExoPlayer Flac Extension # + +## Description ## + +The Flac Extension is a [TrackRenderer][] implementation that helps you bundle +libFLAC (the Flac decoding library) into your app and use it along with +ExoPlayer to play Flac audio on Android devices. + +[TrackRenderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/TrackRenderer.html + +## Build Instructions ## + +* Checkout ExoPlayer along with Extensions: + +``` +git clone https://github.com/google/ExoPlayer.git +``` + +* Set the following environment variables: + +``` +cd "" +EXOPLAYER_ROOT="$(pwd)" +FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" +``` + +* Download the [Android NDK][] and set its location in an environment variable: + +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +``` +NDK_PATH="" +``` + +* Download and extract flac-1.3.1 as "${FLAC_EXT_PATH}/jni/flac" folder: + +``` +curl http://downloads.xiph.org/releases/flac/flac-1.3.1.tar.xz | tar xJ && \ +mv flac-1.3.1 flac +``` + +* Build the JNI native libraries from the command line: + +``` +cd "${FLAC_EXT_PATH}"/jni && \ +${NDK_PATH}/ndk-build APP_ABI=all -j4 +``` + +* In your project, you can add a dependency to the Flac Extension by using a + rule like this: + +``` +// in settings.gradle +include ':..:ExoPlayer:library' +include ':..:ExoPlayer:extension-flac' + +// in build.gradle +dependencies { + compile project(':..:ExoPlayer:library') + compile project(':..:ExoPlayer:extension-flac') +} +``` + +* Now, when you build your app, the Flac extension will be built and the native + libraries will be packaged along with the APK. diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle new file mode 100644 index 0000000000..e170263d74 --- /dev/null +++ b/extensions/flac/build.gradle @@ -0,0 +1,45 @@ +// 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 23 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + lintOptions { + abortOnError false + } + + sourceSets.main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } +} + +dependencies { + compile project(':library') +} + diff --git a/extensions/flac/src/main/.classpath b/extensions/flac/src/main/.classpath new file mode 100644 index 0000000000..503bb38b67 --- /dev/null +++ b/extensions/flac/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/flac/src/main/.project b/extensions/flac/src/main/.project new file mode 100644 index 0000000000..d96e765c9a --- /dev/null +++ b/extensions/flac/src/main/.project @@ -0,0 +1,97 @@ + + + ExoPlayerExt-Flac + + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + ?children? + ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\|| + + + ?name? + + + + org.eclipse.cdt.make.core.append_environment + true + + + org.eclipse.cdt.make.core.buildArguments + + + + org.eclipse.cdt.make.core.buildCommand + ndk-build + + + org.eclipse.cdt.make.core.cleanBuildTarget + clean + + + org.eclipse.cdt.make.core.contents + org.eclipse.cdt.make.core.activeConfigSettings + + + org.eclipse.cdt.make.core.enableAutoBuild + false + + + org.eclipse.cdt.make.core.enableCleanBuild + true + + + org.eclipse.cdt.make.core.enableFullBuild + true + + + org.eclipse.cdt.make.core.stopOnError + true + + + org.eclipse.cdt.make.core.useDefaultBuildCmd + true + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/extensions/flac/src/main/AndroidManifest.xml b/extensions/flac/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1a075c0948 --- /dev/null +++ b/extensions/flac/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java new file mode 100644 index 0000000000..1ffd1d397e --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java @@ -0,0 +1,100 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.util.extensions.SimpleDecoder; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Flac decoder. + */ +/* package */ final class FlacDecoder extends + SimpleDecoder { + + private final int maxOutputBufferSize; + private final FlacJni decoder; + /** + * Creates a Flac decoder. + * + * @param numInputBuffers The number of input buffers. + * @param numOutputBuffers The number of output buffers. + * @param initializationData Codec-specific initialization data. + * @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder. + */ + public FlacDecoder(int numInputBuffers, int numOutputBuffers, List initializationData) + throws FlacDecoderException { + super(new DecoderInputBuffer[numInputBuffers], new FlacOutputBuffer[numOutputBuffers]); + if (initializationData.size() != 1) { + throw new FlacDecoderException("Wrong number of initialization data"); + } + + decoder = new FlacJni(); + + ByteBuffer metadata = ByteBuffer.wrap(initializationData.get(0)); + decoder.setData(metadata); + FlacStreamInfo streamInfo = decoder.decodeMetadata(); + if (streamInfo == null) { + throw new FlacDecoderException("Metadata decoding failed"); + } + + setInitialInputBufferSize(streamInfo.maxFrameSize); + maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); + } + + @Override + public DecoderInputBuffer createInputBuffer() { + return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + @Override + public FlacOutputBuffer createOutputBuffer() { + return new FlacOutputBuffer(this); + } + + @Override + protected void releaseOutputBuffer(FlacOutputBuffer buffer) { + super.releaseOutputBuffer(buffer); + } + + @Override + public FlacDecoderException decode(DecoderInputBuffer inputBuffer, + FlacOutputBuffer outputBuffer) { + ByteBuffer data = inputBuffer.data; + outputBuffer.timestampUs = inputBuffer.timeUs; + data.limit(data.position()); + data.position(data.position() - inputBuffer.size); + outputBuffer.init(maxOutputBufferSize); + decoder.setData(data); + int result = decoder.decodeSample(outputBuffer.data); + if (result < 0) { + return new FlacDecoderException("Frame decoding failed"); + } + outputBuffer.data.position(0); + outputBuffer.data.limit(result); + return null; + } + + @Override + public void release() { + super.release(); + decoder.release(); + } + +} + diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java new file mode 100644 index 0000000000..8aeb564bea --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java @@ -0,0 +1,27 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +/** + * Thrown when an Flac decoder error occurs. + */ +public final class FlacDecoderException extends Exception { + + /* package */ FlacDecoderException(String message) { + super(message); + } + +} 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 new file mode 100644 index 0000000000..dc4ca2e508 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacExtractor.java @@ -0,0 +1,139 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.extractor.ExtractorOutput; +import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Facilitates the extraction of data from the FLAC container format. + */ +public final class FlacExtractor implements Extractor { + /** + * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the + * mandatory STREAMINFO. + */ + private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; + + private ExtractorOutput output; + private TrackOutput trackOutput; + + private FlacJni decoder; + + private boolean metadataParsed; + + private ParsableByteArray outputBuffer; + private ByteBuffer outputByteBuffer; + + @Override + public void init(ExtractorOutput output) { + this.output = output; + this.trackOutput = output.track(0); + output.endTracks(); + + try { + decoder = new FlacJni(); + } catch (FlacDecoderException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] header = new byte[FLAC_SIGNATURE.length]; + input.peekFully(header, 0, FLAC_SIGNATURE.length); + return Arrays.equals(header, FLAC_SIGNATURE); + } + + @Override + public int read(final ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + decoder.setData(input); + + if (!metadataParsed) { + final FlacStreamInfo streamInfo = decoder.decodeMetadata(); + if (streamInfo == null) { + throw new IOException("Metadata decoding failed"); + } + metadataParsed = true; + + output.seekMap(new SeekMap() { + final boolean isSeekable = decoder.getSeekPosition(0) != -1; + final long durationUs = streamInfo.durationUs(); + + @Override + public boolean isSeekable() { + return isSeekable; + } + + @Override + public long getPosition(long timeUs) { + return isSeekable ? decoder.getSeekPosition(timeUs) : 0; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + }); + + Format mediaFormat = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, + Format.NO_VALUE, streamInfo.bitRate(), + streamInfo.channels, streamInfo.sampleRate, null, null); + trackOutput.format(mediaFormat); + + outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); + outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); + } + + outputBuffer.reset(); + int size = decoder.decodeSample(outputByteBuffer); + if (size <= 0) { + return RESULT_END_OF_INPUT; + } + trackOutput.sampleData(outputBuffer, size); + + trackOutput + .sampleMetadata(decoder.getLastSampleTimestamp(), C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + + return decoder.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } + + @Override + public void seek() { + decoder.flush(); + } + + @Override + public void release() { + decoder.release(); + decoder = null; + } + +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java new file mode 100644 index 0000000000..1c26909c26 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacJni.java @@ -0,0 +1,192 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.ExtractorInput; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * JNI wrapper for the libflac Flac decoder. + */ +/* package */ final class FlacJni { + + /** + * Whether the underlying libflac library is available. + */ + public static final boolean IS_AVAILABLE; + static { + boolean isAvailable; + try { + System.loadLibrary("flacJNI"); + isAvailable = true; + } catch (UnsatisfiedLinkError exception) { + isAvailable = false; + } + IS_AVAILABLE = isAvailable; + } + + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has + + private final long nativeDecoderContext; + + private ByteBuffer byteBufferData; + + private ExtractorInput extractorInput; + private boolean endOfExtractorInput; + private byte[] tempBuffer; + + public FlacJni() throws FlacDecoderException { + nativeDecoderContext = flacInit(); + if (nativeDecoderContext == 0) { + throw new FlacDecoderException("Failed to initialize decoder"); + } + } + + /** + * Sets data to be parsed by libflac. + * @param byteBufferData Source {@link ByteBuffer} + */ + public void setData(ByteBuffer byteBufferData) { + this.byteBufferData = byteBufferData; + this.extractorInput = null; + this.tempBuffer = null; + } + + /** + * Sets data to be parsed by libflac. + * @param extractorInput Source {@link ExtractorInput} + */ + public void setData(ExtractorInput extractorInput) { + this.byteBufferData = null; + this.extractorInput = extractorInput; + if (tempBuffer == null) { + this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; + } + endOfExtractorInput = false; + } + + public boolean isEndOfData() { + if (byteBufferData != null) { + return byteBufferData.remaining() == 0; + } else if (extractorInput != null) { + return endOfExtractorInput; + } + return true; + } + + /** + * Reads up to {@code length} bytes from the data source. + *

+ * This method blocks until at least one byte of data can be read, the end of the input is + * detected or an exception is thrown. + *

+ * This method is called from the native code. + * + * @param target A target {@link ByteBuffer} into which data should be written. + * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns + * zero; it just means all the data read from the source. + */ + public int read(ByteBuffer target) throws IOException, InterruptedException { + int byteCount = target.remaining(); + if (byteBufferData != null) { + byteCount = Math.min(byteCount, byteBufferData.remaining()); + int originalLimit = byteBufferData.limit(); + byteBufferData.limit(byteBufferData.position() + byteCount); + + target.put(byteBufferData); + + byteBufferData.limit(originalLimit); + } else if (extractorInput != null) { + byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); + int read = readFromExtractorInput(0, byteCount); + if (read < 4) { + // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in + // the buffer of the input. Do another read to reduce the number of calls to this method + // from the native code. + read += readFromExtractorInput(read, byteCount - read); + } + byteCount = read; + target.put(tempBuffer, 0, byteCount); + } else { + return -1; + } + return byteCount; + } + + public FlacStreamInfo decodeMetadata() { + return flacDecodeMetadata(nativeDecoderContext); + } + + public int decodeSample(ByteBuffer output) { + return output.isDirect() + ? flacDecodeToBuffer(nativeDecoderContext, output) + : flacDecodeToArray(nativeDecoderContext, output.array()); + } + + public long getLastSampleTimestamp() { + return flacGetLastTimestamp(nativeDecoderContext); + } + + /** + * Maps a seek position in microseconds to a corresponding position (byte offset) in the flac + * stream. + * + * @param timeUs A seek position in microseconds. + * @return The corresponding position (byte offset) in the flac stream or -1 if the stream doesn't + * have a seek table. + */ + public long getSeekPosition(long timeUs) { + return flacGetSeekPosition(nativeDecoderContext, timeUs); + } + + public void flush() { + flacFlush(nativeDecoderContext); + } + + public void release() { + flacRelease(nativeDecoderContext); + } + + private int readFromExtractorInput(int offset, int length) + throws IOException, InterruptedException { + int read = extractorInput.read(tempBuffer, offset, length); + if (read == C.RESULT_END_OF_INPUT) { + endOfExtractorInput = true; + read = 0; + } + return read; + } + + private native long flacInit(); + + private native FlacStreamInfo flacDecodeMetadata(long context); + + private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer); + + private native int flacDecodeToArray(long context, byte[] outputArray); + + private native long flacGetLastTimestamp(long context); + + private native long flacGetSeekPosition(long context, long timeUs); + + private native void flacFlush(long context); + + private native void flacRelease(long context); + +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java new file mode 100644 index 0000000000..663beba73e --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java @@ -0,0 +1,56 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +import com.google.android.exoplayer.util.extensions.OutputBuffer; + +import java.nio.ByteBuffer; + +/** + * Buffer for {@link FlacDecoder} output. + */ +public final class FlacOutputBuffer extends OutputBuffer { + + private final FlacDecoder owner; + + public ByteBuffer data; + + /* package */ FlacOutputBuffer(FlacDecoder owner) { + this.owner = owner; + } + + /* package */ void init(int size) { + if (data == null || data.capacity() < size) { + data = ByteBuffer.allocateDirect(size); + } + data.position(0); + data.limit(size); + } + + @Override + public void clear() { + super.clear(); + if (data != null) { + data.clear(); + } + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java new file mode 100644 index 0000000000..8de58f7993 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacStreamInfo.java @@ -0,0 +1,54 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +/** + * Holder for flac stream info. + */ +/* package */ final class FlacStreamInfo { + public final int minBlockSize; + public final int maxBlockSize; + public final int minFrameSize; + public final int maxFrameSize; + public final int sampleRate; + public final int channels; + public final int bitsPerSample; + public final long totalSamples; + + public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, + int sampleRate, int channels, int bitsPerSample, long totalSamples) { + this.minBlockSize = minBlockSize; + this.maxBlockSize = maxBlockSize; + this.minFrameSize = minFrameSize; + this.maxFrameSize = maxFrameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitsPerSample = bitsPerSample; + this.totalSamples = totalSamples; + } + + public int maxDecodedFrameSize() { + return maxBlockSize * channels * 2; + } + + public int bitRate() { + return bitsPerSample * sampleRate; + } + + public long durationUs() { + return (totalSamples * 1000000L) / sampleRate; + } +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java new file mode 100644 index 0000000000..e007c40596 --- /dev/null +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java @@ -0,0 +1,385 @@ +/* + * 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 com.google.android.exoplayer.ext.flac; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.MediaClock; +import com.google.android.exoplayer.SampleSourceTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.TrackStream; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.util.MimeTypes; + +import android.os.Handler; + +import java.util.List; + +/** + * Decodes and renders audio using the native Flac decoder. + */ +public final class LibflacAudioTrackRenderer extends SampleSourceTrackRenderer + implements MediaClock { + + /** + * Interface definition for a callback to be notified of {@link LibflacAudioTrackRenderer} events. + */ + public interface EventListener { + + /** + * Invoked when the {@link AudioTrack} fails to initialize. + * + * @param e The corresponding exception. + */ + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + + /** + * Invoked when an {@link AudioTrack} write fails. + * + * @param e The corresponding exception. + */ + void onAudioTrackWriteError(AudioTrack.WriteException e); + + /** + * Invoked when decoding fails. + * + * @param e The corresponding exception. + */ + void onDecoderError(FlacDecoderException e); + + } + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be a {@link Float} with 0 being silence and 1 being unity gain. + */ + public static final int MSG_SET_VOLUME = 1; + + private static final int NUM_BUFFERS = 16; + + public final CodecCounters codecCounters = new CodecCounters(); + + private final Handler eventHandler; + private final EventListener eventListener; + private final FormatHolder formatHolder; + + private Format format; + private FlacDecoder decoder; + private DecoderInputBuffer inputBuffer; + private FlacOutputBuffer outputBuffer; + + private long currentPositionUs; + private boolean allowPositionDiscontinuity; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private boolean sourceIsReady; + + private final AudioTrack audioTrack; + private int audioSessionId; + + public LibflacAudioTrackRenderer() { + this(null, null); + } + + /** + * @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. + */ + public LibflacAudioTrackRenderer(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + this.audioTrack = new AudioTrack(); + formatHolder = new FormatHolder(); + } + + /** + * Returns whether the underlying libflac library is available. + */ + public static boolean isLibflacAvailable() { + return FlacJni.IS_AVAILABLE; + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + @Override + protected int supportsFormat(Format format) { + return MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType) + ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + protected void render(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + this.sourceIsReady = sourceIsReady; + + // Try and read a format if we don't have one already. + if (format == null && !readFormat()) { + // We can't make progress without one. + return; + } + + // If we don't have a decoder yet, we need to instantiate one. + if (decoder == null) { + // For flac, the format can contain only one entry in initializationData which is the flac + // file header. + List initializationData = format.initializationData; + if (initializationData.size() < 1) { + throw ExoPlaybackException.createForRenderer( + new IllegalStateException("Missing initialization data"), getIndex()); + } + try { + decoder = new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, initializationData); + } catch (FlacDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + decoder.start(); + codecCounters.codecInitCount++; + } + + // Rendering loop. + try { + renderBuffer(); + while (feedInputBuffer()) {} + } catch (AudioTrack.InitializationException e) { + notifyAudioTrackInitializationError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (AudioTrack.WriteException e) { + notifyAudioTrackWriteError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (FlacDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + codecCounters.ensureUpdated(); + } + + private void renderBuffer() throws FlacDecoderException, AudioTrack.InitializationException, + AudioTrack.WriteException { + if (outputStreamEnded) { + return; + } + + if (outputBuffer == null) { + outputBuffer = decoder.dequeueOutputBuffer(); + if (outputBuffer == null) { + return; + } + } + + if (outputBuffer.isEndOfStream()) { + outputStreamEnded = true; + audioTrack.handleEndOfStream(); + outputBuffer.release(); + outputBuffer = null; + return; + } + + if (!audioTrack.isInitialized()) { + if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { + audioTrack.initialize(audioSessionId); + } else { + audioSessionId = audioTrack.initialize(); + } + if (getState() == TrackRenderer.STATE_STARTED) { + audioTrack.play(); + } + } + + int handleBufferResult; + handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.data.position(), + outputBuffer.data.remaining(), outputBuffer.timestampUs); + + // If we are out of sync, allow currentPositionUs to jump backwards. + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + allowPositionDiscontinuity = true; + } + + // Release the buffer if it was consumed. + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + codecCounters.renderedOutputBufferCount++; + outputBuffer.release(); + outputBuffer = null; + } + } + + private boolean feedInputBuffer() throws FlacDecoderException { + if (inputStreamEnded) { + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + int result = readSource(formatHolder, inputBuffer); + if (result == TrackStream.NOTHING_READ) { + return false; + } + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + } + + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return true; + } + + private void flushDecoder() { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + decoder.flush(); + } + + @Override + protected boolean isEnded() { + return outputStreamEnded && !audioTrack.hasPendingData(); + } + + @Override + protected boolean isReady() { + return audioTrack.hasPendingData() + || (format != null && (sourceIsReady || outputBuffer != null)); + } + + @Override + public long getPositionUs() { + long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + return currentPositionUs; + } + + @Override + protected void reset(long positionUs) { + audioTrack.reset(); + currentPositionUs = positionUs; + allowPositionDiscontinuity = true; + inputStreamEnded = false; + outputStreamEnded = false; + sourceIsReady = false; + if (decoder != null) { + flushDecoder(); + } + } + + @Override + protected void onStarted() { + audioTrack.play(); + } + + @Override + protected void onStopped() { + audioTrack.pause(); + } + + @Override + protected void onDisabled() { + inputBuffer = null; + outputBuffer = null; + format = null; + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + try { + if (decoder != null) { + decoder.release(); + decoder = null; + codecCounters.codecReleaseCount++; + } + audioTrack.release(); + } finally { + super.onDisabled(); + } + } + + private boolean readFormat() { + int result = readSource(formatHolder, null); + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + audioTrack.configure(format.getFrameworkMediaFormatV16(), false); + return true; + } + return false; + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_VOLUME) { + audioTrack.setVolume((Float) message); + } else { + super.handleMessage(messageType, message); + } + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackInitializationError(e); + } + }); + } + } + + private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackWriteError(e); + } + }); + } + } + + private void notifyDecoderError(final FlacDecoderException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderError(e); + } + }); + } + } + +} diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk new file mode 100644 index 0000000000..e009333633 --- /dev/null +++ b/extensions/flac/src/main/jni/Android.mk @@ -0,0 +1,38 @@ +# +# 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. +# + +WORKING_DIR := $(call my-dir) + +# build libflacJNI.so +include $(CLEAR_VARS) +include $(WORKING_DIR)/flac_sources.mk + +LOCAL_PATH := $(WORKING_DIR) +LOCAL_MODULE := libflacJNI +LOCAL_ARM_MODE := arm +LOCAL_CPP_EXTENSION := .cc + +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH)/flac/include \ + $(LOCAL_PATH)/flac/src/libFLAC/include +LOCAL_SRC_FILES := $(FLAC_SOURCES) + +LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM +LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC +LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions + +LOCAL_LDLIBS := -llog -lz -lm +include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/flac/src/main/jni/Application.mk b/extensions/flac/src/main/jni/Application.mk new file mode 100644 index 0000000000..59bf5f8f87 --- /dev/null +++ b/extensions/flac/src/main/jni/Application.mk @@ -0,0 +1,20 @@ +# +# 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. +# + +APP_OPTIM := release +APP_STL := gnustl_static +APP_CPPFLAGS := -frtti +APP_PLATFORM := android-9 diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc new file mode 100644 index 0000000000..7fe1887971 --- /dev/null +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -0,0 +1,141 @@ +/* + * 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. + */ + +#include + +#include + +#include + +#include "include/flac_parser.h" + +#define LOG_TAG "FlacJniJNI" +#define ALOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#define ALOGV(...) \ + ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) + +#define FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_flac_FlacJni_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_flac_FlacJni_##NAME( \ + JNIEnv *env, jobject thiz, ##__VA_ARGS__) + +class JavaDataSource : public DataSource { + public: + void setFlacJni(JNIEnv *env, jobject flacJni) { + this->env = env; + this->flacJni = flacJni; + if (mid == NULL) { + jclass cls = env->GetObjectClass(flacJni); + mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I"); + env->DeleteLocalRef(cls); + } + } + + ssize_t readAt(off64_t offset, void *const data, size_t size) { + jobject byteBuffer = env->NewDirectByteBuffer(data, size); + int result = env->CallIntMethod(flacJni, mid, byteBuffer); + if (env->ExceptionOccurred()) { + result = -1; + } + env->DeleteLocalRef(byteBuffer); + return result; + } + + private: + JNIEnv *env; + jobject flacJni; + jmethodID mid; +}; + +struct Context { + JavaDataSource *source; + FLACParser *parser; +}; + +FUNC(jlong, flacInit) { + Context *context = new Context; + context->source = new JavaDataSource(); + context->parser = new FLACParser(context->source); + return reinterpret_cast(context); +} + +FUNC(jobject, flacDecodeMetadata, jlong jContext) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacJni(env, thiz); + if (!context->parser->init()) { + return NULL; + } + + const FLAC__StreamMetadata_StreamInfo &streamInfo = + context->parser->getStreamInfo(); + + jclass cls = env->FindClass( + "com/google/android/exoplayer/ext/flac/" + "FlacStreamInfo"); + jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V"); + + return env->NewObject(cls, constructor, streamInfo.min_blocksize, + streamInfo.max_blocksize, streamInfo.min_framesize, + streamInfo.max_framesize, streamInfo.sample_rate, + streamInfo.channels, streamInfo.bits_per_sample, + streamInfo.total_samples); +} + +FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacJni(env, thiz); + void *outputBuffer = env->GetDirectBufferAddress(jOutputBuffer); + jint outputSize = env->GetDirectBufferCapacity(jOutputBuffer); + return context->parser->readBuffer(outputBuffer, outputSize); +} + +FUNC(jint, flacDecodeToArray, jlong jContext, jbyteArray jOutputArray) { + Context *context = reinterpret_cast(jContext); + context->source->setFlacJni(env, thiz); + jbyte *outputBuffer = env->GetByteArrayElements(jOutputArray, NULL); + jint outputSize = env->GetArrayLength(jOutputArray); + int count = context->parser->readBuffer(outputBuffer, outputSize); + env->ReleaseByteArrayElements(jOutputArray, outputBuffer, 0); + return count; +} + +FUNC(jlong, flacGetLastTimestamp, jlong jContext) { + Context *context = reinterpret_cast(jContext); + return context->parser->getLastTimestamp(); +} + +FUNC(jlong, flacGetSeekPosition, jlong jContext, jlong timeUs) { + Context *context = reinterpret_cast(jContext); + return context->parser->getSeekPosition(timeUs); +} + +FUNC(void, flacFlush, jlong jContext) { + Context *context = reinterpret_cast(jContext); + context->parser->flush(); +} + +FUNC(void, flacRelease, jlong jContext) { + Context *context = reinterpret_cast(jContext); + delete context->parser; + delete context->source; + delete context; +} diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc new file mode 100644 index 0000000000..329288169a --- /dev/null +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -0,0 +1,458 @@ +/* + * 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. + */ + +#include "include/flac_parser.h" + +#include + +#include + +#include +#include + +#define LOG_TAG "FLACParser" +#define ALOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#define ALOGV(...) \ + ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) + +#define LOG_ALWAYS_FATAL(...) \ + (__android_log_assert(NULL, LOG_TAG, ##__VA_ARGS__)) + +#define LITERAL_TO_STRING_INTERNAL(x) #x +#define LITERAL_TO_STRING(x) LITERAL_TO_STRING_INTERNAL(x) + +#define TRESPASS() \ + LOG_ALWAYS_FATAL(__FILE__ \ + ":" LITERAL_TO_STRING(__LINE__) " Should not be here."); +#define CHECK(x) \ + if (!(x)) ALOGE("Check failed: %s ", #x) + +// The FLAC parser calls our C++ static callbacks using C calling conventions, +// inside FLAC__stream_decoder_process_until_end_of_metadata +// and FLAC__stream_decoder_process_single. +// We immediately then call our corresponding C++ instance methods +// with the same parameter list, but discard redundant information. + +FLAC__StreamDecoderReadStatus FLACParser::read_callback( + const FLAC__StreamDecoder * /* decoder */, FLAC__byte buffer[], + size_t *bytes, void *client_data) { + return reinterpret_cast(client_data) + ->readCallback(buffer, bytes); +} + +FLAC__StreamDecoderSeekStatus FLACParser::seek_callback( + const FLAC__StreamDecoder * /* decoder */, + FLAC__uint64 absolute_byte_offset, void *client_data) { + return reinterpret_cast(client_data) + ->seekCallback(absolute_byte_offset); +} + +FLAC__StreamDecoderTellStatus FLACParser::tell_callback( + const FLAC__StreamDecoder * /* decoder */, + FLAC__uint64 *absolute_byte_offset, void *client_data) { + return reinterpret_cast(client_data) + ->tellCallback(absolute_byte_offset); +} + +FLAC__StreamDecoderLengthStatus FLACParser::length_callback( + const FLAC__StreamDecoder * /* decoder */, FLAC__uint64 *stream_length, + void *client_data) { + return reinterpret_cast(client_data) + ->lengthCallback(stream_length); +} + +FLAC__bool FLACParser::eof_callback(const FLAC__StreamDecoder * /* decoder */, + void *client_data) { + return reinterpret_cast(client_data)->eofCallback(); +} + +FLAC__StreamDecoderWriteStatus FLACParser::write_callback( + const FLAC__StreamDecoder * /* decoder */, const FLAC__Frame *frame, + const FLAC__int32 *const buffer[], void *client_data) { + return reinterpret_cast(client_data) + ->writeCallback(frame, buffer); +} + +void FLACParser::metadata_callback(const FLAC__StreamDecoder * /* decoder */, + const FLAC__StreamMetadata *metadata, + void *client_data) { + reinterpret_cast(client_data)->metadataCallback(metadata); +} + +void FLACParser::error_callback(const FLAC__StreamDecoder * /* decoder */, + FLAC__StreamDecoderErrorStatus status, + void *client_data) { + reinterpret_cast(client_data)->errorCallback(status); +} + +// These are the corresponding callbacks with C++ calling conventions + +FLAC__StreamDecoderReadStatus FLACParser::readCallback(FLAC__byte buffer[], + size_t *bytes) { + size_t requested = *bytes; + ssize_t actual = mDataSource->readAt(mCurrentPos, buffer, requested); + if (0 > actual) { + *bytes = 0; + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } else if (0 == actual) { + *bytes = 0; + mEOF = true; + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + } else { + assert(actual <= requested); + *bytes = actual; + mCurrentPos += actual; + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } +} + +FLAC__StreamDecoderSeekStatus FLACParser::seekCallback( + FLAC__uint64 absolute_byte_offset) { + mCurrentPos = absolute_byte_offset; + mEOF = false; + return FLAC__STREAM_DECODER_SEEK_STATUS_OK; +} + +FLAC__StreamDecoderTellStatus FLACParser::tellCallback( + FLAC__uint64 *absolute_byte_offset) { + *absolute_byte_offset = mCurrentPos; + return FLAC__STREAM_DECODER_TELL_STATUS_OK; +} + +FLAC__StreamDecoderLengthStatus FLACParser::lengthCallback( + FLAC__uint64 *stream_length) { + return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; +} + +FLAC__bool FLACParser::eofCallback() { return mEOF; } + +FLAC__StreamDecoderWriteStatus FLACParser::writeCallback( + const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) { + if (mWriteRequested) { + mWriteRequested = false; + // FLAC parser doesn't free or realloc buffer until next frame or finish + mWriteHeader = frame->header; + mWriteBuffer = buffer; + mWriteCompleted = true; + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; + } else { + ALOGE("FLACParser::writeCallback unexpected"); + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } +} + +void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { + switch (metadata->type) { + case FLAC__METADATA_TYPE_STREAMINFO: + if (!mStreamInfoValid) { + mStreamInfo = metadata->data.stream_info; + mStreamInfoValid = true; + } else { + ALOGE("FLACParser::metadataCallback unexpected STREAMINFO"); + } + break; + case FLAC__METADATA_TYPE_SEEKTABLE: + mSeekTable = &metadata->data.seek_table; + break; + default: + ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); + break; + } +} + +void FLACParser::errorCallback(FLAC__StreamDecoderErrorStatus status) { + ALOGE("FLACParser::errorCallback status=%d", status); + mErrorStatus = status; +} + +// Copy samples from FLAC native 32-bit non-interleaved to 16-bit interleaved. +// These are candidates for optimization if needed. + +static void copyMono8(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i] << 8; + } +} + +static void copyStereo8(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i] << 8; + *dst++ = src[1][i] << 8; + } +} + +static void copyMultiCh8(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned nChannels) { + for (unsigned i = 0; i < nSamples; ++i) { + for (unsigned c = 0; c < nChannels; ++c) { + *dst++ = src[c][i] << 8; + } + } +} + +static void copyMono16(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i]; + } +} + +static void copyStereo16(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i]; + *dst++ = src[1][i]; + } +} + +static void copyMultiCh16(int16_t *dst, const int *const *src, + unsigned nSamples, unsigned nChannels) { + for (unsigned i = 0; i < nSamples; ++i) { + for (unsigned c = 0; c < nChannels; ++c) { + *dst++ = src[c][i]; + } + } +} + +// 24-bit versions should do dithering or noise-shaping, here or in AudioFlinger + +static void copyMono24(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i] >> 8; + } +} + +static void copyStereo24(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned /* nChannels */) { + for (unsigned i = 0; i < nSamples; ++i) { + *dst++ = src[0][i] >> 8; + *dst++ = src[1][i] >> 8; + } +} + +static void copyMultiCh24(int16_t *dst, const int *const *src, + unsigned nSamples, unsigned nChannels) { + for (unsigned i = 0; i < nSamples; ++i) { + for (unsigned c = 0; c < nChannels; ++c) { + *dst++ = src[c][i] >> 8; + } + } +} + +static void copyTrespass(int16_t * /* dst */, const int *const * /* src */, + unsigned /* nSamples */, unsigned /* nChannels */) { + TRESPASS(); +} + +// FLACParser + +FLACParser::FLACParser(DataSource *source) + : mDataSource(source), + mCopy(copyTrespass), + mDecoder(NULL), + mSeekTable(NULL), + firstFrameOffset(0LL), + mCurrentPos(0LL), + mEOF(false), + mStreamInfoValid(false), + mWriteRequested(false), + mWriteCompleted(false), + mWriteBuffer(NULL), + mErrorStatus((FLAC__StreamDecoderErrorStatus)-1) { + ALOGV("FLACParser::FLACParser"); + memset(&mStreamInfo, 0, sizeof(mStreamInfo)); + memset(&mWriteHeader, 0, sizeof(mWriteHeader)); +} + +FLACParser::~FLACParser() { + ALOGV("FLACParser::~FLACParser"); + if (mDecoder != NULL) { + FLAC__stream_decoder_delete(mDecoder); + mDecoder = NULL; + } +} + +bool FLACParser::init() { + // setup libFLAC parser + mDecoder = FLAC__stream_decoder_new(); + if (mDecoder == NULL) { + // The new should succeed, since probably all it does is a malloc + // that always succeeds in Android. But to avoid dependence on the + // libFLAC internals, we check and log here. + ALOGE("new failed"); + return false; + } + FLAC__stream_decoder_set_md5_checking(mDecoder, false); + FLAC__stream_decoder_set_metadata_ignore_all(mDecoder); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_STREAMINFO); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_SEEKTABLE); + FLAC__StreamDecoderInitStatus initStatus; + initStatus = FLAC__stream_decoder_init_stream( + mDecoder, read_callback, seek_callback, tell_callback, length_callback, + eof_callback, write_callback, metadata_callback, error_callback, + reinterpret_cast(this)); + if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + // A failure here probably indicates a programming error and so is + // unlikely to happen. But we check and log here similarly to above. + ALOGE("init_stream failed %d", initStatus); + return false; + } + // parse all metadata + if (!FLAC__stream_decoder_process_until_end_of_metadata(mDecoder)) { + ALOGE("end_of_metadata failed"); + return false; + } + // store first frame offset + FLAC__stream_decoder_get_decode_position(mDecoder, &firstFrameOffset); + + if (mStreamInfoValid) { + // check channel count + if (getChannels() == 0 || getChannels() > 8) { + ALOGE("unsupported channel count %u", getChannels()); + return false; + } + // check bit depth + switch (getBitsPerSample()) { + case 8: + case 16: + case 24: + break; + default: + ALOGE("unsupported bits per sample %u", getBitsPerSample()); + return false; + } + // check sample rate + switch (getSampleRate()) { + case 8000: + case 11025: + case 12000: + case 16000: + case 22050: + case 24000: + case 32000: + case 44100: + case 48000: + case 88200: + case 96000: + break; + default: + ALOGE("unsupported sample rate %u", getSampleRate()); + return false; + } + // configure the appropriate copy function, defaulting to trespass + static const struct { + unsigned mChannels; + unsigned mBitsPerSample; + void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned nChannels); + } table[] = { + {1, 8, copyMono8}, {2, 8, copyStereo8}, {8, 8, copyMultiCh8}, + {1, 16, copyMono16}, {2, 16, copyStereo16}, {8, 16, copyMultiCh16}, + {1, 24, copyMono24}, {2, 24, copyStereo24}, {8, 24, copyMultiCh24}, + }; + for (unsigned i = 0; i < sizeof(table) / sizeof(table[0]); ++i) { + if (table[i].mChannels >= getChannels() && + table[i].mBitsPerSample == getBitsPerSample()) { + mCopy = table[i].mCopy; + break; + } + } + } else { + ALOGE("missing STREAMINFO"); + return false; + } + return true; +} + +size_t FLACParser::readBuffer(void *output, size_t output_size) { + mWriteRequested = true; + mWriteCompleted = false; + + if (!FLAC__stream_decoder_process_single(mDecoder)) { + ALOGE("FLACParser::readBuffer process_single failed. Status: %s", + FLAC__stream_decoder_get_resolved_state_string(mDecoder)); + return -1; + } + if (!mWriteCompleted) { + if (FLAC__stream_decoder_get_state(mDecoder) != + FLAC__STREAM_DECODER_END_OF_STREAM) { + ALOGE("FLACParser::readBuffer write did not complete. Status: %s", + FLAC__stream_decoder_get_resolved_state_string(mDecoder)); + } + return -1; + } + + // verify that block header keeps the promises made by STREAMINFO + unsigned blocksize = mWriteHeader.blocksize; + if (blocksize == 0 || blocksize > getMaxBlockSize()) { + ALOGE("FLACParser::readBuffer write invalid blocksize %u", blocksize); + return -1; + } + if (mWriteHeader.sample_rate != getSampleRate() || + mWriteHeader.channels != getChannels() || + mWriteHeader.bits_per_sample != getBitsPerSample()) { + ALOGE( + "FLACParser::readBuffer write changed parameters mid-stream: %d/%d/%d " + "-> %d/%d/%d", + getSampleRate(), getChannels(), getBitsPerSample(), + mWriteHeader.sample_rate, mWriteHeader.channels, + mWriteHeader.bits_per_sample); + return -1; + } + + size_t bufferSize = blocksize * getChannels() * sizeof(int16_t); + if (bufferSize > output_size) { + ALOGE( + "FLACParser::readBuffer not enough space in output buffer " + "%zu < %zu", + output_size, bufferSize); + return -1; + } + + // copy PCM from FLAC write buffer to our media buffer, with interleaving. + (*mCopy)(reinterpret_cast(output), mWriteBuffer, blocksize, + getChannels()); + + // fill in buffer metadata + CHECK(mWriteHeader.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER); + + return bufferSize; +} + +int64_t FLACParser::getSeekPosition(int64_t timeUs) { + if (!mSeekTable) { + return -1; + } + + int64_t sample = (timeUs * getSampleRate()) / 1000000LL; + if (sample >= getTotalSamples()) { + sample = getTotalSamples(); + } + + FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; + for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { + if (points[i].sample_number <= sample) { + return firstFrameOffset + points[i].stream_offset; + } + } + return firstFrameOffset; +} diff --git a/extensions/flac/src/main/jni/flac_sources.mk b/extensions/flac/src/main/jni/flac_sources.mk new file mode 100644 index 0000000000..ade9daa359 --- /dev/null +++ b/extensions/flac/src/main/jni/flac_sources.mk @@ -0,0 +1,45 @@ +# +# 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. +# + +FLAC_SOURCES = \ + flac_jni.cc \ + flac_parser.cc \ + flac/src/libFLAC/bitmath.c \ + flac/src/libFLAC/bitreader.c \ + flac/src/libFLAC/bitwriter.c \ + flac/src/libFLAC/cpu.c \ + flac/src/libFLAC/crc.c \ + flac/src/libFLAC/fixed.c \ + flac/src/libFLAC/fixed_intrin_sse2.c \ + flac/src/libFLAC/fixed_intrin_ssse3.c \ + flac/src/libFLAC/float.c \ + flac/src/libFLAC/format.c \ + flac/src/libFLAC/lpc.c \ + flac/src/libFLAC/lpc_intrin_avx2.c \ + flac/src/libFLAC/lpc_intrin_sse2.c \ + flac/src/libFLAC/lpc_intrin_sse41.c \ + flac/src/libFLAC/lpc_intrin_sse.c \ + flac/src/libFLAC/md5.c \ + flac/src/libFLAC/memory.c \ + flac/src/libFLAC/metadata_iterators.c \ + flac/src/libFLAC/metadata_object.c \ + flac/src/libFLAC/stream_decoder.c \ + flac/src/libFLAC/stream_encoder.c \ + flac/src/libFLAC/stream_encoder_framing.c \ + flac/src/libFLAC/stream_encoder_intrin_avx2.c \ + flac/src/libFLAC/stream_encoder_intrin_sse2.c \ + flac/src/libFLAC/stream_encoder_intrin_ssse3.c \ + flac/src/libFLAC/window.c diff --git a/extensions/flac/src/main/jni/include/data_source.h b/extensions/flac/src/main/jni/include/data_source.h new file mode 100644 index 0000000000..175431dd7a --- /dev/null +++ b/extensions/flac/src/main/jni/include/data_source.h @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#ifndef INCLUDE_DATA_SOURCE_H_ +#define INCLUDE_DATA_SOURCE_H_ + +#include +#include + +class DataSource { + public: + // Returns the number of bytes read, or -1 on failure. It's not an error if + // this returns zero; it just means the given offset is equal to, or + // beyond, the end of the source. + virtual ssize_t readAt(off64_t offset, void* const data, size_t size) = 0; +}; + +#endif // INCLUDE_DATA_SOURCE_H_ diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h new file mode 100644 index 0000000000..22c17f7cff --- /dev/null +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -0,0 +1,133 @@ +/* + * 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. + */ + +#ifndef FLAC_PARSER_H_ +#define FLAC_PARSER_H_ + +#include + +// libFLAC parser +#include "FLAC/stream_decoder.h" + +#include "include/data_source.h" + +typedef int status_t; + +class FLACParser { + public: + FLACParser(DataSource *source); + ~FLACParser(); + + bool init(); + + // stream properties + unsigned getMaxBlockSize() const { return mStreamInfo.max_blocksize; } + unsigned getSampleRate() const { return mStreamInfo.sample_rate; } + unsigned getChannels() const { return mStreamInfo.channels; } + unsigned getBitsPerSample() const { return mStreamInfo.bits_per_sample; } + FLAC__uint64 getTotalSamples() const { return mStreamInfo.total_samples; } + + const FLAC__StreamMetadata_StreamInfo& getStreamInfo() const { + return mStreamInfo; + } + + int64_t getLastTimestamp() const { + return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); + } + + size_t readBuffer(void *output, size_t output_size); + + int64_t getSeekPosition(int64_t timeUs); + + void flush() { + if (mDecoder != NULL) { + FLAC__stream_decoder_flush(mDecoder); + } + } + + private: + DataSource *mDataSource; + + void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, + unsigned nChannels); + + // handle to underlying libFLAC parser + FLAC__StreamDecoder *mDecoder; + + // current position within the data source + off64_t mCurrentPos; + bool mEOF; + + // cached when the STREAMINFO metadata is parsed by libFLAC + FLAC__StreamMetadata_StreamInfo mStreamInfo; + bool mStreamInfoValid; + + const FLAC__StreamMetadata_SeekTable *mSeekTable; + uint64_t firstFrameOffset; + + // cached when a decoded PCM block is "written" by libFLAC parser + bool mWriteRequested; + bool mWriteCompleted; + FLAC__FrameHeader mWriteHeader; + const FLAC__int32 *const *mWriteBuffer; + + // most recent error reported by libFLAC parser + FLAC__StreamDecoderErrorStatus mErrorStatus; + + // no copy constructor or assignment + FLACParser(const FLACParser &); + FLACParser &operator=(const FLACParser &); + + // FLAC parser callbacks as C++ instance methods + FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[], + size_t *bytes); + FLAC__StreamDecoderSeekStatus seekCallback(FLAC__uint64 absolute_byte_offset); + FLAC__StreamDecoderTellStatus tellCallback( + FLAC__uint64 *absolute_byte_offset); + FLAC__StreamDecoderLengthStatus lengthCallback(FLAC__uint64 *stream_length); + FLAC__bool eofCallback(); + FLAC__StreamDecoderWriteStatus writeCallback( + const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); + void metadataCallback(const FLAC__StreamMetadata *metadata); + void errorCallback(FLAC__StreamDecoderErrorStatus status); + + // FLAC parser callbacks as C-callable functions + static FLAC__StreamDecoderReadStatus read_callback( + const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, + void *client_data); + static FLAC__StreamDecoderSeekStatus seek_callback( + const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, + void *client_data); + static FLAC__StreamDecoderTellStatus tell_callback( + const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, + void *client_data); + static FLAC__StreamDecoderLengthStatus length_callback( + const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, + void *client_data); + static FLAC__bool eof_callback(const FLAC__StreamDecoder *decoder, + void *client_data); + static FLAC__StreamDecoderWriteStatus write_callback( + const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, + const FLAC__int32 *const buffer[], void *client_data); + static void metadata_callback(const FLAC__StreamDecoder *decoder, + const FLAC__StreamMetadata *metadata, + void *client_data); + static void error_callback(const FLAC__StreamDecoder *decoder, + FLAC__StreamDecoderErrorStatus status, + void *client_data); +}; + +#endif // FLAC_PARSER_H_ diff --git a/extensions/flac/src/main/proguard.cfg b/extensions/flac/src/main/proguard.cfg new file mode 100644 index 0000000000..d951cd9496 --- /dev/null +++ b/extensions/flac/src/main/proguard.cfg @@ -0,0 +1,11 @@ +# Proguard rules specific to the Flac extension. + +# This prevents the names of native methods from being obfuscated. +-keepclasseswithmembernames class * { + native ; +} + +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer.ext.flac.FlacJni { + *; +} diff --git a/extensions/flac/src/main/project.properties b/extensions/flac/src/main/project.properties new file mode 100644 index 0000000000..b92a03b7ab --- /dev/null +++ b/extensions/flac/src/main/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-23 +android.library=true +android.library.reference.1=../../../../library/src/main diff --git a/settings.gradle b/settings.gradle index 63dd803377..468378033e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,3 +13,8 @@ // limitations under the License. include ':library' include ':demo' +include ':extension-okhttp' +include ':extension-flac' + +project(':extension-okhttp').projectDir = new File(settingsDir, 'extensions/okhttp') +project(':extension-flac').projectDir = new File(settingsDir, 'extensions/flac')