Source: lib/codec/opus.js

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

goog.provide('shaka.codec.Opus');

goog.require('shaka.util.BufferUtils');

goog.requireType('shaka.util.TsParser');


/**
 * Opus utils
 */
shaka.codec.Opus = class {
  /**
   * Converts an OpusHead (Ogg and Matroska) to the payload of the dOps box
   * (MP4).  Both carry the same fields, but OpusHead is little endian and dOps
   * is big endian.
   *
   * @param {!Uint8Array} head
   * @return {!Uint8Array}
   */
  static getDops(head) {
    const Opus = shaka.codec.Opus;
    const input = shaka.util.BufferUtils.toDataView(head);
    // The bytes that follow the gain are the channel mapping, which is the
    // same in both boxes.
    const mapping = head.subarray(Opus.HEAD_MAPPING_FAMILY_);
    const output = new Uint8Array(Opus.DOPS_MAPPING_FAMILY_ + mapping.length);
    const view = shaka.util.BufferUtils.toDataView(output);
    // The version of dOps is 0, whereas the one of OpusHead is 1.
    view.setUint8(0, 0);
    view.setUint8(1, input.getUint8(9));
    view.setUint16(2, input.getUint16(10, true));
    view.setUint32(4, input.getUint32(12, true));
    view.setInt16(8, input.getInt16(16, true));
    output.set(mapping, Opus.DOPS_MAPPING_FAMILY_);
    return output;
  }

  /**
   * Makes the OpusHead of a plain mono or stereo stream.
   *
   * @param {number} channelCount
   * @param {number} preSkip In samples at 48 kHz.
   * @return {!Uint8Array}
   */
  static makeHead(channelCount, preSkip) {
    const Opus = shaka.codec.Opus;
    const head = new Uint8Array(Opus.HEAD_SIZE_);
    const view = shaka.util.BufferUtils.toDataView(head);
    head.set(Opus.HEAD_MAGIC_);
    view.setUint8(8, 1);
    view.setUint8(9, channelCount);
    view.setUint16(10, preSkip, /* littleEndian= */ true);
    view.setUint32(12, Opus.SAMPLE_RATE, true);
    return head;
  }

  /**
   * @param {!shaka.util.TsParser.OpusMetadata} metadata
   * @return {!Uint8Array}
   */
  static getAudioConfig(metadata) {
    let mapping = [];
    switch (metadata.channelConfigCode) {
      case 0x01:
      case 0x02:
        mapping = [0x0];
        break;
      case 0x00: // dualmono
        mapping = [0xFF, 1, 1, 0, 1];
        break;
      case 0x80: // dualmono
        mapping = [0xFF, 2, 0, 0, 1];
        break;
      case 0x03:
        mapping = [0x01, 2, 1, 0, 2, 1];
        break;
      case 0x04:
        mapping = [0x01, 2, 2, 0, 1, 2, 3];
        break;
      case 0x05:
        mapping = [0x01, 3, 2, 0, 4, 1, 2, 3];
        break;
      case 0x06:
        mapping = [0x01, 4, 2, 0, 4, 1, 2, 3, 5];
        break;
      case 0x07:
        mapping = [0x01, 4, 2, 0, 4, 1, 2, 3, 5, 6];
        break;
      case 0x08:
        mapping = [0x01, 5, 3, 0, 6, 1, 2, 3, 4, 5, 7];
        break;
      case 0x82:
        mapping = [0x01, 1, 2, 0, 1];
        break;
      case 0x83:
        mapping = [0x01, 1, 3, 0, 1, 2];
        break;
      case 0x84:
        mapping = [0x01, 1, 4, 0, 1, 2, 3];
        break;
      case 0x85:
        mapping = [0x01, 1, 5, 0, 1, 2, 3, 4];
        break;
      case 0x86:
        mapping = [0x01, 1, 6, 0, 1, 2, 3, 4, 5];
        break;
      case 0x87:
        mapping = [0x01, 1, 7, 0, 1, 2, 3, 4, 5, 6];
        break;
      case 0x88:
        mapping = [0x01, 1, 8, 0, 1, 2, 3, 4, 5, 6, 7];
        break;
    }

    return new Uint8Array([
      0x00,         // Version (1)
      metadata.channelCount, // OutputChannelCount: 2
      0x00, 0x00,   // PreSkip: 2
      (metadata.sampleRate >>> 24) & 0xFF,  // Audio sample rate: 4
      (metadata.sampleRate >>> 17) & 0xFF,
      (metadata.sampleRate >>> 8) & 0xFF,
      (metadata.sampleRate >>> 0) & 0xFF,
      0x00, 0x00,  // Global Gain : 2
      ...mapping,
    ]);
  }

  /**
   * Returns the number of 48 kHz samples represented by an opus packet,
   * derived from its TOC byte and (for code 3) the frame-count byte.
   * See RFC 6716 §3.1.
   *
   * One PES packet in an MPEG-TS opus stream can carry a multi-frame opus
   * packet (e.g. browser MediaRecorder emits code-3 packets with 3x 20ms
   * CELT frames = 60ms per packet). Treating one PES packet as one frame's
   * worth of samples under-counts duration and breaks timeline alignment
   * in the resulting mp4.
   *
   * Returned counts are at 48 kHz. This assumes the caller writes the opus
   * track with a 48 kHz mp4 timescale, which TsParser hardcodes for opus
   * (RFC 6716: opus always decodes internally at 48 kHz; OpusHead's input
   * sample rate is informational). If that hardcode ever becomes variable,
   * scale the return value by (timescale / 48000).
   *
   * @param {!Uint8Array} packet  Opus packet starting at the TOC byte.
   * @return {number}  Sample count at 48 kHz.
   */
  static getPacketSampleCount(packet) {
    if (packet.length < 1) {
      return shaka.codec.Opus.OPUS_AUDIO_SAMPLE_PER_FRAME;
    }
    const toc = packet[0];
    const config = (toc >> 3) & 0x1F;
    const code = toc & 0x03;
    const spf = shaka.codec.Opus.SAMPLES_PER_FRAME_BY_CONFIG_[config];
    let frames;
    if (code === 0) {
      frames = 1;
    } else if (code === 1 || code === 2) {
      frames = 2;
    } else {
      // Code 3: number of frames is in bits 0-5 of the second byte.
      if (packet.length < 2) {
        return spf;
      }
      frames = packet[1] & 0x3F;
      if (frames === 0) {
        return spf;
      }
    }
    return spf * frames;
  }
};

