import { ay as Observable, as as Vector3, Q as Quaternion, W as WebRequest, L as Logger, br as PrecisionDate } from './index-BvJBnIUK.esm.js';

var AudioNodeType;
(function (AudioNodeType) {
    AudioNodeType[AudioNodeType["HAS_INPUTS"] = 1] = "HAS_INPUTS";
    AudioNodeType[AudioNodeType["HAS_OUTPUTS"] = 2] = "HAS_OUTPUTS";
    AudioNodeType[AudioNodeType["HAS_INPUTS_AND_OUTPUTS"] = 3] = "HAS_INPUTS_AND_OUTPUTS";
})(AudioNodeType || (AudioNodeType = {}));
/**
 * Abstract class for an audio node.
 *
 * An audio node is a processing unit that can receive audio data from an upstream node and/or send audio data to a
 * downstream node.
 *
 * Nodes can be connected to other nodes to create an audio graph. The audio graph represents the flow of audio data.
 *
 * There are 3 types of audio nodes:
 * 1. Input: Receives audio data from upstream nodes.
 * 2. Output: Sends audio data to downstream nodes.
 * 3. Input/Output: Receives audio data from upstream nodes and sends audio data to downstream nodes.
 */
class AbstractAudioNode {
    constructor(engine, nodeType) {
        /**
         * Observable for when the audio node is disposed.
         */
        this.onDisposeObservable = new Observable();
        this.engine = engine;
        if (nodeType & 1 /* AudioNodeType.HAS_INPUTS */) {
            this._upstreamNodes = new Set();
        }
        if (nodeType & 2 /* AudioNodeType.HAS_OUTPUTS */) {
            this._downstreamNodes = new Set();
        }
    }
    /**
     * Releases associated resources.
     * - Triggers `onDisposeObservable`.
     * @see {@link onDisposeObservable}
     */
    dispose() {
        if (this._downstreamNodes) {
            for (const node of Array.from(this._downstreamNodes)) {
                if (!this._disconnect(node)) {
                    throw new Error("Disconnect failed");
                }
            }
            this._downstreamNodes.clear();
        }
        if (this._upstreamNodes) {
            for (const node of Array.from(this._upstreamNodes)) {
                if (!node._disconnect(this)) {
                    throw new Error("Disconnect failed");
                }
            }
            this._upstreamNodes.clear();
        }
        this.onDisposeObservable.notifyObservers(this);
        this.onDisposeObservable.clear();
    }
    /**
     * Connect to a downstream audio input node.
     * @param node - The downstream audio input node to connect
     * @returns `true` if the node is successfully connected; otherwise `false`
     */
    _connect(node) {
        if (!this._downstreamNodes) {
            return false;
        }
        if (this._downstreamNodes.has(node)) {
            return false;
        }
        if (!node._onConnect(this)) {
            return false;
        }
        this._downstreamNodes.add(node);
        return true;
    }
    /**
     * Disconnects a downstream audio input node.
     * @param node - The downstream audio input node to disconnect
     * @returns `true` if the node is successfully disconnected; otherwise `false`
     */
    _disconnect(node) {
        if (!this._downstreamNodes) {
            return false;
        }
        if (!this._downstreamNodes.delete(node)) {
            return false;
        }
        return node._onDisconnect(this);
    }
    /**
     * Called when an upstream audio output node is connecting.
     * @param node - The connecting upstream audio node
     * @returns `true` if the node is successfully connected; otherwise `false`
     */
    _onConnect(node) {
        if (!this._upstreamNodes) {
            return false;
        }
        if (this._upstreamNodes.has(node)) {
            return false;
        }
        this._upstreamNodes.add(node);
        return true;
    }
    /**
     * Called when an upstream audio output node disconnects.
     * @param node - The disconnecting upstream audio node
     * @returns `true` if node is sucessfully disconnected; otherwise `false`
     */
    _onDisconnect(node) {
        return this._upstreamNodes?.delete(node) ?? false;
    }
}
/**
 * Abstract class for a named audio node.
 */
