Source: audio/BaseAudio.js

import { normalizeAudioValue } from "./internal/normalizeAudioValue";

/**
 * The BaseAudio class provides methods and properties to control audio playback,
 * including volume, panning, reverb, and filtering effects. It manages the creation,
 * connection, and disconnection of audio nodes in a Web Audio API context.
 */
export class BaseAudio {
  constructor() {
    this._volume = 1;
    this._muted = false;
    this._pan = 0;
    this._reverbDelayTime = 0;
    this._reverbFeedbackGain = 0;
    this.filters = [];
  }

  /**
   * Gets the volume.
   * @returns {number} The current volume.
   */
  get volume() {
    return this._volume;
  }
  set volume(volume) {
    this._volume = normalizeAudioValue(volume, 1, 0);
    this.$updateGain();
  }

  /**
   * Gets muted state.
   * @returns {boolean} Whether the audio output is muted.
   */
  get muted() {
    return this._muted;
  }
  set muted(muted) {
    this._muted = muted;
    this.$updateGain();
  }

  /**
   * Gets the ordered audio filter list.
   * @returns {Array<Object>} The current filter list.
   */
  get filters() {
    return this._filters;
  }
  set filters(filters) {
    const currentFilters = this._filters;

    if (this.$nodesConnected && this.$filterInputNode) {
      this.$filterInputNode.disconnect();
      currentFilters?.forEach((filter) => filter?.disconnect?.());
    }

    currentFilters?.forEach((filter) => filter?.$setOnChange?.(null));

    this._filters = new Proxy(filters ?? [], {
      deleteProperty: (target, property) => {
        target[property]?.$setOnChange?.(null);
        delete target[property];
        this.$reconnectFilterChain();
        return true;
      },
      set: (target, property, value) => {
        target[property]?.$setOnChange?.(null);
        target[property] = value;
        value?.$setOnChange?.(() => this.$reconnectFilterChain());
        this.$reconnectFilterChain();
        return true;
      },
    });

    this._filters.forEach((filter) => filter?.$setOnChange?.(() => this.$reconnectFilterChain()));

    this.$reconnectFilterChain();
  }

  /**
   * Gets the value of the pan property.
   * @returns {number} The current value of the pan.
   */
  get pan() {
    return this._pan;
  }
  set pan(pan) {
    this._pan = normalizeAudioValue(pan, 0, -1, 1);
    if (this.$nodesConnected) {
      this.$panNode.pan.value = this._pan;
    }
  }

  /**
   * Gets the reverb delay time.
   * @returns {number} The current reverb delay time.
   */
  get reverbDelayTime() {
    return this._reverbDelayTime;
  }
  set reverbDelayTime(delayTime) {
    this._reverbDelayTime = normalizeAudioValue(delayTime, 0, 0);
    if (this.$nodesConnected) {
      this.$delayNode.delayTime.value = this._reverbDelayTime;
    }
  }

  /**
   * Gets the reverb feedback gain value.
   * @returns {number} The current reverb feedback gain.
   */
  get reverbFeedbackGain() {
    return this._reverbFeedbackGain;
  }
  set reverbFeedbackGain(feedbackGain) {
    this._reverbFeedbackGain = normalizeAudioValue(feedbackGain, 0, 0);
    if (this.$nodesConnected) {
      this.$feedbackGainNode.gain.value = this._reverbFeedbackGain;
    }
  }

  /**
   * @ignore
   */
  $createNodes(context) {
    if (context) {
      this.$context = context;
      this.$gainNode = context.createGain();
      this.$panNode = context.createStereoPanner();
      this.$delayNode = context.createDelay();
      this.$feedbackGainNode = context.createGain();

      this.$nodesCreated = true;
      this.$updateGain();
    }
  }

  /**
   * @ignore
   */
  $connectNodes(destination) {
    if (destination && this.$nodesCreated) {
      this.$destinationNode = destination;
      this.$gainNode.connect(this.$panNode);
      this.$panNode.connect(this.$delayNode);
      this.$delayNode.connect(this.$feedbackGainNode);
      this.$feedbackGainNode.connect(this.$delayNode);
      this.$filterInputNode = this.$delayNode;
      this.$connectFilterChain(this.$filterInputNode, destination);

      this.$nodesConnected = true;
    }
  }

  /**
   * @ignore
   */
  $connectFilterChain(inputNode, destination) {
    if (inputNode && destination) {
      let previousNode = inputNode;

      this._filters.forEach((filter) => {
        if (filter && filter.on !== false) {
          if (filter.$context !== this.$context) {
            filter.destruct?.();
            filter.$context = this.$context;
          }

          if (!filter.input || !filter.output) {
            filter.createNodes(this.$context);
          }

          filter.updateNodes?.();

          if (filter.input && filter.output) {
            previousNode.connect(filter.input);
            previousNode = filter.output;
          }
        }
      });

      previousNode.connect(destination);
    }
  }

  /**
   * @ignore
   */
  $disconnectFilterChain() {
    this._filters.forEach((filter) => filter?.disconnect?.());
  }

  /**
   * @ignore
   */
  $reconnectFilterChain() {
    if (this.$nodesConnected && this.$filterInputNode && this.$destinationNode) {
      this.$filterInputNode.disconnect();
      this.$disconnectFilterChain();
      this.$connectFilterChain(this.$filterInputNode, this.$destinationNode);
    }
  }

  /**
   * @ignore
   */
  $disconnectNodes() {
    if (this.$nodesConnected) {
      this.$disconnectFilterChain();
      this.$gainNode.disconnect();
      this.$panNode.disconnect();
      this.$delayNode.disconnect();
      this.$feedbackGainNode.disconnect();

      this.$gainNode = this.$panNode = this.$delayNode = this.$feedbackGainNode = this.$filterInputNode = this.$destinationNode = null;

      this.$nodesConnected = this.$nodesCreated = false;
    }
  }

  /**
   * @ignore
   */
  $setConfig(config) {
    if (config.filters && config.filters !== this._filters) {
      this.filters = config.filters;
    }

    this.volume = config.volume ?? 1;
    this.muted = config.muted ?? false;
    this.pan = config.pan ?? 0;
    this.reverbDelayTime = config.reverbDelayTime ?? 0;
    this.reverbFeedbackGain = config.reverbFeedbackGain ?? 0;
  }

  /**
   * @ignore
   */
  $updateGain() {
    if (this.$gainNode) {
      this.$gainNode.gain.value = this._muted ? 0 : this._volume;
    }
  }
}