blob: a4a2b64cb927a996b608aa53aef4ed290b3de5b2 [file] [log] [blame]
/*
* 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.exoplayer2.extractor;
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 com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Holder for FLAC metadata.
*
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
* METADATA_BLOCK_SEEKTABLE</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
* METADATA_BLOCK_PICTURE</a>
*/
public final class FlacStreamMetadata {
/** A FLAC seek table. */
public static class SeekTable {
/** Seek points sample numbers. */
public final long[] pointSampleNumbers;
/** Seek points byte offsets from the first frame. */
public final long[] pointOffsets;
public SeekTable(long[] pointSampleNumbers, long[] pointOffsets) {
this.pointSampleNumbers = pointSampleNumbers;
this.pointOffsets = pointOffsets;
}
}
private static final String TAG = "FlacStreamMetadata";
/** Indicates that a value is not in the corresponding lookup table. */
public static final int NOT_IN_LOOKUP_TABLE = -1;
/** Separator between the field name of a Vorbis comment and the corresponding value. */
private static final String SEPARATOR = "=";
/** 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;
/** Seek table, or {@code null} if it is not provided. */
@Nullable public final SeekTable seekTable;
/** Content metadata, or {@code null} if it is not provided. */
@Nullable private final Metadata metadata;
/**
* Parses binary FLAC stream info metadata.
*
* @param data An array containing binary FLAC stream info block.
* @param offset The offset of the stream info block in {@code data}, excluding the header (i.e.
* the offset points to the first byte of the minimum block size).
*/
public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8);
minBlockSizeSamples = scratch.readBits(16);
maxBlockSizeSamples = scratch.readBits(16);
minFrameSize = scratch.readBits(24);
maxFrameSize = scratch.readBits(24);
sampleRate = scratch.readBits(20);
sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
channels = scratch.readBits(3) + 1;
bitsPerSample = scratch.readBits(5) + 1;
bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
totalSamples = scratch.readBitsToLong(36);
seekTable = null;
metadata = null;
}
// Used in native code.
public FlacStreamMetadata(
int minBlockSizeSamples,
int maxBlockSizeSamples,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples,
ArrayList<String> vorbisComments,
ArrayList<PictureFrame> pictureFrames) {
this(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
/* seekTable= */ null,
buildMetadata(vorbisComments, pictureFrames));
}
private FlacStreamMetadata(
int minBlockSizeSamples,
int maxBlockSizeSamples,
int minFrameSize,
int maxFrameSize,
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples,
@Nullable SeekTable seekTable,
@Nullable Metadata metadata) {
this.minBlockSizeSamples = minBlockSizeSamples;
this.maxBlockSizeSamples = maxBlockSizeSamples;
this.minFrameSize = minFrameSize;
this.maxFrameSize = maxFrameSize;
this.sampleRate = sampleRate;
this.sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
this.channels = channels;
this.bitsPerSample = bitsPerSample;
this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
this.totalSamples = totalSamples;
this.seekTable = seekTable;
this.metadata = metadata;
}
/** Returns the maximum size for a decoded frame from the FLAC stream. */
public int getMaxDecodedFrameSize() {
return maxBlockSizeSamples * channels * (bitsPerSample / 8);
}
/** Returns the bitrate of the stream after it's decoded into PCM. */
public int getDecodedBitrate() {
return bitsPerSample * sampleRate * channels;
}
/**
* 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;
}
/**
* Returns the sample number of the sample at a given time.
*
* @param timeUs Time position in microseconds in the FLAC stream.
* @return The sample number corresponding to the time position.
*/
public long getSampleNumber(long timeUs) {
long sampleNumber = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
return Util.constrainValue(sampleNumber, /* min= */ 0, totalSamples - 1);
}
/** Returns the approximate number of bytes per frame for the current FLAC stream. */
public long getApproxBytesPerFrame() {
long approxBytesPerFrame;
if (maxFrameSize > 0) {
approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
} 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 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.
* @param id3Metadata The ID3 metadata of the stream, or {@code null} if there is no such data.
* @return The extracted {@link Format}.
*/
public Format getFormat(byte[] streamMarkerAndInfoBlock, @Nullable Metadata id3Metadata) {
// Set the last metadata block flag, ignore the other blocks.
streamMarkerAndInfoBlock[4] = (byte) 0x80;
int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE;
@Nullable Metadata metadataWithId3 = getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_FLAC)
.setMaxInputSize(maxInputSize)
.setChannelCount(channels)
.setSampleRate(sampleRate)
.setInitializationData(Collections.singletonList(streamMarkerAndInfoBlock))
.setMetadata(metadataWithId3)
.build();
}
/** Returns a copy of the content metadata with entries from {@code other} appended. */
@Nullable
public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) {
return metadata == null ? other : metadata.copyWithAppendedEntriesFrom(other);
}
/** Returns a copy of {@code this} with the seek table replaced by the one given. */
public FlacStreamMetadata copyWithSeekTable(@Nullable SeekTable seekTable) {
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
seekTable,
metadata);
}
/** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */
public FlacStreamMetadata copyWithVorbisComments(List<String> vorbisComments) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(vorbisComments, Collections.emptyList()));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
seekTable,
appendedMetadata);
}
/** Returns a copy of {@code this} with the given picture frames added to the metadata. */
public FlacStreamMetadata copyWithPictureFrames(List<PictureFrame> pictureFrames) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(Collections.emptyList(), pictureFrames));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
totalSamples,
seekTable,
appendedMetadata);
}
private static int getSampleRateLookupKey(int sampleRate) {
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 static int getBitsPerSampleLookupKey(int bitsPerSample) {
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(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
}