widgets/multimeter.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

import { defineChildWidget } from '../child_widget.js';
import { LevelMeter } from './levelmeter.js';
import { Label } from './label.js';
import { Container } from './container.js';
import { addClass, removeClass, toggleClass } from '../utils/dom.js';
import { objectSub } from '../utils/object.js';

const mapped_options = {
  labels: 'label',
  layout: 'layout',
};

function extractChildOptions(O, i) {
  const o = {};

  for (const _key in mapped_options) {
    if (!Object.prototype.hasOwnProperty.call(O, _key)) continue;
    const ckey = mapped_options[_key];
    const value = O[_key];
    const _type = LevelMeter.getOptionType(_key) || '';
    if (Array.isArray(value) && _type.search('array') === -1) {
      if (i < value.length) o[ckey] = value[i];
    } else {
      o[ckey] = value;
    }
  }

  return o;
}

function addMeter(options) {
  const l = this.meters.length;
  const O = options;
  const opt = extractChildOptions(O, l);
  const m = new LevelMeter(opt);

  this.meters.push(m);
  this.appendChild(m);
}
function removeMeter(meter) {
  /* meter can be int or meter instance */
  let M = this.meters;

  let m = -1;
  if (typeof meter == 'number') {
    m = meter;
  } else {
    for (let i = 0; i < M.length; i++) {
      if (M[i] == meter) {
        m = i;
        break;
      }
    }
  }
  if (m < 0 || m > M.length - 1) return;
  this.removeChild(M[m]);
  M[m].element.remove();
  // TODO: no destroy function in levelmeter at this point?
  //this.meters[m].destroy();
  M = M.splice(m, 1);
}

function mapChildOptionSimple(value, key) {
  const M = this.meters;
  for (let i = 0; i < M.length; i++) M[i].set(mapped_options[key], value);
}

function mapChildOption(value, key) {
  const M = this.meters;

  if (Array.isArray(value)) {
    for (let i = 0; i < M.length && i < value.length; i++)
      M[i].set(mapped_options[key], value[i]);
  } else {
    for (let i = 0; i < M.length; i++) M[i].set(key, value);
  }
}

const multimeterOptionTypes = {
  count: 'int',
  label: 'boolean|string',
  labels: 'array|string',
  layout: 'string',
  show_scale: 'boolean',
};

const multimeterOptionDefaults = {
  count: 2,
  label: false,
  labels: null,
  layout: 'left',
  show_scale: true,
  presets: {
    mono: { count: 1, labels: ['C'] },
    stereo: { count: 2, labels: ['L', 'R'] },
    '2.1': { count: 3, labels: ['L', 'R', 'LF'] },
    '3': { count: 3, labels: ['L', 'C', 'R'] },
    '3.1': { count: 4, labels: ['L', 'C', 'R', 'LF'] },
    '4': { count: 4, labels: ['L', 'R', "L'", "R'"] },
    '4.1': { count: 5, labels: ['L', 'R', "L'", "R'", 'LF'] },
    '5': { count: 5, labels: ['L', 'C', 'R', "L'", 'R'] },
    '5.1': { count: 6, labels: ['L', 'C', 'R', "L'", "R'", 'LF'] },
    '7.1': {
      count: 6,
      labels: ['L', 'C', 'R', "L'", "L''", "R''", "R'", 'LF'],
    },
    dolby_digital_1_0: { count: 1, labels: ['C'] },
    dolby_digital_2_0: { count: 2, labels: ['L', 'R'] },
    dolby_digital_3_0: { count: 3, labels: ['L', 'R', 'C'] },
    dolby_digital_2_1: { count: 3, labels: ['L', 'R', "C'"] },
    'dolby_digital_2_1.1': { count: 4, labels: ['L', 'R', "C'", 'LF'] },
    dolby_digital_3_1: { count: 4, labels: ['L', 'R', 'C', "C'"] },
    'dolby_digital_3_1.1': { count: 5, labels: ['L', 'R', 'C', "C'", 'LF'] },
    dolby_digital_2_2: { count: 4, labels: ['L', 'R', "L'", "R'"] },
    'dolby_digital_2_2.1': { count: 5, labels: ['L', 'R', "L'", "R'", 'LF'] },
    dolby_digital_3_2: { count: 5, labels: ['L', 'R', 'C', "L'", "R'"] },
    'dolby_digital_3_2.1': {
      count: 6,
      labels: ['L', 'R', 'C', "L'", "R'", 'LF'],
    },
    dolby_digital_ex: {
      count: 7,
      labels: ['L', 'R', 'C', "L'", "R'", "C'", 'LF'],
    },
    dolby_stereo: { count: 4, labels: ['L', 'R', 'C', "C'"] },
    dolby_digital: { count: 4, labels: ['L', 'R', 'C', "C'"] },
    dolby_pro_logic: { count: 4, labels: ['L', 'R', 'C', "C'"] },
    dolby_pro_logic_2: {
      count: 6,
      labels: ['L', 'R', 'C', "L'", "R'", 'LF'],
    },
    dolby_pro_logic_2x: {
      count: 8,
      labels: ['L', 'R', 'C', "L'", "L''", "R''", "R'", 'LF'],
    },
    dolby_e_mono: {
      count: 8,
      labels: ['1', '2', '3', '4', '5', '6', '7', '8'],
    },
    dolby_e_stereo: {
      count: 8,
      labels: ['1L', '1R', '2L', '2R', '3L', '3R', '4L', '4R'],
    },
    'dolby_e_5.1_stereo': {
      count: 8,
      labels: ['1L', '1R', '1C', "1L'", "1R'", '1LF', '2L', '2R'],
    },
  },
};