class AbstractNamedAudioNode extends AbstractAudioNode {
    constructor(name, engine, nodeType) {
        super(engine, nodeType);
        /**
         * Observable for when the audio node is renamed.
         */
        this.onNameChangedObservable = new Observable();
        this._name = name;
    }
    /**
     * The name of the audio node.
     * - Triggers `onNameChangedObservable` when changed.
     * @see {@link onNameChangedObservable}
     */
    get name() {
        return this._name;
    }
    set name(newName) {
        if (this._name === newName) {
            return;
        }
        const oldName = this._name;
        this._name = newName;
        this.onNameChangedObservable.notifyObservers({ newName, oldName, node: this });
    }
    dispose() {
        super.dispose();
        this.onNameChangedObservable.clear();
    }
}

/**
 * Provides a common interface for attaching an audio listener or source to a specific entity, ensuring only one entity
 * is attached at a time.
 * @internal
 */
class _SpatialAudioAttacherComponent {
    /** @internal */
    constructor(spatialAudioNode) {
        /** @internal */
        this._attachmentType = 3 /* SpatialAudioAttachmentType.PositionAndRotation */;
        this._position = new Vector3();
        this._rotationQuaternion = new Quaternion();
        this._sceneNode = null;
        this._useBoundingBox = false;
        /**
         * Releases associated resources.
         */
        this.dispose = () => {
            this.detach();
        };
        this._spatialAudioNode = spatialAudioNode;
    }
    /**
     * Returns `true` if attached to a scene node; otherwise returns `false`.
     */
    get isAttached() {
        return this._sceneNode !== null;
    }
    /**
     * The scene node this attacher is currently attached to, or `null` if not attached.
     */
    get sceneNode() {
        return this._sceneNode;
    }
    /**
     * Whether the attacher is using the scene node's bounding box for positioning.
     */
    get useBoundingBox() {
        return this._useBoundingBox;
    }
    /**
     * Which components (position, rotation, or both) of the scene node's world transform drive the spatial audio.
     */
    get attachmentType() {
        return this._attachmentType;
    }
    /**
     * Attaches to a scene node.
     *
     * Detaches automatically before attaching to the given scene node.
     * If `sceneNode` is `null` it is the same as calling `detach()`.
     *
     * @param sceneNode The scene node to attach to, or `null` to detach.
     * @param useBoundingBox Whether to use the scene node's bounding box for positioning. Defaults to `false`.
     * @param attachmentType Whether to attach to the scene node's position and/or rotation. Defaults to `PositionAndRotation`.
     */
    attach(sceneNode, useBoundingBox, attachmentType) {
        if (this._sceneNode === sceneNode) {
            return;
        }
        this.detach();
        if (!sceneNode) {
            return;
        }
        this._attachmentType = attachmentType;
        this._sceneNode = sceneNode;
        this._sceneNode.onDisposeObservable.add(this.dispose);
        this._useBoundingBox = useBoundingBox;
    }
    /**
     * Detaches from the scene node if attached.
     */
    detach() {
        this._sceneNode?.onDisposeObservable.removeCallback(this.dispose);
        this._sceneNode = null;
    }
    /**
     * Updates the position and rotation of the associated audio engine object in the audio rendering graph.
     *
     * This is called automatically by default and only needs to be called manually if automatic updates are disabled.
     */
    update() {
        const updatesPosition = !!(this._attachmentType & 1 /* SpatialAudioAttachmentType.Position */);
        if (updatesPosition) {
            if (this._useBoundingBox && this._sceneNode.getBoundingInfo) {
                this._position.copyFrom(this._sceneNode.getBoundingInfo().boundingBox.centerWorld);
            }
            else {
                this._sceneNode?.getWorldMatrix().getTranslationToRef(this._position);
            }
            this._spatialAudioNode.position.copyFrom(this._position);
            this._spatialAudioNode._updatePosition();
        }
        if (this._attachmentType & 2 /* SpatialAudioAttachmentType.Rotation */) {
            this._sceneNode?.getWorldMatrix().decompose(undefined, this._rotationQuaternion);
            this._spatialAudioNode.rotationQuaternion.copyFrom(this._rotationQuaternion);
            this._spatialAudioNode._updateRotation();
        }
        if (!updatesPosition && "panningEnabled" in this._spatialAudioNode && !this._spatialAudioNode.panningEnabled) {
            this._spatialAudioNode._updatePosition();
        }
    }
}

