Add Java FLAC extractor

Seeking, live streams support and exposure of vorbis and ID3 data
are not part of this commit.

Issue: #6406
PiperOrigin-RevId: 281083332
This commit is contained in:
kimvde 2019-11-18 17:16:45 +00:00 committed by Oliver Woodman
parent ea342a671c
commit dcebf93ab4
26 changed files with 1989 additions and 80 deletions

View File

@ -1,5 +1,13 @@
# Release notes #
### 2.11.2 (TBD) ###
* Add Java FLAC extractor
([#6406](https://github.com/google/ExoPlayer/issues/6406)).
This extractor does not support seeking and live streams, and does not expose
vorbis, ID3 and picture data. If `DefaultExtractorsFactory` is used, this
extractor is only used if the FLAC extension is not loaded.
### 2.11.1 (2019-12-20) ###
* UI: Exclude `DefaultTimeBar` region from system gesture detection

View File

@ -411,6 +411,10 @@
"name": "Google Play (Ogg/Vorbis)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
},
{
"name": "Google Play (FLAC)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
},
{
"name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"

View File

@ -41,7 +41,7 @@ import java.nio.ByteBuffer;
super(
new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
streamMetadata.durationUs(),
streamMetadata.getDurationUs(),
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,

View File

@ -72,7 +72,7 @@ import java.util.List;
int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize();
}
@Override

View File

@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacMetadataReader;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
@ -42,7 +43,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ -72,9 +72,6 @@ public final class FlacExtractor implements Extractor {
*/
public static final int FLAG_DISABLE_ID3_METADATA = 1;
/** FLAC stream marker */
private static final byte[] FLAC_STREAM_MARKER = {'f', 'L', 'a', 'C'};
private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
private final boolean id3MetadataDisabled;
@ -120,10 +117,8 @@ public final class FlacExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input);
}
return peekFlacStreamMarker(input);
id3Metadata = peekId3Data(input);
return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
@Override
@ -230,7 +225,7 @@ public final class FlacExtractor implements Extractor {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
}
}
@ -251,18 +246,6 @@ public final class FlacExtractor implements Extractor {
return seekResult;
}
/**
* Peeks from the beginning of the input to see if {@link #FLAC_STREAM_MARKER} is present.
*
* @return Whether the input begins with {@link #FLAC_STREAM_MARKER}.
*/
private static boolean peekFlacStreamMarker(ExtractorInput input)
throws IOException, InterruptedException {
byte[] header = new byte[FLAC_STREAM_MARKER.length];
input.peekFully(header, /* offset= */ 0, FLAC_STREAM_MARKER.length);
return Arrays.equals(header, FLAC_STREAM_MARKER);
}
/**
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
* handle seeks.
@ -277,14 +260,14 @@ public final class FlacExtractor implements Extractor {
FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap;
if (haveSeekTable) {
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
binarySearchSeeker =
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
seekMap = binarySearchSeeker.getSeekMap();
} else {
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs());
}
output.seekMap(seekMap);
return binarySearchSeeker;
@ -297,8 +280,8 @@ public final class FlacExtractor implements Extractor {
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
streamMetadata.bitRate(),
streamMetadata.maxDecodedFrameSize(),
streamMetadata.getBitRate(),
streamMetadata.getMaxDecodedFrameSize(),
streamMetadata.channels,
streamMetadata.sampleRate,
getPcmEncoding(streamMetadata.bitsPerSample),

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
@ -55,12 +56,13 @@ import java.lang.reflect.Constructor;
*/
public final class DefaultExtractorsFactory implements ExtractorsFactory {
private static final Constructor<? extends Extractor> FLAC_EXTRACTOR_CONSTRUCTOR;
private static final Constructor<? extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR;
static {
Constructor<? extends Extractor> flacExtractorConstructor = null;
Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
try {
// LINT.IfChange
flacExtractorConstructor =
flacExtensionExtractorConstructor =
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
.asSubclass(Extractor.class)
.getConstructor();
@ -71,7 +73,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
// The FLAC extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating FLAC extension", e);
}
FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;
FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor;
}
private boolean constantBitrateSeekingEnabled;
@ -208,7 +210,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override
public synchronized Extractor[] createExtractors() {
Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14];
Extractor[] extractors = new Extractor[14];
extractors[0] = new MatroskaExtractor(matroskaFlags);
extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor(mp4Flags);
@ -237,13 +239,16 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0));
extractors[12] = new Ac4Extractor();
if (FLAC_EXTRACTOR_CONSTRUCTOR != null) {
// Prefer the FLAC extension extractor because it supports seeking.
if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) {
try {
extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance();
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
}
} else {
extractors[13] = new FlacExtractor();
}
return extractors;
}

View File