/**
 * Samples per opus frame at 48 kHz, indexed by the 5-bit config field from
 * the TOC byte. See RFC 6716 §3.1, Table 2.
 *
 * @private @const {!Array<number>}
 */
shaka.codec.Opus.SAMPLES_PER_FRAME_BY_CONFIG_ = [
  480, 960, 1920, 2880,    // SILK NB     10/20/40/60 ms
  480, 960, 1920, 2880,    // SILK MB     10/20/40/60 ms
  480, 960, 1920, 2880,    // SILK WB     10/20/40/60 ms
  480, 960,                // Hybrid SWB  10/20 ms
  480, 960,                // Hybrid FB   10/20 ms
  120, 240, 480, 960,      // CELT NB     2.5/5/10/20 ms
  120, 240, 480, 960,      // CELT WB
  120, 240, 480, 960,      // CELT SWB
  120, 240, 480, 960,      // CELT FB
];

/**
 * Retained for backward compatibility. Prefer getPacketSampleCount(), which
 * handles multi-frame opus packets correctly.
 *
 * @const {number}
 */
shaka.codec.Opus.OPUS_AUDIO_SAMPLE_PER_FRAME = 960;


/**
 * Opus always decodes at 48 kHz (RFC 6716); the input rate an OpusHead
 * announces is informational.
 *
 * @const {number}
 */
shaka.codec.Opus.SAMPLE_RATE = 48000;

/**
 * The 8 ASCII bytes 'OpusHead' that start the identification header.
 *
 * @private @const {!Uint8Array}
 */
shaka.codec.Opus.HEAD_MAGIC_ =
    new Uint8Array([0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]);

/**
 * The size of an OpusHead without a channel mapping table.
 *
 * @private @const {number}
 */
shaka.codec.Opus.HEAD_SIZE_ = 19;

/**
 * Offset of the channel mapping family byte in OpusHead (after the 8-byte
 * magic, the version, the channel count, the pre-skip, the input rate and the
 * gain) and in dOps (which lacks the magic).  Everything from there on is the
 * same in both.
 *
 * @private @const {number}
 */
shaka.codec.Opus.HEAD_MAPPING_FAMILY_ = 18;

/** @private @const {number} */
shaka.codec.Opus.DOPS_MAPPING_FAMILY_ = 10;