const multimeterStaticEvents = {
  set_labels: mapChildOption,
  set_layout: mapChildOption,
};

const levelmeterOwnOptions = objectSub(
  LevelMeter.getOptionTypes(),
  Container.getOptionTypes()
);

for (const key in levelmeterOwnOptions) {
  if (!Object.prototype.hasOwnProperty.call(levelmeterOwnOptions, key))
    continue;
  if (multimeterOptionTypes.hasOwnProperty(key)) continue;

  const type = levelmeterOwnOptions[key];

  if (type.search('array') !== -1) {
    multimeterOptionTypes[key] = type;
    mapped_options[key] = key;
    multimeterStaticEvents['set_' + key] = mapChildOptionSimple;
  } else {
    multimeterOptionTypes[key] = 'array|' + type;
    mapped_options[key] = key;
    multimeterStaticEvents['set_' + key] = mapChildOption;
  }
}

/**
 * MultiMeter is a collection of {@link LevelMeter}s to show levels of channels
 * containing multiple audio streams. It offers all options of {@link LevelMeter} and
 * {@link Meter} which are passed to all instantiated level meters.
 *
 * Available presets:
 *
 * <ul>
 *   <li>mono</li>
 *   <li>2.1</li>
 *   <li>3</li>
 *   <li>3.1</li>
 *   <li>4</li>
 *   <li>4.1</li>
 *   <li>5</li>
 *   <li>5.1</li>
 *   <li>7.1</li>
 *   <li>dolby_digital_1_0</li>
 *   <li>dolby_digital_2_0</li>
 *   <li>dolby_digital_3_0</li>
 *   <li>dolby_digital_2_1</li>
 *   <li>dolby_digital_2_1.1</li>
 *   <li>dolby_digital_3_1</li>
 *   <li>dolby_digital_3_1.1</li>
 *   <li>dolby_digital_2_2</li>
 *   <li>dolby_digital_2_2.1</li>
 *   <li>dolby_digital_3_2</li>
 *   <li>dolby_digital_3_2.1</li>
 *   <li>dolby_digital_ex</li>
 *   <li>dolby_stereo</li>
 *   <li>dolby_digital</li>
 *   <li>dolby_pro_logic</li>
 *   <li>dolby_pro_logic_2</li>
 *   <li>dolby_pro_logic_2x</li>
 *   <li>dolby_e_mono</li>
 *   <li>dolby_e_stereo</li>
 *   <li>dolby_e_5.1_stereo</li>
 * </ul>
 *
 * @class MultiMeter
 *
 * @extends Container
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Number} [options.count=2] - The number of level meters.
 * @property {String} [options.label=""] - The label of the multi meter. Set to `false` to hide the label from the DOM.
 * @property {Array<String>} [options.labels=["L", "R"]] - An Array containing labels for the level meters. Their order is the same as the meters.
 * @property {Array<Number>} [options.values=[]] - An Array containing values for the level meters. Their order is the same as the meters.
 * @property {Array<Number>} [options.value_labels=[]] - An Array containing label values for the level meters. Their order is the same as the meters.
 * @property {Array<Boolean>} [options.clips=[]] - An Array containing clippings for the level meters. Their order is the same as the meters.
 * @property {Array<Number>} [options.tops=[]] - An Array containing values for top for the level meters. Their order is the same as the meters.
 * @property {Array<Number>} [options.bottoms=[]] - An Array containing values for bottom for the level meters. Their order is the same as the meters.
 */