@ -0,0 +1,302 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacFrameReader;
import com.google.android.exoplayer2.util.FlacFrameReader.BlockSizeHolder;
import com.google.android.exoplayer2.util.FlacMetadataReader;
import com.google.android.exoplayer2.util.FlacMetadataReader.FirstFrameMetadata;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// TODO: implement seeking.
// TODO: expose vorbis and ID3 data.
// TODO: support live streams.
/**
* Extracts data from FLAC container format.
*
* <p>The format specification can be found at https://xiph.org/flac/format.html.
*/
public final class FlacExtractor implements Extractor {
/** Factory for {@link FlacExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
/** Parser state. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_READ_ID3_TAG,
STATE_READ_STREAM_MARKER,
STATE_READ_STREAM_INFO_BLOCK,
STATE_SKIP_OPTIONAL_METADATA_BLOCKS,
STATE_GET_FIRST_FRAME_METADATA,
STATE_READ_FRAMES
})
private @interface State {}
private static final int STATE_READ_ID3_TAG = 0;
private static final int STATE_READ_STREAM_MARKER = 1;
private static final int STATE_READ_STREAM_INFO_BLOCK = 2;
private static final int STATE_SKIP_OPTIONAL_METADATA_BLOCKS = 3;
private static final int STATE_GET_FIRST_FRAME_METADATA = 4;
private static final int STATE_READ_FRAMES = 5;
/** Arbitrary scratch length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
private static final int SCRATCH_LENGTH = 32 * 1024;
/** Value of an unknown block size. */
private static final int BLOCK_SIZE_UNKNOWN = -1;
private final byte[] streamMarkerAndInfoBlock;
private final ParsableByteArray scratch;
private final BlockSizeHolder blockSizeHolder;
@MonotonicNonNull private ExtractorOutput extractorOutput;
@MonotonicNonNull private TrackOutput trackOutput;
private @State int state;
@MonotonicNonNull private FlacStreamMetadata flacStreamMetadata;
private int minFrameSize;
private int frameStartMarker;
private int currentFrameBlockSizeSamples;
private int currentFrameBytesWritten;
private long totalSamplesWritten;
public FlacExtractor() {
streamMarkerAndInfoBlock =
new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE];
scratch = new ParsableByteArray(SCRATCH_LENGTH);
blockSizeHolder = new BlockSizeHolder();
}
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.peekId3Data(input);
return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
output.endTracks();
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
switch (state) {
case STATE_READ_ID3_TAG:
readId3Tag(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_STREAM_MARKER:
readStreamMarker(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_STREAM_INFO_BLOCK:
readStreamInfoBlock(input);
return Extractor.RESULT_CONTINUE;
case STATE_SKIP_OPTIONAL_METADATA_BLOCKS:
skipOptionalMetadataBlocks(input);
return Extractor.RESULT_CONTINUE;
case STATE_GET_FIRST_FRAME_METADATA:
getFirstFrameMetadata(input);
return Extractor.RESULT_CONTINUE;
case STATE_READ_FRAMES:
return readFrames(input);
default:
throw new IllegalStateException();
}
}
@Override
public void seek(long position, long timeUs) {
state = STATE_READ_ID3_TAG;
currentFrameBytesWritten = 0;
totalSamplesWritten = 0;
scratch.reset();
}
@Override
public void release() {
// Do nothing.
}
// Private methods.
private void readId3Tag(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.readId3Data(input);
state = STATE_READ_STREAM_MARKER;
}
private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException {
FlacMetadataReader.readStreamMarker(
input, streamMarkerAndInfoBlock, /* scratchWriteIndex= */ 0);
state = STATE_READ_STREAM_INFO_BLOCK;
}
private void readStreamInfoBlock(ExtractorInput input) throws IOException, InterruptedException {
flacStreamMetadata =
FlacMetadataReader.readStreamInfoBlock(
input,
/* scratchData= */ streamMarkerAndInfoBlock,
/* scratchWriteIndex= */ FlacConstants.STREAM_MARKER_SIZE);
minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE);
boolean isLastMetadataBlock =
(streamMarkerAndInfoBlock[FlacConstants.STREAM_MARKER_SIZE] >> 7 & 1) == 1;
castNonNull(trackOutput).format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock));
castNonNull(extractorOutput)
.seekMap(new SeekMap.Unseekable(flacStreamMetadata.getDurationUs()));
if (isLastMetadataBlock) {
state = STATE_GET_FIRST_FRAME_METADATA;
} else {
state = STATE_SKIP_OPTIONAL_METADATA_BLOCKS;
}
}
private void skipOptionalMetadataBlocks(ExtractorInput input)
throws IOException, InterruptedException {
FlacMetadataReader.skipMetadataBlocks(input);
state = STATE_GET_FIRST_FRAME_METADATA;
}
private void getFirstFrameMetadata(ExtractorInput input)
throws IOException, InterruptedException {
FirstFrameMetadata firstFrameMetadata = FlacMetadataReader.getFirstFrameMetadata(input);
frameStartMarker = firstFrameMetadata.frameStartMarker;
currentFrameBlockSizeSamples = firstFrameMetadata.blockSizeSamples;
state = STATE_READ_FRAMES;
}
// TODO: consider sending bytes within min frame size directly from the input to the sample queue
// to avoid unnecessary copies in scratch.
private int readFrames(ExtractorInput input) throws IOException, InterruptedException {
Assertions.checkNotNull(trackOutput);
Assertions.checkNotNull(flacStreamMetadata);
// Copy more bytes into the scratch.
int currentLimit = scratch.limit();
int bytesRead =
input.read(
scratch.data, /* offset= */ currentLimit, /* length= */ SCRATCH_LENGTH - currentLimit);
boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
if (!foundEndOfInput) {
scratch.setLimit(currentLimit + bytesRead);
} else if (scratch.bytesLeft() == 0) {
return C.RESULT_END_OF_INPUT;
}
// Search for a frame.
int positionBeforeFindingAFrame = scratch.getPosition();
// Skip frame search on the bytes within the minimum frame size.
if (currentFrameBytesWritten < minFrameSize) {
scratch.skipBytes(Math.min(minFrameSize, scratch.bytesLeft()));
}
int nextFrameBlockSizeSamples = findFrame(scratch, foundEndOfInput);
int numberOfFrameBytes = scratch.getPosition() - positionBeforeFindingAFrame;
scratch.setPosition(positionBeforeFindingAFrame);
trackOutput.sampleData(scratch, numberOfFrameBytes);
currentFrameBytesWritten += numberOfFrameBytes;
// Frame found.
if (nextFrameBlockSizeSamples != BLOCK_SIZE_UNKNOWN || foundEndOfInput) {
long timeUs = getTimeUs(totalSamplesWritten, flacStreamMetadata.sampleRate);
trackOutput.sampleMetadata(
timeUs,
C.BUFFER_FLAG_KEY_FRAME,
currentFrameBytesWritten,
/* offset= */ 0,
/* encryptionData= */ null);
totalSamplesWritten += currentFrameBlockSizeSamples;
currentFrameBytesWritten = 0;
currentFrameBlockSizeSamples = nextFrameBlockSizeSamples;
}
if (scratch.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) {
// The next frame header may not fit in the rest of the scratch, so put the trailing bytes at
// the start of the scratch, and reset the position and limit.
System.arraycopy(
scratch.data, scratch.getPosition(), scratch.data, /* destPos= */ 0, scratch.bytesLeft());
scratch.reset(scratch.bytesLeft());
}
return Extractor.RESULT_CONTINUE;
}
/**
* Searches for the start of a frame in {@code scratch}.
*
* <ul>
* <li>If the search is successful, the position is set to the start of the found frame.
* <li>Otherwise, the position is set to the first unsearched byte.
* </ul>
*
* @param scratch The array to be searched.
* @param foundEndOfInput If the end of input was met when filling in the {@code scratch}.
* @return The block size of the frame found, or {@code BLOCK_SIZE_UNKNOWN} if the search was not
* successful.
*/
private int findFrame(ParsableByteArray scratch, boolean foundEndOfInput) {
Assertions.checkNotNull(flacStreamMetadata);
int frameOffset = scratch.getPosition();
while (frameOffset <= scratch.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) {
scratch.setPosition(frameOffset);
if (FlacFrameReader.checkAndReadFrameHeader(
scratch, flacStreamMetadata, frameStartMarker, blockSizeHolder)) {
scratch.setPosition(frameOffset);
return blockSizeHolder.blockSizeSamples;
}
frameOffset++;
}
if (foundEndOfInput) {
// Reached the end of the file. Assume it's the end of the frame.
scratch.setPosition(scratch.limit());
} else {
scratch.setPosition(frameOffset);
}
return BLOCK_SIZE_UNKNOWN;
}
private long getTimeUs(long numSamples, int sampleRate) {
return numSamples * C.MICROS_PER_SECOND / sampleRate;
}
}

View File

