Source: lib/transmuxer/mpeg_audio.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.transmuxer.MpegAudio');


/**
 *  MPEG parser utils
 *
 *  @see https://en.wikipedia.org/wiki/MP3
 */
shaka.transmuxer.MpegAudio = class {
  /**
   * @param {!Uint8Array} data
   * @param {!number} offset
   * @return {?{sampleRate: number, channelCount: number,
   * frameLength: number, samplesPerFrame: number}}
   */
  static parseHeader(data, offset) {
    const MpegAudio = shaka.transmuxer.MpegAudio;

    const mpegVersion = (data[offset + 1] >> 3) & 3;
    const mpegLayer = (data[offset + 1] >> 1) & 3;
    const bitRateIndex = (data[offset + 2] >> 4) & 15;
    const sampleRateIndex = (data[offset + 2] >> 2) & 3;
    if (mpegVersion !== 1 && bitRateIndex !== 0 &&
      bitRateIndex !== 15 && sampleRateIndex !== 3) {
      const paddingBit = (data[offset + 2] >> 1) & 1;
      const channelMode = data[offset + 3] >> 6;
      let columnInBitrates = 0;
      if (mpegVersion === 3) {
        columnInBitrates = 3 - mpegLayer;
      } else {
        columnInBitrates = mpegLayer === 3 ? 3 : 4;
      }
      const bitRate = MpegAudio.BITRATES_MAP_[
          columnInBitrates * 14 + bitRateIndex - 1] * 1000;
      const columnInSampleRates =
        mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2;
      const sampleRate = MpegAudio.SAMPLINGRATE_MAP_[
          columnInSampleRates * 3 + sampleRateIndex];
      // If bits of channel mode are `11` then it is a single channel (Mono)
      const channelCount = channelMode === 3 ? 1 : 2;
      const sampleCoefficient =
          MpegAudio.SAMPLES_COEFFICIENTS_[mpegVersion][mpegLayer];
      const bytesInSlot = MpegAudio.BYTES_IN_SLOT_[mpegLayer];
      const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot;
      const frameLength =
        Math.floor((sampleCoefficient * bitRate) / sampleRate + paddingBit) *
        bytesInSlot;

      const userAgent = navigator.userAgent || '';
      // This affect to Tizen also for example.
      const result = userAgent.match(/Chrome\/(\d+)/i);
      const chromeVersion = result ? parseInt(result[1], 10) : 0;
      const needChromeFix = !!chromeVersion && chromeVersion <= 87;

      if (needChromeFix && mpegLayer === 2 &&
        bitRate >= 224000 && channelMode === 0) {
        // Work around bug in Chromium by setting channelMode
        // to dual-channel (01) instead of stereo (00)
        data[offset + 3] = data[offset + 3] | 0x80;
      }

      return {sampleRate, channelCount, frameLength, samplesPerFrame};
    }
    return null;
  }

  /**
   * @param {!Uint8Array} data
   * @param {!number} offset
   * @return {boolean}
   */
  static isHeaderPattern(data, offset) {
    return (
      data[offset] === 0xff &&
      (data[offset + 1] & 0xe0) === 0xe0 &&
      (data[offset + 1] & 0x06) !== 0x00
    );
  }

  /**
   * @param {!Uint8Array} data
   * @param {!number} offset
   * @return {boolean}
   */
  static isHeader(data, offset) {
    // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either
    // 0 or 1 and Y or Z should be 1
    // Layer bits (position 14 and 15) in header should be always different
    // from 0 (Layer I or Layer II or Layer III)
    // More info http://www.mp3-tech.org/programmer/frame_header.html
    return offset + 1 < data.length &&
      shaka.transmuxer.MpegAudio.isHeaderPattern(data, offset);
  }

  /**
   * @param {!Uint8Array} data
   * @param {!number} offset
   * @return {boolean}
   */
  static probe(data, offset) {
    const MpegAudio = shaka.transmuxer.MpegAudio;
    // same as isHeader but we also check that MPEG frame follows last
    // MPEG frame or end of data is reached
    if (offset + 1 < data.length &&
        MpegAudio.isHeaderPattern(data, offset)) {
      // MPEG header Length
      const headerLength = 4;
      // MPEG frame Length
      const header = MpegAudio.parseHeader(data, offset);
      let frameLength = headerLength;
      if (header && header.frameLength) {
        frameLength = header.frameLength;
      }

      const newOffset = offset + frameLength;
      return newOffset === data.length ||
          MpegAudio.isHeader(data, newOffset);
    }
    return false;
  }
};


/**
 * @private {!Array.<number>}
 */
shaka.transmuxer.MpegAudio.BITRATES_MAP_ = [
  32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 32, 48, 56,
  64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 32, 40, 48, 56, 64, 80,
  96, 112, 128, 160, 192, 224, 256, 320, 32, 48, 56, 64, 80, 96, 112, 128, 144,
  160, 176, 192, 224, 256, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144,
  160,
];

/**
 * @private {!Array.<number>}
 */
shaka.transmuxer.MpegAudio.SAMPLINGRATE_MAP_ = [
  44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000,
];

/**
 * @private {!Array.<!Array.<number>>}
 */
shaka.transmuxer.MpegAudio.SAMPLES_COEFFICIENTS_ = [
  // MPEG 2.5
  [
    0, // Reserved
    72, // Layer3
    144, // Layer2
    12, // Layer1
  ],
  // Reserved
  [
    0, // Reserved
    0, // Layer3
    0, // Layer2
    0, // Layer1
  ],
  // MPEG 2
  [
    0, // Reserved
    72, // Layer3
    144, // Layer2
    12, // Layer1
  ],
  // MPEG 1
  [
    0, // Reserved
    144, // Layer3
    144, // Layer2
    12, // Layer1
  ],
];


/**
 * @private {!Array.<number>}
 */
shaka.transmuxer.MpegAudio.BYTES_IN_SLOT_ = [
  0, // Reserved
  1, // Layer3
  1, // Layer2
  4, // Layer1
];

/**
 * @const {number}
 */
shaka.transmuxer.MpegAudio.MPEG_AUDIO_SAMPLE_PER_FRAME = 1152;