summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java
blob: 893481d8da2d32fcbc42e4b1c483721c48b3a42a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
/*
 * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;

import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.VorbisComment;
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 bit-rate of the FLAC stream. */
  public int getBitRate() {
    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 Format.createAudioSampleFormat(
        /* id= */ null,
        MimeTypes.AUDIO_FLAC,
        /* codecs= */ null,
        getBitRate(),
        maxInputSize,
        channels,
        sampleRate,
        /* pcmEncoding= */ Format.NO_VALUE,
        /* encoderDelay= */ 0,
        /* encoderPadding= */ 0,
        /* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock),
        /* drmInitData= */ null,
        /* selectionFlags= */ 0,
        /* language= */ null,
        metadataWithId3);
  }

  /** 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);
  }
}