@ -15,18 +15,14 @@
*/
package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* {@link StreamReader} to extract Flac data out of Ogg byte stream.
@ -72,24 +68,8 @@ import java.util.List;
byte[] data = packet.data;
if (streamMetadata == null) {
streamMetadata = new FlacStreamMetadata(data, 17);
int maxInputSize =
streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize;
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata);
setupData.format =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_FLAC,
/* codecs= */ null,
streamMetadata.bitRate(),
maxInputSize,
streamMetadata.channels,
streamMetadata.sampleRate,
initializationData,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
setupData.format = streamMetadata.getFormat(metadata);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet);
@ -220,7 +200,7 @@ import java.util.List;
@Override
public long getDurationUs() {
return streamMetadata.durationUs();
return streamMetadata.getDurationUs();
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
/** Defines constants used by the FLAC extractor. */
public final class FlacConstants {
/** Size of the FLAC stream marker in bytes. */
public static final int STREAM_MARKER_SIZE = 4;
/** Size of the header of a FLAC metadata block in bytes. */
public static final int METADATA_BLOCK_HEADER_SIZE = 4;
/** Size of the FLAC stream info block (header included) in bytes. */
public static final int STREAM_INFO_BLOCK_SIZE = 38;
/** Minimum size of a FLAC frame header in bytes. */
public static final int MIN_FRAME_HEADER_SIZE = 6;
/** Maximum size of a FLAC frame header in bytes. */
public static final int MAX_FRAME_HEADER_SIZE = 16;
private FlacConstants() {}
}

View File

@ -0,0 +1,257 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
/** Reads and peeks FLAC frame elements. */
public final class FlacFrameReader {
/** Holds a frame block size. */
public static final class BlockSizeHolder {
/** The block size in samples. */
public int blockSizeSamples;
}
/**
* Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame
* block size in {@code blockSizeHolder}.
*
* <p>If the header is valid, the position of {@code scratch} is moved to the byte following it.
* Otherwise, there is no guarantee on the position.
*
* @param scratch The array to read the data from, whose position must correspond to the frame
* header.
* @param flacStreamMetadata The stream metadata.
* @param frameStartMarker The frame start marker of the stream.
* @param blockSizeHolder The holder used to contain the block size.
* @return Whether the frame header is valid.
*/
public static boolean checkAndReadFrameHeader(
ParsableByteArray scratch,
FlacStreamMetadata flacStreamMetadata,
int frameStartMarker,
BlockSizeHolder blockSizeHolder) {
int frameStartPosition = scratch.getPosition();
long frameHeaderBytes = scratch.readUnsignedInt();
if (frameHeaderBytes >>> 16 != frameStartMarker) {
return false;
}
int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF);
int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF);
int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF);
int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7);
boolean reservedBit = (frameHeaderBytes & 1) == 1;
return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata)
&& checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata)
&& !reservedBit
&& checkAndReadUtf8Data(scratch)
&& checkAndReadBlockSizeSamples(scratch, flacStreamMetadata, blockSizeKey, blockSizeHolder)
&& checkAndReadSampleRate(scratch, flacStreamMetadata, sampleRateKey)
&& checkAndReadCrc(scratch, frameStartPosition);
}
/**
* Returns the block size of the given frame.
*
* <p>If no exception is thrown, the position of {@code scratch} is left unchanged. Otherwise,
* there is no guarantee on the position.
*
* @param scratch The array to get the data from, whose position must correspond to the start of a
* frame.
* @return The block size in samples, or -1 if the block size is invalid.
*/
public static int getFrameBlockSizeSamples(ParsableByteArray scratch) {
int blockSizeKey = (scratch.data[2] & 0xFF) >> 4;
if (blockSizeKey < 6 || blockSizeKey > 7) {
return readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey);
}
scratch.skipBytes(4);
scratch.readUtf8EncodedLong();
int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey);
scratch.setPosition(0);
return blockSizeSamples;
}
/**
* Reads the given block size.
*
* @param scratch The array to read the data from, whose position must correspond to the block
* size bits.
* @param blockSizeKey The key in the block size lookup table.
* @return The block size in samples.
*/
public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray scratch, int blockSizeKey) {
switch (blockSizeKey) {
case 1:
return 192;
case 2:
case 3:
case 4:
case 5:
return 576 << (blockSizeKey - 2);
case 6:
return scratch.readUnsignedByte() + 1;
case 7:
return scratch.readUnsignedShort() + 1;
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
return 256 << (blockSizeKey - 8);
default:
return -1;
}
}
/**
* Checks whether the given channel assignment is valid.
*
* @param channelAssignmentKey The channel assignment lookup key.
* @param flacStreamMetadata The stream metadata.
* @return Whether the channel assignment is valid.
*/
private static boolean checkChannelAssignment(
int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) {
if (channelAssignmentKey <= 7) {
return channelAssignmentKey == flacStreamMetadata.channels - 1;
} else if (channelAssignmentKey <= 10) {
return flacStreamMetadata.channels == 2;
} else {
return false;
}
}
/**
* Checks whether the given number of bits per sample is valid.
*
* @param bitsPerSampleKey The bits per sample lookup key.
* @param flacStreamMetadata The stream metadata.
* @return Whether the number of bits per sample is valid.
*/
private static boolean checkBitsPerSample(
int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) {
if (bitsPerSampleKey == 0) {
return true;
}
return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey;
}
/**
* Checks whether the given UTF-8 data is valid and, if so, reads it.
*
* <p>If the UTF-8 data is valid, the position of {@code scratch} is moved to the byte following
* it. Otherwise, there is no guarantee on the position.
*
* @param scratch The array to read the data from, whose position must correspond to the UTF-8
* data.
* @return Whether the UTF-8 data is valid.
*/
private static boolean checkAndReadUtf8Data(ParsableByteArray scratch) {
try {
scratch.readUtf8EncodedLong();
} catch (NumberFormatException e) {
return false;
}
return true;
}
/**
* Checks whether the given frame block size key and block size bits are valid and, if so, reads
* the block size bits and writes the block size in {@code blockSizeHolder}.
*
* <p>If the block size is valid, the position of {@code scratch} is moved to the byte following
* the block size bits. Otherwise, there is no guarantee on the position.
*
* @param scratch The array to read the data from, whose position must correspond to the block
* size bits.
* @param flacStreamMetadata The stream metadata.
* @param blockSizeKey The key in the block size lookup table.
* @param blockSizeHolder The holder used to contain the block size.
* @return Whether the block size is valid.
*/
private static boolean checkAndReadBlockSizeSamples(
ParsableByteArray scratch,
FlacStreamMetadata flacStreamMetadata,
int blockSizeKey,
BlockSizeHolder blockSizeHolder) {
int blockSizeSamples = readFrameBlockSizeSamplesFromKey(scratch, blockSizeKey);
if (blockSizeSamples == -1 || blockSizeSamples > flacStreamMetadata.maxBlockSizeSamples) {
return false;
}
blockSizeHolder.blockSizeSamples = blockSizeSamples;
return true;
}
/**
* Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the
* sample rate bits.
*
* <p>If the sample rate is valid, the position of {@code scratch} is moved to the byte following
* the sample rate bits. Otherwise, there is no guarantee on the position.
*
* @param scratch The array to read the data from, whose position must indicate the sample rate
* bits.
* @param flacStreamMetadata The stream metadata.
* @param sampleRateKey The key in the sample rate lookup table.
* @return Whether the sample rate is valid.
*/
private static boolean checkAndReadSampleRate(
ParsableByteArray scratch, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) {
int expectedSampleRate = flacStreamMetadata.sampleRate;
if (sampleRateKey == 0) {
return true;
} else if (sampleRateKey <= 11) {
return sampleRateKey == flacStreamMetadata.sampleRateLookupKey;
} else if (sampleRateKey == 12) {
return scratch.readUnsignedByte() * 1000 == expectedSampleRate;
} else if (sampleRateKey <= 14) {
int sampleRate = scratch.readUnsignedShort();
if (sampleRateKey == 14) {
sampleRate *= 10;
}
return sampleRate == expectedSampleRate;
} else {
return false;
}
}
/**
* Checks whether the given CRC is valid and, if so, reads it.
*
* <p>If the CRC is valid, the position of {@code scratch} is moved to the byte following it.
* Otherwise, there is no guarantee on the position.
*
* <p>The {@code scratch} array must contain the whole frame header.
*
* @param scratch The array to read the data from, whose position must indicate the CRC.
* @param frameStartPosition The frame start offset in {@code scratch}.
* @return Whether the CRC is valid.
*/
private static boolean checkAndReadCrc(ParsableByteArray scratch, int frameStartPosition) {
int crc = scratch.readUnsignedByte();
int frameEndPosition = scratch.getPosition();
int expectedCrc =
Util.crc8(scratch.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0);
return crc == expectedCrc;
}
private FlacFrameReader() {}
}