const _FileExtensionRegex = new RegExp("\\.(\\w{3,4})($|\\?)");
const CurveLength = 100;
const TmpLineValues = new Float32Array([0, 0]);
let TmpCurveValues = null;
let ExpCurve = null;
let LogCurve = null;
/**
 * @returns A Float32Array representing an exponential ramp from (0, 0) to (1, 1).
 */
function GetExpCurve() {
    if (!ExpCurve) {
        ExpCurve = new Float32Array(CurveLength);
        const increment = 1 / (CurveLength - 1);
        let x = increment;
        for (let i = 1; i < CurveLength; i++) {
            ExpCurve[i] = Math.exp(-11.512925464970227 * (1 - x));
            x += increment;
        }
    }
    return ExpCurve;
}
/**
 * @returns A Float32Array representing a logarithmic ramp from (0, 0) to (1, 1).
 */
function GetLogCurve() {
    if (!LogCurve) {
        LogCurve = new Float32Array(CurveLength);
        const increment = 1 / CurveLength;
        let x = increment;
        for (let i = 0; i < CurveLength; i++) {
            LogCurve[i] = 1 + Math.log10(x) / Math.log10(CurveLength);
            x += increment;
        }
    }
    return LogCurve;
}
/** @internal */
function _GetAudioParamCurveValues(shape, from, to) {
    if (!TmpCurveValues) {
        TmpCurveValues = new Float32Array(CurveLength);
    }
    let normalizedCurve;
    if (shape === "linear" /* AudioParameterRampShape.Linear */) {
        TmpLineValues[0] = from;
        TmpLineValues[1] = to;
        return TmpLineValues;
    }
    else if (shape === "exponential" /* AudioParameterRampShape.Exponential */) {
        normalizedCurve = GetExpCurve();
    }
    else if (shape === "logarithmic" /* AudioParameterRampShape.Logarithmic */) {
        normalizedCurve = GetLogCurve();
    }
    else {
        throw new Error(`Unknown ramp shape: ${shape}`);
    }
    const direction = Math.sign(to - from);
    const range = Math.abs(to - from);
    if (direction === 1) {
        for (let i = 0; i < normalizedCurve.length; i++) {
            TmpCurveValues[i] = from + range * normalizedCurve[i];
        }
    }
    else {
        let j = CurveLength - 1;
        for (let i = 0; i < normalizedCurve.length; i++, j--) {
            TmpCurveValues[i] = from - range * (1 - normalizedCurve[j]);
        }
    }
    return TmpCurveValues;
}
/** @internal */
function _CleanUrl(url) {
    return url.replace(/#/gm, "%23");
}
/**
 * Applies `WebRequest.CustomRequestModifiers` URL transformations to the given URL for use with a streaming
 * `<audio>` element. Unlike `_LoadArrayBufferFromUrlAsync`, this does NOT download the audio content —
 * the browser streams it natively. If custom request headers are present they cannot be forwarded to the
 * `<audio>` element (which has no API for custom headers), so a warning is logged and the headers are ignored.
 * @param url - The URL to process (should already be cleaned via `_CleanUrl` if needed).
 * @returns The (possibly rewritten) URL with URL modifiers applied.
 * @internal
 */
function _GetUrlForStreaming(url) {
    const { url: modifiedUrl, headers } = WebRequest._CollectCustomizations(url);
    if (Object.keys(headers).length > 0) {
        Logger.Warn("WebAudioStreamingSound: Custom request headers cannot be applied to a streaming <audio> element and will be ignored. " +
            "To use custom headers with audio, switch to a static (non-streaming) sound which fetches the file up-front.");
    }
    return modifiedUrl;
}
/**
 * Loads an `ArrayBuffer` from the given URL using `WebRequest.FetchAsync`, so that
 * `WebRequest.CustomRequestHeaders` and `WebRequest.CustomRequestModifiers` are respected for audio network
 * requests just like for any other Babylon.js network call.
 * Uses the Fetch API when available, falling back to XMLHttpRequest otherwise.
 * @param url - The URL to load from (should already be cleaned via `_CleanUrl` if needed).
 * @returns A promise that resolves with the loaded `ArrayBuffer` and the HTTP `Content-Type` response header value.
 * @internal
 */
async function _LoadArrayBufferFromUrlAsync(url) {
    const response = await WebRequest.FetchAsync(url);
    if (!response.ok) {
        throw new Error(`HTTP ${response.status} loading '${url}': ${response.statusText}`);
    }
    const data = await response.arrayBuffer();
    const contentType = response.headers.get("Content-Type") ?? "";
    return { data, contentType };
}

/**
 * Minimum duration in seconds for a ramp to be considered valid.
 *
 * If the duration is less than this value, the value will be set immediately instead of being ramped smoothly since
 * there is no perceptual difference for such short durations, so a ramp is not needed.
 */
const MinRampDuration = 0.000001;
let Warn = true;
/** @internal */
class _WebAudioParameterComponent {
    /** @internal */
    constructor(engine, param) {
        this._rampEndTime = 0;
        this._engine = engine;
        this._param = param;
        this._targetValue = param.value;
    }
    /** @internal */
    get isRamping() {
        return this._engine.currentTime < this._rampEndTime;
    }
    /** @internal */
    get targetValue() {
        return this._targetValue;
    }
    set targetValue(value) {
        this.setTargetValue(value);
    }
    /** @internal */
    get value() {
        return this._param.value;
    }
    /** @internal */
    dispose() {
        this._param = null;
        this._engine = null;
    }
    /**
     * Sets the target value of the audio parameter with an optional ramping duration and shape.
     *
     * @internal
     */
    setTargetValue(value, options = null) {
        if (!Number.isFinite(value)) {
            Logger.Warn(`Attempted to set audio parameter to non-finite value: ${value}`);
            return;
        }
        this._param.cancelScheduledValues(0);
        const shape = typeof options?.shape === "string" ? options.shape : "linear" /* AudioParameterRampShape.Linear */;
        const startTime = this._engine.currentTime;
        if (shape === "none" /* AudioParameterRampShape.None */) {
            this._param.value = this._targetValue = value;
            this._rampEndTime = startTime;
            return;
        }
        let duration = typeof options?.duration === "number" ? Math.max(options.duration, this._engine.parameterRampDuration) : this._engine.parameterRampDuration;
        this._targetValue = value;
        if ((duration = Math.max(this._engine.parameterRampDuration, duration)) < MinRampDuration) {
            this._param.setValueAtTime(value, startTime);
            return;
        }
        try {
            this._param.setValueCurveAtTime(_GetAudioParamCurveValues(shape, Number.isFinite(this._param.value) ? this._param.value : 0, value), startTime, duration);
            this._rampEndTime = startTime + duration;
        }
        catch (e) {
            if (Warn) {
                Logger.Warn(`Audio parameter ramping failed: ${e.message}`);
                Warn = false;
            }
        }
    }
}

/** @internal */
class _SpatialWebAudioUpdaterComponent {
    /** @internal */
    constructor(parent, autoUpdate, minUpdateTime) {
        this._autoUpdate = true;
        this._lastUpdateTime = 0;
        /**
         * The minimum time in seconds between spatial audio updates. Defaults to `0`.
         * @internal
         */
        this.minUpdateTime = 0;
        if (!autoUpdate) {
            return;
        }
        this.minUpdateTime = minUpdateTime;
        const update = () => {
            if (!this._autoUpdate) {
                return;
            }
            let skipUpdate = false;
            if (0 < this.minUpdateTime) {
                const now = PrecisionDate.Now;
                if (this._lastUpdateTime && now - this._lastUpdateTime < this.minUpdateTime * 1000) {
                    skipUpdate = true;
                }
                this._lastUpdateTime = now;
            }
            if (!skipUpdate) {
                parent.update();
            }
            requestAnimationFrame(update);
        };
        requestAnimationFrame(update);
    }
    /** @internal */
    dispose() {
        this._autoUpdate = false;
    }
}

export { AbstractAudioNode as A, _GetUrlForStreaming as _, _CleanUrl as a, _LoadArrayBufferFromUrlAsync as b, _FileExtensionRegex as c, _WebAudioParameterComponent as d, _SpatialAudioAttacherComponent as e, _SpatialWebAudioUpdaterComponent as f, AbstractNamedAudioNode as g };
//# sourceMappingURL=spatialWebAudioUpdaterComponent-qDEnhmP0.esm.js.map
