FlacStreamMetadata.java

/*
 * 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 androidx.media3.extractor;

import static androidx.media3.extractor.VorbisUtil.parseVorbisComments;

import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.ParsableBitArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.metadata.flac.PictureFrame;
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>
 */
@UnstableApi
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;

  /** 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,
        concatenateVorbisMetadata(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(parseVorbisComments(vorbisComments));
    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(new Metadata(pictureFrames));
    return new FlacStreamMetadata(
        minBlockSizeSamples,
        maxBlockSizeSamples,
        minFrameSize,
        maxFrameSize,
        sampleRate,
        channels,
        bitsPerSample,
        totalSamples,
        seekTable,
        appendedMetadata);
  }

  /**
   * Returns a new {@link Metadata} instance created from {@code vorbisComments} and {@code
   * pictureFrames}.
   */
  @Nullable
  private static Metadata concatenateVorbisMetadata(
      List<String> vorbisComments, List<PictureFrame> pictureFrames) {
    @Nullable Metadata parsedVorbisComments = parseVorbisComments(vorbisComments);
    if (parsedVorbisComments == null && pictureFrames.isEmpty()) {
      return null;
    }
    return new Metadata(pictureFrames).copyWithAppendedEntriesFrom(parsedVorbisComments);
  }

  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;
    }
  }
}