View File

@ -0,0 +1,208 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.Id3Peeker;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import java.io.IOException;
/** Reads and peeks FLAC stream metadata elements from an {@link ExtractorInput}. */
public final class FlacMetadataReader {
/** Holds the metadata extracted from the first frame. */
public static final class FirstFrameMetadata {
/** The frame start marker, which should correspond to the 2 first bytes of each frame. */
public final int frameStartMarker;
/** The block size in samples. */
public final int blockSizeSamples;
public FirstFrameMetadata(int frameStartMarker, int blockSizeSamples) {
this.frameStartMarker = frameStartMarker;
this.blockSizeSamples = blockSizeSamples;
}
}
private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC"
private static final int SYNC_CODE = 0x3FFE;
/**
* Peeks ID3 Data.
*
* @param input Input stream to peek the ID3 data from.
* @throws IOException If peeking from the input fails. In this case, there is no guarantee on the
* peek position.
* @throws InterruptedException If interrupted while peeking from input. In this case, there is no
* guarantee on the peek position.
*/
public static void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
new Id3Peeker().peekId3Data(input, Id3Decoder.NO_FRAMES_PREDICATE);
}
/**
* Peeks the FLAC stream marker.
*
* @param input Input stream to peek the stream marker from.
* @return Whether the data peeked is the FLAC stream marker.
* @throws IOException If peeking from the input fails. In this case, the peek position is left
* unchanged.
* @throws InterruptedException If interrupted while peeking from input. In this case, the peek
* position is left unchanged.
*/
public static boolean checkAndPeekStreamMarker(ExtractorInput input)
throws IOException, InterruptedException {
ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
input.peekFully(scratch.data, /* offset= */ 0, FlacConstants.STREAM_MARKER_SIZE);
return scratch.readUnsignedInt() == STREAM_MARKER;
}
/**
* Reads ID3 Data.
*
* <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
* position.
*
* @param input Input stream to read the ID3 data from.
* @throws IOException If reading from the input fails. In this case, the read position is left
* unchanged and there is no guarantee on the peek position.
* @throws InterruptedException If interrupted while reading from input. In this case, the read
* position is left unchanged and there is no guarantee on the peek position.
*/
public static void readId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
long startingPeekPosition = input.getPeekPosition();
new Id3Peeker().peekId3Data(input, Id3Decoder.NO_FRAMES_PREDICATE);
int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition);
input.skipFully(peekedId3Bytes);
}
/**
* Reads the FLAC stream marker.
*
* @param input Input stream to read the stream marker from.
* @param scratchData The array in which the data read should be copied. This array must have size
* at least {@code scratchWriteIndex} + {@link FlacConstants#STREAM_MARKER_SIZE}.
* @param scratchWriteIndex The index of {@code scratchData} from which to write.
* @throws ParserException If an error occurs parsing the stream marker. In this case, the
* position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes.
* @throws IOException If reading from the input fails. In this case, the position is left
* unchanged.
* @throws InterruptedException If interrupted while reading from input. In this case, the
* position is left unchanged.
*/
public static void readStreamMarker(
ExtractorInput input, byte[] scratchData, int scratchWriteIndex)
throws IOException, InterruptedException {
ParsableByteArray scratch = new ParsableByteArray(scratchData);
input.readFully(
scratch.data,
/* offset= */ scratchWriteIndex,
/* length= */ FlacConstants.STREAM_MARKER_SIZE);
scratch.setPosition(scratchWriteIndex);
if (scratch.readUnsignedInt() != STREAM_MARKER) {
throw new ParserException("Failed to read FLAC stream marker.");
}
}
/**
* Reads the stream info block.
*
* @param input Input stream to read the stream info block from.
* @param scratchData The array in which the data read should be copied. This array must have size
* at least {@code scratchWriteIndex} + {@link FlacConstants#STREAM_INFO_BLOCK_SIZE}.
* @param scratchWriteIndex The index of {@code scratchData} from which to write.
* @return A new {@link FlacStreamMetadata} read from {@code input}.
* @throws IOException If reading from the input fails. In this case, the position is left
* unchanged.
* @throws InterruptedException If interrupted while reading from input. In this case, the
* position is left unchanged.
*/
public static FlacStreamMetadata readStreamInfoBlock(
ExtractorInput input, byte[] scratchData, int scratchWriteIndex)
throws IOException, InterruptedException {
input.readFully(
scratchData,
/* offset= */ scratchWriteIndex,
/* length= */ FlacConstants.STREAM_INFO_BLOCK_SIZE);
return new FlacStreamMetadata(
scratchData, /* offset= */ scratchWriteIndex + FlacConstants.METADATA_BLOCK_HEADER_SIZE);
}
/**
* Skips the stream metadata blocks.
*
* <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
* position.
*
* @param input Input stream to read the metadata blocks from.
* @throws IOException If reading from the input fails. In this case, the read position will be at
* the start of a metadata block and there is no guarantee on the peek position.
* @throws InterruptedException If interrupted while reading from input. In this case, the read
* position will be at the start of a metadata block and there is no guarantee on the peek
* position.
*/
public static void skipMetadataBlocks(ExtractorInput input)
throws IOException, InterruptedException {
input.resetPeekPosition();
ParsableBitArray scratch = new ParsableBitArray(new byte[4]);
boolean lastMetadataBlock = false;
while (!lastMetadataBlock) {
input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 4);
scratch.setPosition(0);
lastMetadataBlock = scratch.readBit();
scratch.skipBits(7);
int length = scratch.readBits(24);
input.skipFully(4 + length);
}
}
/**
* Returns some metadata extracted from the first frame of a FLAC stream.
*
* <p>The read position of {@code input} is left unchanged and the peek position is aligned with
* the read position.
*
* @param input Input stream to get the metadata from (starting from the read position).
* @return A {@link FirstFrameMetadata} containing the frame start marker (which should be the
* same for all the frames in the stream) and the block size of the frame.
* @throws ParserException If an error occurs parsing the frame metadata.
* @throws IOException If peeking from the input fails.
* @throws InterruptedException If interrupted while peeking from input.
*/
public static FirstFrameMetadata getFirstFrameMetadata(ExtractorInput input)
throws IOException, InterruptedException {
input.resetPeekPosition();
ParsableByteArray scratch =
new ParsableByteArray(new byte[FlacConstants.MAX_FRAME_HEADER_SIZE]);
input.peekFully(scratch.data, /* offset= */ 0, FlacConstants.MAX_FRAME_HEADER_SIZE);
int frameStartMarker = scratch.readUnsignedShort();
int syncCode = frameStartMarker >> 2;
if (syncCode != SYNC_CODE) {
input.resetPeekPosition();
throw new ParserException("First frame does not start with sync code.");
}
scratch.setPosition(0);
int firstFrameBlockSizeSamples = FlacFrameReader.getFrameBlockSizeSamples(scratch);
input.resetPeekPosition();
return new FirstFrameMetadata(frameStartMarker, firstFrameBlockSizeSamples);
}
private FlacMetadataReader() {}
}

