Source: syngen/audio/mixer/send/reverb.js

/**
 * Provides an interface for routing audio to the global reverb auxiliary send.
 * Importantly, it models physical space to add pre-delay and attenuate based on distance.
 * @interface
 * @see syngen.audio.mixer.auxiliary.reverb
 * @todo Document private members
 */
syngen.audio.mixer.send.reverb = {}

/**
 * Creates a reverb send.
 * @param {Object} [options]
 * @param {Number} [options.x=0]
 * @param {Number} [options.y=0]
 * @param {Number} [options.z=0]
 * @returns {syngen.audio.mixer.send.reverb}
 * @static
 */
syngen.audio.mixer.send.reverb.create = function (options) {
  return Object.create(this.prototype).construct(options)
}

syngen.audio.mixer.send.reverb.prototype = {
  /**
   * Initializes the instance.
   * @instance
   * @private
   */
  construct: function (options) {
    const context = syngen.audio.context()

    this.delay = context.createDelay()
    this.input = context.createGain()
    this.relative = syngen.utility.vector3d.create()
    this.send = syngen.audio.mixer.auxiliary.reverb.createSend()

    this.send.gain.value = syngen.const.zeroGain

    this.onSendActivate = this.onSendActivate.bind(this)
    syngen.audio.mixer.auxiliary.reverb.on('activate', this.onSendActivate)

    this.onSendDeactivate = this.onSendDeactivate.bind(this)
    syngen.audio.mixer.auxiliary.reverb.on('deactivate', this.onSendDeactivate)

    if (syngen.audio.mixer.auxiliary.reverb.isActive()) {
      this.onSendActivate()
    } else {
      this.onSendDeactivate()
    }

    this.update(options)

    return this
  },
  /**
   * Prepares the instance for garbage collection.
   * Immediately disconnects from all inputs and outputs.
   * @instance
   */
  destroy: function () {
    syngen.audio.mixer.auxiliary.reverb.off('activate', this.onSendActivate)
    syngen.audio.mixer.auxiliary.reverb.off('deactivate', this.onSendDeactivate)
    this.send.disconnect()
    return this
  },
  /**
   * Connects `input` to this.
   * @instance
   */
  from: function (input) {
    input.connect(this.input)
    return this
  },
  /**
   * Handles whenever the auxiliary send activates.
   * @instance
   * @listens syngen.audio.mixer.auxiliary.reverb#event:activate
   * @private
   */
  onSendActivate: function () {
    this.update(this.relative)
    this.input.connect(this.delay)
    this.delay.connect(this.send)
    return this
  },
  /**
   * Handles whenever the auxiliary send deactivates.
   * @instance
   * @listens syngen.audio.mixer.auxiliary.reverb#event:activate
   * @private
   */
  onSendDeactivate: function () {
    this.input.disconnect()
    this.delay.disconnect()
    return this
  },
  /**
   * Updates the circuit with `options` relative to an observer at the origin.
   * @instance
   * @param {Object} [options={}]
   * @param {Number} [options.x=0]
   * @param {Number} [options.y=0]
   * @param {Number} [options.z=0]
   * @todo Assess whether it'd be better to simply pass the distance
   */
  update: function ({
    x = 0,
    y = 0,
    z = 0,
  } = {}) {
    this.relative.set({
      x,
      y,
      z,
    })

    if (!syngen.audio.mixer.auxiliary.reverb.isActive()) {
      return this
    }

    const distance = this.relative.distance(),
      power = Math.min(syngen.utility.distanceToPower(distance), syngen.utility.fromDb(-1))

    const delayTime = syngen.utility.clamp(distance / syngen.const.speedOfSound, syngen.const.zeroTime, 1),
      gain = syngen.utility.clamp((power ** 0.75) * (1 - (power ** 0.75)), syngen.const.zeroGain, 1)

    syngen.audio.ramp.set(this.delay.delayTime, delayTime)
    syngen.audio.ramp.set(this.send.gain, gain)

    return this
  },
}