Source: syngen/prop/base.js

/**
 * The most basic prop that exists on the soundstage.
 * With its {@link syngen.prop.base.invent|invent} method, implementations can extend and create a hierarchy of prototypes with a variety of sounds and behaviors.
 * Instances _should_ be created and destroyed via {@link syngen.props}.
 * @augments syngen.utility.physical
 * @interface
 */
syngen.prop.base = {
  /**
   * Binaural processor for the prop.
   * @instance
   * @type {syngen.audio.binaural}
   */
  binaural: undefined,
  /**
   * Initializes the prop with `options` and fades in its volume.
   * Derivative props are discouraged from overriding this method.
   * Instead they should define an {@link syngen.prop.base#onConstruct|onConstruct} method.
   * @instance
   * @param {Object} [options={}]
   * @param {GainNode} [options.destination={@link syngen.audio.mixer.master.input|syngen.audio.mixer.master.input()}]
   * @param {Number} [options.radius]
   *   Defaults to the prototype's radius.
   * @param {String} [options.token={@link syngen.utility.uuid|syngen.utility.uuid()}]
   * @param {Number} [options.x=0]
   * @param {Number} [options.y=0]
   * @param {Number} [options.z=0]
   * @see syngen.prop.base#onConstruct
   * @see syngen.props.create
   */
  construct: function ({
    destination = syngen.audio.mixer.master.input(),
    radius = this.radius || 0,
    token = syngen.utility.uuid(),
    x = 0,
    y = 0,
    z = 0,
    ...options
  } = {}) {
    const context = syngen.audio.context()

    this.binaural = syngen.audio.binaural.create()
    this.instantiated = true
    this.output = context.createGain()
    this.radius = radius
    this.token = token
    this.x = x
    this.y = y
    this.z = z

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

    this.binaural.from(this.output)
    this.binaural.to(destination)

    if (this.reverb) {
      this.reverb = syngen.audio.mixer.send.reverb.create()
      this.reverb.from(this.output)
    }

    syngen.utility.physical.decorate(this)

    this.recalculate()
    this.onConstruct(options)

    syngen.audio.ramp.linear(this.output.gain, 1, this.fadeInDuration)

    return this
  },
  /**
   * Prepares the instance for garbage collection and fades out its volume.
   * Derivative props are discouraged from overriding this method.
   * Instead they should define an {@link syngen.prop.base#onConstruct|onDestroy} method.
   * @instance
   * @see syngen.prop.base#onDestroy
   * @see syngen.props.destroy
   */
  destroy: function () {
    syngen.audio.ramp.linear(this.output.gain, syngen.const.zeroGain, this.fadeOutDuration)

    setTimeout(() => {
      this.output.disconnect()
      this.binaural.destroy()

      if (this.reverb) {
        this.reverb.destroy()
      }

      this.onDestroy()
    }, (syngen.const.audioLookaheadTime + this.fadeOutDuration) * 1000)

    return this
  },
  /**
   * The distance of the prop relative to the observer's coordinates.
   * @instance
   * @type {Number}
   */
  distance: undefined,
  /**
   * Duration of fade in when instantiated.
   * @type {Number}
   */
  fadeInDuration: syngen.const.zeroTime,
  /**
   * Duration of fade out when destroyed.
   * @type {Number}
   */
  fadeOutDuration: syngen.const.zeroTime,
  /**
   * Indicates whether the prop has been instantiated.
   * @instance
   * @type {Boolean}
   */
  instantiated: false,
  /**
   * Invents a new prototype with `definition` that inherits the prototype from this prop.
   * @param {Object} definition
   * @returns {syngen.prop.base}
   * @static
   */
  invent: function (definition = {}) {
    if (typeof definition == 'function') {
      definition = definition(this)
    }

    return Object.setPrototypeOf({...definition}, this)
  },
  /**
   * Identifier of the prop type.
   * Instances are discouraged from modifying this.
   * @type {String}
   */
  name: 'base',
  /**
   * Called after a prop is instantiated.
   * Props should define this method to perform setup tasks after being constructed.
   * @instance
   * @see syngen.prop.base#construct
   */
  onConstruct: () => {},
  /**
   * Called before a prop is destroyed.
   * Props should define this method to perform tear tasks before being destroyed.
   * @instance
   * @see syngen.prop.base#destroy
   */
  onDestroy: () => {},
  /**
   * Called when a prop is updated.
   * Props should define this method to perform tasks every frame.
   * @instance
   * @see syngen.prop.base#update
   */
  onUpdate: () => {},
  /**
   * Main output for audio synthesis and playback.
   * This is not connected directly to the main audio destination; rather, it's routed through the binaural and reverb sends.
   * On creation and destruction its gain is ramped to fade in and out.
   * It's not recommended to modify its gain directly.
   * @instance
   * @type {GainNode}
   */
  output: undefined,
  /**
   * Radius of the prop, in meters.
   * @instance
   * @type {Number}
   */
  radius: 0,
  /**
   * Recalculates the prop's relative coordinates and distance, binaural circuit, and reverb send.
   * @instance
   * @see syngen.prop.base#binaural
   * @see syngen.prop.base#distance
   * @see syngen.prop.base#relative
   * @see syngen.prop.base#reverb
   */
  recalculate: function () {
    const positionQuaternion = syngen.position.getQuaternion(),
      positionVector = syngen.position.getVector()

    this.relative = this.vector()
      .subtract(positionVector)
      .subtractRadius(this.radius)
      .rotateQuaternion(positionQuaternion.conjugate())

    this.distance = this.relative.distance()

    this.binaural.update({...this.relative})

    if (this.reverb) {
      this.reverb.update({...this.relative})
    }

    return this
  },
  /**
   * Returns the rectangular prism surrounding the prop.
   * @instance
   * @returns {Object}
   */
  rect: function () {
    return {
      depth: this.radius * 2,
      height: this.radius * 2,
      width: this.radius * 2,
      x: this.x - this.radius,
      y: this.y - this.radius,
      z: this.y - this.radius,
    }
  },
  /**
   * The coordinates of the prop relative to the observer's coordinates and orientation.
   * @instance
   * @type {syngen.utility.vector3d}
   */
  relative: undefined,
  /**
   * Reverb send for the prop.
   * Implementations can disable reverb for certain prototypes by explicitly setting this to `false`.
   * @instance
   * @type {syngen.audio.mixer.send.reverb|Boolean}
   */
  reverb: true,
  /**
   * Universally unique identifier provided during instantiation.
   * @instance
   * @name syngen.prop.base#token
   * @type {String}
   */
   token: undefined,
  /**
   * Called every frame.
   * Derivative props are discouraged from overriding this method.
   * Instead they should define an {@link syngen.prop.base#onConstruct|onUpdate} method.
   * @instance
   * @see syngen.prop.base#onUpdate
   * @see syngen.props.update
   */
  update: function ({
    paused,
  } = {}) {
    this.onUpdate.apply(this, arguments)

    if (paused) {
      return this
    }

    this.updatePhysics()
    this.recalculate()

    return this
  },
}