View File

@ -17,10 +17,12 @@ package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Holder for FLAC metadata. */
@ -28,14 +30,45 @@ public final class FlacStreamMetadata {
private static final String TAG = "FlacStreamMetadata";
public final int minBlockSize;
public final int maxBlockSize;
/** Indicates that a value is not in the corresponding lookup table. */
public static final int NOT_IN_LOOKUP_TABLE = -1;
/** Minimum number of samples per block. */
public final int minBlockSizeSamples;
/** Maximum number of samples per block. */
public final int maxBlockSizeSamples;
/** Minimum frame size in bytes, or 0 if the value is unknown. */
public final int minFrameSize;
/** Maximum frame size in bytes, or 0 if the value is unknown. */
public final int maxFrameSize;
/** Sample rate in Hertz. */
public final int sampleRate;
/**
* Lookup key corresponding to the stream sample rate, or {@link #NOT_IN_LOOKUP_TABLE} if it is
* not in the lookup table.
*
* <p>This key is used to indicate the sample rate in the frame header for the most common values.
*
* <p>The sample rate lookup table is described in https://xiph.org/flac/format.html#frame_header.
*/
public final int sampleRateLookupKey;
/** Number of audio channels. */
public final int channels;
/** Number of bits per sample. */
public final int bitsPerSample;
/**
* Lookup key corresponding to the number of bits per sample of the stream, or {@link
* #NOT_IN_LOOKUP_TABLE} if it is not in the lookup table.
*
* <p>This key is used to indicate the number of bits per sample in the frame header for the most
* common values.
*
* <p>The sample size lookup table is described in https://xiph.org/flac/format.html#frame_header.
*/
public final int bitsPerSampleLookupKey;
/** Total number of samples, or 0 if the value is unknown. */
public final long totalSamples;
/** Stream content metadata. */
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "=";
@ -44,27 +77,29 @@ public final class FlacStreamMetadata {
* Parses binary FLAC stream info metadata.
*
* @param data An array containing binary FLAC stream info metadata.
* @param offset The offset of the stream info metadata in {@code data}.
* @param offset The offset of the stream info block in {@code data} (header excluded).
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8);
this.minBlockSize = scratch.readBits(16);
this.maxBlockSize = scratch.readBits(16);
this.minBlockSizeSamples = scratch.readBits(16);
this.maxBlockSizeSamples = scratch.readBits(16);
this.minFrameSize = scratch.readBits(24);
this.maxFrameSize = scratch.readBits(24);
this.sampleRate = scratch.readBits(20);
this.sampleRateLookupKey = getSampleRateLookupKey();
this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey();
this.totalSamples = scratch.readBitsToLong(36);
this.metadata = null;
}
/**
* @param minBlockSize Minimum block size of the FLAC stream.
* @param maxBlockSize Maximum block size of the FLAC stream.
* @param minBlockSizeSamples Minimum block size of the FLAC stream.
* @param maxBlockSizeSamples Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream.
* @param maxFrameSize Maximum frame size of the FLAC stream.
* @param sampleRate Sample rate of the FLAC stream.
@ -81,8 +116,8 @@ public final class FlacStreamMetadata {
* METADATA_BLOCK_PICTURE</a>
*/
public FlacStreamMetadata(
int minBlockSize,
int maxBlockSize,
int minBlockSizeSamples,
int maxBlockSizeSamples,
int minFrameSize,
int maxFrameSize,
int sampleRate,
@ -91,30 +126,35 @@ public final class FlacStreamMetadata {
long totalSamples,
List<String> vorbisComments,
List<PictureFrame> pictureFrames) {
this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize;
this.minBlockSizeSamples = minBlockSizeSamples;
this.maxBlockSizeSamples = maxBlockSizeSamples;
this.minFrameSize = minFrameSize;
this.maxFrameSize = maxFrameSize;
this.sampleRate = sampleRate;
this.sampleRateLookupKey = getSampleRateLookupKey();
this.channels = channels;
this.bitsPerSample = bitsPerSample;
this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey();
this.totalSamples = totalSamples;
this.metadata = buildMetadata(vorbisComments, pictureFrames);
this.metadata = getMetadata(vorbisComments, pictureFrames);
}
/** Returns the maximum size for a decoded frame from the FLAC stream. */
public int maxDecodedFrameSize() {
return maxBlockSize * channels * (bitsPerSample / 8);
public int getMaxDecodedFrameSize() {
return maxBlockSizeSamples * channels * (bitsPerSample / 8);
}
/** Returns the bit-rate of the FLAC stream. */
public int bitRate() {
public int getBitRate() {
return bitsPerSample * sampleRate * channels;
}
/** Returns the duration of the FLAC stream in microseconds. */
public long durationUs() {
return (totalSamples * 1000000L) / sampleRate;
/**
* Returns the duration of the FLAC stream in microseconds, or {@link C#TIME_UNSET} if the total
* number of samples if unknown.
*/
public long getDurationUs() {
return totalSamples == 0 ? C.TIME_UNSET : totalSamples * C.MICROS_PER_SECOND / sampleRate;
}
/**
@ -125,7 +165,7 @@ public final class FlacStreamMetadata {
*/
public long getSampleIndex(long timeUs) {
long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
return Util.constrainValue(sampleIndex, 0, totalSamples - 1);
return Util.constrainValue(sampleIndex, /* min= */ 0, totalSamples - 1);
}
/** Returns the approximate number of bytes per frame for the current FLAC stream. */
@ -136,14 +176,91 @@ public final class FlacStreamMetadata {
} else {
// Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
// default value for FLAC block-size, which is 4096.
long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096;
approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64;
long blockSizeSamples =
(minBlockSizeSamples == maxBlockSizeSamples && minBlockSizeSamples > 0)
? minBlockSizeSamples
: 4096;
approxBytesPerFrame = (blockSizeSamples * channels * bitsPerSample) / 8 + 64;
}
return approxBytesPerFrame;
}
/**
* Returns a {@link Format} extracted from the FLAC stream metadata.
*
* <p>{@code streamMarkerAndInfoBlock} is updated to set the bit corresponding to the stream info
* last metadata block flag to true.
*
* @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the
* stream info block.
* @return The extracted {@link Format}.
*/
public Format getFormat(byte[] streamMarkerAndInfoBlock) {
// Set the last metadata block flag, ignore the other blocks.
streamMarkerAndInfoBlock[4] = (byte) 0x80;
int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE;
return Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_FLAC,
/* codecs= */ null,
getBitRate(),
maxInputSize,
channels,
sampleRate,
Collections.singletonList(streamMarkerAndInfoBlock),
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
}
private int getSampleRateLookupKey() {
switch (sampleRate) {
case 88200:
return 1;
case 176400:
return 2;
case 192000:
return 3;
case 8000:
return 4;
case 16000:
return 5;
case 22050:
return 6;
case 24000:
return 7;
case 32000:
return 8;
case 44100:
return 9;
case 48000:
return 10;
case 96000:
return 11;
default:
return NOT_IN_LOOKUP_TABLE;
}
}
private int getBitsPerSampleLookupKey() {
switch (bitsPerSample) {
case 8:
return 1;
case 12:
return 2;
case 16:
return 4;
case 20:
return 5;
case 24:
return 6;
default:
return NOT_IN_LOOKUP_TABLE;
}
}
@Nullable
private static Metadata buildMetadata(
private static Metadata getMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;

Binary file not shown.

View File

@ -0,0 +1,163 @@
seekMap:
isSeekable = false
duration = 2741000
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = 5776
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 83F6895
total output bytes = 164431
sample count = 33
sample 0:
time = 0
flags = 1
data = length 5030, hash D2B60530
sample 1:
time = 85333
flags = 1
data = length 5066, hash 4C932A54
sample 2:
time = 170666
flags = 1
data = length 5112, hash 7E5A7B61
sample 3:
time = 256000
flags = 1
data = length 5044, hash 7EF93F13
sample 4:
time = 341333
flags = 1
data = length 4943, hash DE7E27F8
sample 5:
time = 426666
flags = 1
data = length 5121, hash 6D0D0B40
sample 6:
time = 512000
flags = 1
data = length 5068, hash 9924644F
sample 7:
time = 597333
flags = 1
data = length 5143, hash 6C34F0CE
sample 8:
time = 682666
flags = 1
data = length 5109, hash E3B7BEFB
sample 9:
time = 768000
flags = 1
data = length 5129, hash 44111D9B
sample 10:
time = 853333
flags = 1
data = length 5031, hash 9D55EA53
sample 11:
time = 938666
flags = 1
data = length 5119, hash E1CB9BA6
sample 12:
time = 1024000
flags = 1
data = length 5360, hash 17265C5D
sample 13:
time = 1109333
flags = 1
data = length 5340, hash A90FDDF1
sample 14:
time = 1194666
flags = 1
data = length 5162, hash 31F65AD5
sample 15:
time = 1280000
flags = 1
data = length 5168, hash F2394F2D
sample 16:
time = 1365333
flags = 1
data = length 5776, hash 58437AB3
sample 17:
time = 1450666
flags = 1
data = length 5394, hash EBAB20A8
sample 18:
time = 1536000
flags = 1
data = length 5168, hash BF37C7A5
sample 19:
time = 1621333
flags = 1
data = length 5324, hash 59546B7B
sample 20:
time = 1706666
flags = 1
data = length 5172, hash 6036EF0B
sample 21:
time = 1792000
flags = 1
data = length 5102, hash 5A131071
sample 22:
time = 1877333
flags = 1
data = length 5111, hash 3D9EBB3B
sample 23:
time = 1962666
flags = 1
data = length 5113, hash 61101D4F
sample 24:
time = 2048000
flags = 1
data = length 5229, hash D2E55742
sample 25:
time = 2133333
flags = 1
data = length 5162, hash 7F2E97FA
sample 26:
time = 2218666
flags = 1
data = length 5255, hash D92A782
sample 27:
time = 2304000
flags = 1
data = length 5196, hash 98FE5138
sample 28:
time = 2389333
flags = 1
data = length 5214, hash 3D35C38C
sample 29:
time = 2474666
flags = 1
data = length 5211, hash 7E25420F
sample 30:
time = 2560000
flags = 1
data = length 5230, hash 2AD96FBC
sample 31:
time = 2645333
flags = 1
data = length 3384, hash 938BCDD9
sample 32:
time = 2730666
flags = 1
data = length 445, hash A388E3D6
tracksEnded = true

View File

@ -0,0 +1,163 @@
seekMap:
isSeekable = false
duration = 2741000
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 9218FDB7
total output bytes = 164431
sample count = 33
sample 0:
time = 0
flags = 1
data = length 5030, hash D2B60530
sample 1:
time = 85333
flags = 1
data = length 5066, hash 4C932A54
sample 2:
time = 170666
flags = 1
data = length 5112, hash 7E5A7B61
sample 3:
time = 256000
flags = 1
data = length 5044, hash 7EF93F13
sample 4:
time = 341333
flags = 1
data = length 4943, hash DE7E27F8
sample 5:
time = 426666
flags = 1
data = length 5121, hash 6D0D0B40
sample 6:
time = 512000
flags = 1
data = length 5068, hash 9924644F
sample 7:
time = 597333
flags = 1
data = length 5143, hash 6C34F0CE
sample 8:
time = 682666
flags = 1
data = length 5109, hash E3B7BEFB
sample 9:
time = 768000
flags = 1
data = length 5129, hash 44111D9B
sample 10:
time = 853333
flags = 1
data = length 5031, hash 9D55EA53
sample 11:
time = 938666
flags = 1
data = length 5119, hash E1CB9BA6
sample 12:
time = 1024000
flags = 1
data = length 5360, hash 17265C5D
sample 13:
time = 1109333
flags = 1
data = length 5340, hash A90FDDF1
sample 14:
time = 1194666
flags = 1
data = length 5162, hash 31F65AD5
sample 15:
time = 1280000
flags = 1
data = length 5168, hash F2394F2D
sample 16:
time = 1365333
flags = 1
data = length 5776, hash 58437AB3
sample 17:
time = 1450666
flags = 1
data = length 5394, hash EBAB20A8
sample 18:
time = 1536000
flags = 1
data = length 5168, hash BF37C7A5
sample 19:
time = 1621333
flags = 1
data = length 5324, hash 59546B7B
sample 20:
time = 1706666
flags = 1
data = length 5172, hash 6036EF0B
sample 21:
time = 1792000
flags = 1
data = length 5102, hash 5A131071
sample 22:
time = 1877333
flags = 1
data = length 5111, hash 3D9EBB3B
sample 23:
time = 1962666
flags = 1
data = length 5113, hash 61101D4F
sample 24:
time = 2048000
flags = 1
data = length 5229, hash D2E55742
sample 25:
time = 2133333
flags = 1
data = length 5162, hash 7F2E97FA
sample 26:
time = 2218666
flags = 1
data = length 5255, hash D92A782
sample 27:
time = 2304000
flags = 1
data = length 5196, hash 98FE5138
sample 28:
time = 2389333
flags = 1
data = length 5214, hash 3D35C38C
sample 29:
time = 2474666
flags = 1
data = length 5211, hash 7E25420F
sample 30:
time = 2560000
flags = 1
data = length 5230, hash 2AD96FBC
sample 31:
time = 2645333
flags = 1
data = length 3384, hash 938BCDD9
sample 32:
time = 2730666
flags = 1
data = length 445, hash A388E3D6
tracksEnded = true

View File

@ -0,0 +1,163 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = 5776
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 49FA2C21
total output bytes = 164431
sample count = 33
sample 0:
time = 0
flags = 1
data = length 5030, hash D2B60530
sample 1:
time = 85333
flags = 1
data = length 5066, hash 4C932A54
sample 2:
time = 170666
flags = 1
data = length 5112, hash 7E5A7B61
sample 3:
time = 256000
flags = 1
data = length 5044, hash 7EF93F13
sample 4:
time = 341333
flags = 1
data = length 4943, hash DE7E27F8
sample 5:
time = 426666
flags = 1
data = length 5121, hash 6D0D0B40
sample 6:
time = 512000
flags = 1
data = length 5068, hash 9924644F
sample 7:
time = 597333
flags = 1
data = length 5143, hash 6C34F0CE
sample 8:
time = 682666
flags = 1
data = length 5109, hash E3B7BEFB
sample 9:
time = 768000
flags = 1
data = length 5129, hash 44111D9B
sample 10:
time = 853333
flags = 1
data = length 5031, hash 9D55EA53
sample 11:
time = 938666
flags = 1
data = length 5119, hash E1CB9BA6
sample 12:
time = 1024000
flags = 1
data = length 5360, hash 17265C5D
sample 13:
time = 1109333
flags = 1
data = length 5340, hash A90FDDF1
sample 14:
time = 1194666
flags = 1
data = length 5162, hash 31F65AD5
sample 15:
time = 1280000
flags = 1
data = length 5168, hash F2394F2D
sample 16:
time = 1365333
flags = 1
data = length 5776, hash 58437AB3
sample 17:
time = 1450666
flags = 1
data = length 5394, hash EBAB20A8
sample 18:
time = 1536000
flags = 1
data = length 5168, hash BF37C7A5
sample 19:
time = 1621333
flags = 1
data = length 5324, hash 59546B7B
sample 20:
time = 1706666
flags = 1
data = length 5172, hash 6036EF0B
sample 21:
time = 1792000
flags = 1
data = length 5102, hash 5A131071
sample 22:
time = 1877333
flags = 1
data = length 5111, hash 3D9EBB3B
sample 23:
time = 1962666
flags = 1
data = length 5113, hash 61101D4F
sample 24:
time = 2048000
flags = 1
data = length 5229, hash D2E55742
sample 25:
time = 2133333
flags = 1
data = length 5162, hash 7F2E97FA
sample 26:
time = 2218666
flags = 1
data = length 5255, hash D92A782
sample 27:
time = 2304000
flags = 1
data = length 5196, hash 98FE5138
sample 28:
time = 2389333
flags = 1
data = length 5214, hash 3D35C38C
sample 29:
time = 2474666
flags = 1
data = length 5211, hash 7E25420F
sample 30:
time = 2560000
flags = 1
data = length 5230, hash 2AD96FBC
sample 31:
time = 2645333
flags = 1
data = length 3384, hash 938BCDD9
sample 32:
time = 2730666
flags = 1
data = length 445, hash A388E3D6
tracksEnded = true

View File

@ -0,0 +1,163 @@
seekMap:
isSeekable = false
duration = 2741000
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = 5776
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 83F6895
total output bytes = 164431
sample count = 33
sample 0:
time = 0
flags = 1
data = length 5030, hash D2B60530
sample 1:
time = 85333
flags = 1
data = length 5066, hash 4C932A54
sample 2:
time = 170666
flags = 1
data = length 5112, hash 7E5A7B61
sample 3:
time = 256000
flags = 1
data = length 5044, hash 7EF93F13
sample 4:
time = 341333
flags = 1
data = length 4943, hash DE7E27F8
sample 5:
time = 426666
flags = 1
data = length 5121, hash 6D0D0B40
sample 6:
time = 512000
flags = 1
data = length 5068, hash 9924644F
sample 7:
time = 597333
flags = 1
data = length 5143, hash 6C34F0CE
sample 8:
time = 682666
flags = 1
data = length 5109, hash E3B7BEFB
sample 9:
time = 768000
flags = 1
data = length 5129, hash 44111D9B
sample 10:
time = 853333
flags = 1
data = length 5031, hash 9D55EA53
sample 11:
time = 938666
flags = 1
data = length 5119, hash E1CB9BA6
sample 12:
time = 1024000
flags = 1
data = length 5360, hash 17265C5D
sample 13:
time = 1109333
flags = 1
data = length 5340, hash A90FDDF1
sample 14:
time = 1194666
flags = 1
data = length 5162, hash 31F65AD5
sample 15:
time = 1280000
flags = 1
data = length 5168, hash F2394F2D
sample 16:
time = 1365333
flags = 1
data = length 5776, hash 58437AB3
sample 17:
time = 1450666
flags = 1
data = length 5394, hash EBAB20A8
sample 18:
time = 1536000
flags = 1
data = length 5168, hash BF37C7A5
sample 19:
time = 1621333
flags = 1
data = length 5324, hash 59546B7B
sample 20:
time = 1706666
flags = 1
data = length 5172, hash 6036EF0B
sample 21:
time = 1792000
flags = 1
data = length 5102, hash 5A131071
sample 22:
time = 1877333
flags = 1
data = length 5111, hash 3D9EBB3B
sample 23:
time = 1962666
flags = 1
data = length 5113, hash 61101D4F
sample 24:
time = 2048000
flags = 1
data = length 5229, hash D2E55742
sample 25:
time = 2133333
flags = 1
data = length 5162, hash 7F2E97FA
sample 26:
time = 2218666
flags = 1
data = length 5255, hash D92A782
sample 27:
time = 2304000
flags = 1
data = length 5196, hash 98FE5138
sample 28:
time = 2389333
flags = 1
data = length 5214, hash 3D35C38C
sample 29:
time = 2474666
flags = 1
data = length 5211, hash 7E25420F
sample 30:
time = 2560000
flags = 1
data = length 5230, hash 2AD96FBC
sample 31:
time = 2645333
flags = 1
data = length 3384, hash 938BCDD9
sample 32:
time = 2730666
flags = 1
data = length 445, hash A388E3D6
tracksEnded = true

View File

@ -0,0 +1,139 @@
seekMap:
isSeekable = false
duration = 2741000
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1408000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = 6456
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 44000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 7249A1B8
total output bytes = 144086
sample count = 27
sample 0:
time = 0
flags = 1
data = length 5415, hash 915DBC66
sample 1:
time = 104727
flags = 1
data = length 5529, hash EFD564F7
sample 2:
time = 209454
flags = 1
data = length 5480, hash ADA922FB
sample 3:
time = 314181
flags = 1
data = length 5290, hash 7BCEA5FC
sample 4:
time = 418909
flags = 1
data = length 5579, hash DBB36F37
sample 5:
time = 523636
flags = 1
data = length 5423, hash AB53F799
sample 6:
time = 628363
flags = 1
data = length 5583, hash 7243C284
sample 7:
time = 733090
flags = 1
data = length 5547, hash 9DA9C99E
sample 8:
time = 837818
flags = 1
data = length 5414, hash 90768345
sample 9:
time = 942545
flags = 1
data = length 5531, hash 1CD2FF67
sample 10:
time = 1047272
flags = 1
data = length 5870, hash A9A5CAEE
sample 11:
time = 1152000
flags = 1
data = length 5667, hash 875566A1
sample 12:
time = 1256727
flags = 1
data = length 5614, hash 5573694C
sample 13:
time = 1361454
flags = 1
data = length 6456, hash 921F3DE7
sample 14:
time = 1466181
flags = 1
data = length 5817, hash EBECBD16
sample 15:
time = 1570909
flags = 1
data = length 5751, hash 4A7D4C6B
sample 16:
time = 1675636
flags = 1
data = length 5620, hash B78F8E8D
sample 17:
time = 1780363
flags = 1
data = length 5535, hash 8187C107
sample 18:
time = 1885090
flags = 1
data = length 5517, hash 79FF36CB
sample 19:
time = 1989818
flags = 1
data = length 5716, hash 349FC281
sample 20:
time = 2094545
flags = 1
data = length 5556, hash BE97B2CA
sample 21:
time = 2199272
flags = 1
data = length 5703, hash 531F9FE3
sample 22:
time = 2304000
flags = 1
data = length 5652, hash 1277485D
sample 23:
time = 2408727
flags = 1
data = length 5607, hash 14862CB6
sample 24:
time = 2513454
flags = 1
data = length 5829, hash FCAF2F1C
sample 25:
time = 2618181
flags = 1
data = length 2837, hash 10F1716E
sample 26:
time = 2722909
flags = 1
data = length 548, hash B46F603C
tracksEnded = true

Binary file not shown.

View File

@ -0,0 +1,163 @@
seekMap:
isSeekable = false
duration = 2741000
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:
bitrate = 1536000
id = null
containerMimeType = null
sampleMimeType = audio/flac
maxInputSize = 5776
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = 2
sampleRate = 48000
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 42, hash 83F6895
total output bytes = 164431
sample count = 33
sample 0:
time = 0
flags = 1
data = length 5030, hash D2B60530
sample 1:
time = 85333
flags = 1
data = length 5066, hash 4C932A54
sample 2:
time = 170666
flags = 1
data = length 5112, hash 7E5A7B61
sample 3:
time = 256000
flags = 1
data = length 5044, hash 7EF93F13
sample 4:
time = 341333
flags = 1
data = length 4943, hash DE7E27F8
sample 5:
time = 426666
flags = 1
data = length 5121, hash 6D0D0B40
sample 6:
time = 512000
flags = 1
data = length 5068, hash 9924644F
sample 7:
time = 597333
flags = 1
data = length 5143, hash 6C34F0CE
sample 8:
time = 682666
flags = 1
data = length 5109, hash E3B7BEFB
sample 9:
time = 768000
flags = 1
data = length 5129, hash 44111D9B
sample 10:
time = 853333
flags = 1
data = length 5031, hash 9D55EA53
sample 11:
time = 938666
flags = 1
data = length 5119, hash E1CB9BA6
sample 12:
time = 1024000
flags = 1
data = length 5360, hash 17265C5D
sample 13:
time = 1109333
flags = 1
data = length 5340, hash A90FDDF1
sample 14:
time = 1194666
flags = 1
data = length 5162, hash 31F65AD5
sample 15:
time = 1280000
flags = 1
data = length 5168, hash F2394F2D
sample 16:
time = 1365333
flags = 1
data = length 5776, hash 58437AB3
sample 17:
time = 1450666
flags = 1
data = length 5394, hash EBAB20A8
sample 18:
time = 1536000
flags = 1
data = length 5168, hash BF37C7A5
sample 19:
time = 1621333
flags = 1
data = length 5324, hash 59546B7B
sample 20:
time = 1706666
flags = 1
data = length 5172, hash 6036EF0B
sample 21:
time = 1792000
flags = 1
data = length 5102, hash 5A131071
sample 22:
time = 1877333
flags = 1
data = length 5111, hash 3D9EBB3B
sample 23:
time = 1962666
flags = 1
data = length 5113, hash 61101D4F
sample 24:
time = 2048000
flags = 1
data = length 5229, hash D2E55742
sample 25:
time = 2133333
flags = 1
data = length 5162, hash 7F2E97FA
sample 26:
time = 2218666
flags = 1
data = length 5255, hash D92A782
sample 27:
time = 2304000
flags = 1
data = length 5196, hash 98FE5138
sample 28:
time = 2389333
flags = 1
data = length 5214, hash 3D35C38C
sample 29:
time = 2474666
flags = 1
data = length 5211, hash 7E25420F
sample 30:
time = 2560000
flags = 1
data = length 5230, hash 2AD96FBC
sample 31:
time = 2645333
flags = 1
data = length 3384, hash 938BCDD9
sample 32:
time = 2730666
flags = 1
data = length 445, hash A388E3D6
tracksEnded = true

View File

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
@ -64,7 +65,8 @@ public final class DefaultExtractorsFactoryTest {
PsExtractor.class,
WavExtractor.class,
AmrExtractor.class,
Ac4Extractor.class
Ac4Extractor.class,
FlacExtractor.class
};
assertThat(listCreatedExtractorClasses).containsNoDuplicates();

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.flac;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link FlacExtractor}. */
@RunWith(AndroidJUnit4.class)
public class FlacExtractorTest {
@Test
public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear.flac");
}
@Test
public void testSampleWithId3() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_with_id3.flac");
}
@Test
public void testOneMetadataBlock() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac");
}
@Test
public void testNoMinMaxFrameSize() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_min_max_frame_size.flac");
}
@Test
public void testNoNumSamples() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_no_num_samples.flac");
}
@Test
public void testUncommonSampleRate() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac");
}
}