Source: audio/AudioItem.js

import { BaseAudio } from "./BaseAudio";

/**
 * Represents an audio item that can be loaded, played, and manipulated.
 * Extends the BaseAudio class to provide additional functionality for handling audio data.
 */
export class AudioItem extends BaseAudio {
  /**
   * Creates an instance of AudioItem.
   *
   * @param {string|null} [url=null] - The URL of the audio file to load.
   * @param {Object} [config={}] - Configuration for the audio item.
   */
  constructor(url = null, config = {}) {
    super();

    this.$setConfig(config);
    this.load(url);
  }

  /**
   * Gets the loop property.
   * @returns {boolean} The current value of the loop property.
   */
  get loop() {
    return this._loop;
  }
  set loop(loop) {
    this._loop = loop;
    if (this.$nodesConnected) this._source.loop = loop;
  }

  /**
   * Gets the pitch value.
   * @returns {number} The current pitch value.
   */
  get pitch() {
    return this._pitch;
  }
  set pitch(pitch) {
    this._pitch = pitch;
    if (this.$nodesConnected) this._source.playbackRate.value = pitch;
  }

  /**
   * Asynchronously loads audio data from the provided URL.
   *
   * @param {string} url - The URL of the audio resource to load.
   * @returns {Promise<void>} A promise that resolves when the audio data is loaded.
   */
  async load(url) {
    if (url) {
      this.url = url;
      this._audioResponse = await fetch(url);
      this._update();
    }
  }

  /**
   * Unloads the current instance by disconnecting and clearing the buffer.
   */
  unload() {
    this.disconnect();
    this._buffer = null;
  }

  /**
   * Connects the current instance to the provided audio mixer.
   * If already connected to a different audio mixer, it will first disconnect from the current one.
   *
   * @param {Object} audioMixer - The audio mixer to connect to.
   */
  connect(audioMixer) {
    if (this._audioMixer !== audioMixer) {
      this.disconnect();
      this._audioMixer = audioMixer;
      this._update();
      audioMixer.connect(this);
    }
  }

  /**
   * Disconnects the current instance from the audio mixer.
   * If an audio mixer is connected, it stops the audio and disconnects the instance from the mixer.
   */
  disconnect() {
    if (this._audioMixer) {
      this.stop();
      this._audioMixer.disconnect(this);
      this._audioMixer = null;
    }
  }

  /**
   * Starts playing the audio from a specified time.
   *
   * @param {number} [from=0] - The time in seconds from which to start playing the audio.
   */
  play(from = 0) {
    this.stop();

    this.isPlaying = true;

    const audioMixer = this._audioMixer,
      context = audioMixer.context;

    this.$createNodes(context);
    this.$connectNodes(audioMixer.node);

    if (context) {
      this._startTime = context.currentTime;
      try {
        this.$nodesConnected && this._source.start(context.currentTime, from);
      } catch (e) {}
    }

    this.$setConfig(this);
  }

  /**
   * Stops the audio playback and updates the start time based on the current playback position.
   * If an audio mixer and buffer are present, the start time is recalculated to allow resuming from the same position.
   */
  stop() {
    this.isPlaying = false;

    const audioMixer = this._audioMixer,
      buffer = this._buffer;

    if (audioMixer && buffer)
      this._startTime =
        (audioMixer.context.currentTime - this._startTime) % buffer.duration;

    this.$disconnectNodes();
  }

  /**
   * Resumes playback from the specified start time.
   */
  resume() {
    this.play(this._startTime);
  }

  /**
   * @ignore
   */
  $createNodes(context) {
    const buffer = this._buffer;

    if (context && buffer) {
      this._source = context.createBufferSource();
      this._source.buffer = buffer;
      super.$createNodes(context);
    }
  }

  /**
   * @ignore
   */
  $connectNodes(destination) {
    if (destination && this.$nodesCreated) {
      this._source.connect(this.$gainNode);
      super.$connectNodes(destination);
    }
  }

  /**
   * @ignore
   */
  $disconnectNodes() {
    if (this.$nodesConnected) {
      this._source.stop();
      this._source.disconnect();
      this._source = null;
      super.$disconnectNodes();
    }
  }

  /**
   * @ignore
   */
  $setConfig(config = {}) {
    this.loop = config.loop ?? false;
    this.pitch = config.pitch ?? 1;
    super.$setConfig(config);
  }

  /**
   * @ignore
   */
  async _update() {
    const audioResponse = this._audioResponse,
      audioMixer = this._audioMixer;

    if (audioResponse && audioMixer) {
      const arrayBuffer = await audioResponse.arrayBuffer();
      this._buffer = await audioMixer.context.decodeAudioData(arrayBuffer);
      this.isPlaying && this.play();
    }
  }
}