UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

482 lines (475 loc) 17.4 kB
import { O as Observable, V as Vector3, aD as Quaternion, bI as WebRequest, y as Logger, bR as PrecisionDate } from './index-MZPybX0H.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-jS7MKp73.esm.js.map