@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
JavaScript
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