Source: engines/Metronome.js

import AudioTimeEngine from '../core/AudioTimeEngine';

function optOrDef(opt, def) {
  if(opt !== undefined)
    return opt;

  return def;
}

/**
 * Metronome audio engine. It extends Time Engine as a transported interface.
 * [example]{@link https://rawgit.com/wavesjs/waves-audio/master/examples/metronome/index.html}
 *
 * @extends AudioTimeEngine
 * @example
 * import * as audio from 'waves-audio';
 * const scheduler = audio.getScheduler();
 * const metronome = new audio.Metronome({period: 0.333});
 *
 * scheduler.add(metronome);
 *
 * @param {Object} [options={}] - Default options
 * @param {Number} [options.period=1] - Metronome period
 * @param {Number} [options.clickFreq=600] - Metronome click frequency
 * @param {Number} [options.clickAttack=0.002] - Metronome click attack time
 * @param {Number} [options.clickRelease=0.098] - Metronome click release time
 * @param {Number} [options.gain=1] - Gain
 */
class Metronome extends AudioTimeEngine {
  constructor(options = {}) {
    super(options.audioContext);

    /**
     * Metronome period
     * @type {Number}
     * @private
     */
    this.__period = optOrDef(options.period, 1);

    /**
     * Metronome click frequency
     *
     * @type {Number}
     * @memberof Metronome
     * @name clickFreq
     * @instance
     */
    this.clickFreq = optOrDef(options.clickFreq, 600);

    /**
     * Metronome click attack time
     *
     * @type {Number}
     * @memberof Metronome
     * @name clickAttack
     * @instance
     */
    this.clickAttack = optOrDef(options.clickAttack, 0.002);

    /**
     * Metronome click release time
     *
     * @type {Number}
     * @memberof Metronome
     * @name clickRelease
     * @instance
     */
    this.clickRelease = optOrDef(options.clickRelease, 0.098);

    this.__lastTime = 0;
    this.__phase = 0;

    this.__gainNode = this.audioContext.createGain();
    this.__gainNode.gain.value = optOrDef(options.gain, 1);

    this.outputNode = this.__gainNode;
  }

  // TimeEngine method (scheduled interface)
  advanceTime(time) {
    this.trigger(time);
    this.__lastTime = time;
    return time + this.__period;
  }

  // TimeEngine method (transported interface)
  syncPosition(time, position, speed) {
    if (this.__period > 0) {
      var nextPosition = (Math.floor(position / this.__period) + this.__phase) * this.__period;

      if (speed > 0 && nextPosition < position)
        nextPosition += this.__period;
      else if (speed < 0 && nextPosition > position)
        nextPosition -= this.__period;

      return nextPosition;
    }

    return Infinity * speed;
  }

  // TimeEngine method (transported interface)
  advancePosition(time, position, speed) {
    this.trigger(time);

    if (speed < 0)
      return position - this.__period;

    return position + this.__period;
  }

  /**
   * Trigger metronome click
   * @param {Number} time metronome click synthesis audio time
   */
  trigger(time) {
    const audioContext = this.audioContext;
    const clickAttack = this.clickAttack;
    const clickRelease = this.clickRelease;

    const env = audioContext.createGain();
    env.gain.value = 0.0;
    env.gain.setValueAtTime(0, time);
    env.gain.linearRampToValueAtTime(1.0, time + clickAttack);
    env.gain.exponentialRampToValueAtTime(0.0000001, time + clickAttack + clickRelease);
    env.gain.setValueAtTime(0, time);
    env.connect(this.outputNode);

    const osc = audioContext.createOscillator();
    osc.frequency.value = this.clickFreq;
    osc.start(time);
    osc.stop(time + clickAttack + clickRelease);
    osc.connect(env);
  }

  /**
   * linear gain factor
   *
   * @type {Number}
   * @name gain
   * @memberof Metronome
   * @instance
   */
  set gain(value) {
    this.__gainNode.gain.value = value;
  }

  get gain() {
    return this.__gainNode.gain.value;
  }

  /**
   * metronome period
   *
   * @type {Number}
   * @name period
   * @memberof Metronome
   * @instance
   */
  set period(period) {
    this.__period = period;

    const master = this.master;

    if (master) {
      if (master.resetEngineTime)
        master.resetEngineTime(this, this.__lastTime + period);
      else if (master.resetEnginePosition)
        master.resetEnginePosition(this);
    }
  }

  get period() {
    return this.__period;
  }

  /**
   * Set phase parameter (available only when 'transported'), should be
   * between [0, 1[
   *
   * @type {Number}
   * @name phase
   * @memberof Metronome
   * @instance
   */
  set phase(phase) {
    this.__phase = phase - Math.floor(phase);

    const master = this.master;

    if (master && master.resetEnginePosition !== undefined)
      master.resetEnginePosition(this);
  }

  get phase() {
    return this.__phase;
  }
}

export default Metronome;