/* TODO: The following is not ideal cause we need to maintain it according to
  LevelMeters and Meter options. */
export class MultiMeter extends Container {
  static get _options() {
    return Object.assign({}, Container.getOptionTypes(), multimeterOptionTypes);
  }

  static get options() {
    return Object.assign(
      {},
      multimeterOptionDefaults,
      LevelMeter.getDefaultOptions()
    );
  }

  static get static_events() {
    return multimeterStaticEvents;
  }

  initialize(options) {
    super.initialize(options, true);
    /**
     * @member {HTMLDivElement} MultiMeter#element - The main DIV container.
     *   Has class <code>.aux-multimeter</code>.
     */
    this.meters = [];
  }

  draw(O, element) {
    addClass(element, 'aux-multimeter');

    super.draw(O, element);
  }

  redraw() {
    const O = this.options;
    const I = this.invalid;
    const E = this.element;
    const M = this.meters;

    if (I.count) {
      while (M.length > O.count) removeMeter.call(this, M[M.length - 1]);
      while (M.length < O.count) addMeter.call(this, O);
      E.setAttribute(
        'class',
        E.getAttribute('class').replace(/aux-count-[0-9]*/g, '')
      );
      E.setAttribute('class', E.getAttribute('class').replace(/ +/g, ' '));
      addClass(E, 'aux-count-' + O.count);
    }

    if (I.layout || I.count) {
      I.count = I.layout = false;
      removeClass(
        E,
        'aux-vertical',
        'aux-horizontal',
        'aux-left',
        'aux-right',
        'aux-top',
        'aux-bottom'
      );
      switch (O.layout) {
        case 'left':
          addClass(E, 'aux-vertical', 'aux-left');
          break;
        case 'right':
          addClass(E, 'aux-vertical', 'aux-right');
          break;
        case 'top':
          addClass(E, 'aux-horizontal', 'aux-top');
          break;
        case 'bottom':
          addClass(E, 'aux-horizontal', 'aux-bottom');
          break;
        default:
          throw new Error('unsupported layout');
      }
      switch (O.layout) {
        case 'top':
        case 'left':
          for (let i = 0; i < M.length - 1; i++) M[i].set('show_scale', false);
          if (M.length)
            M[this.meters.length - 1].set('show_scale', O.show_scale);
          break;
        case 'bottom':
        case 'right':
          for (let i = 0; i < M.length; i++) M[i].set('show_scale', false);
          if (M.length) M[0].set('show_scale', O.show_scale);
          break;
      }
    }

    if (I.show_value) {
      I.show_value = false;
      toggleClass(E, 'aux-has-values', O.show_value !== false);
    }

    if (I.labels) {
      I.labels = false;
      toggleClass(E, 'aux-has-labels', O.labels !== false);
    }

    super.redraw();
  }
}

/**
 * @member {HTMLDivElement} MultiMeter#label - The {@link Label} widget displaying the meters title.
 */
defineChildWidget(MultiMeter, 'label', {
  create: Label,
  show: false,
  option: 'label',
  default_options: { class: 'aux-label' },
  map_options: { label: 'label' },
  toggle_class: true,
});