UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

379 lines 13.7 kB
import { Logger } from "../../Misc/logger.js"; import { Tools } from "../../Misc/tools.js"; import { StreamingSound } from "../abstractAudio/streamingSound.js"; import { _StreamingSoundInstance } from "../abstractAudio/streamingSoundInstance.js"; import { _HasSpatialAudioOptions } from "../abstractAudio/subProperties/abstractSpatialAudio.js"; import { _StereoAudio } from "../abstractAudio/subProperties/stereoAudio.js"; import { _CleanUrl } from "../audioUtils.js"; import { _WebAudioBusAndSoundSubGraph } from "./subNodes/webAudioBusAndSoundSubGraph.js"; import { _SpatialWebAudio } from "./subProperties/spatialWebAudio.js"; /** @internal */ export class _WebAudioStreamingSound extends StreamingSound { /** @internal */ constructor(name, engine, options) { super(name, engine); this._spatial = null; this._spatialAutoUpdate = true; this._spatialMinUpdateTime = 0; this._stereo = null; if (typeof options.spatialAutoUpdate === "boolean") { this._spatialAutoUpdate = options.spatialAutoUpdate; } if (typeof options.spatialMinUpdateTime === "number") { this._spatialMinUpdateTime = options.spatialMinUpdateTime; } this._options = { autoplay: options.autoplay ?? false, loop: options.loop ?? false, maxInstances: options.maxInstances ?? Infinity, preloadCount: options.preloadCount ?? 1, startOffset: options.startOffset ?? 0, }; this._subGraph = new _WebAudioStreamingSound._SubGraph(this); } /** @internal */ async _init(source, options) { const audioContext = this.engine._audioContext; if (!(audioContext instanceof AudioContext)) { throw new Error("Unsupported audio context type."); } this._audioContext = audioContext; this._source = source; if (options.outBus) { this.outBus = options.outBus; } else { await this.engine.isReadyPromise; this.outBus = this.engine.defaultMainBus; } await this._subGraph.init(options); if (_HasSpatialAudioOptions(options)) { this._initSpatialProperty(); } if (this.preloadCount) { await this.preloadInstancesAsync(this.preloadCount); } if (options.autoplay) { this.play(options); } this.engine._addNode(this); } /** @internal */ get _inNode() { return this._subGraph._inNode; } /** @internal */ get _outNode() { return this._subGraph._outNode; } /** @internal */ get spatial() { if (this._spatial) { return this._spatial; } return this._initSpatialProperty(); } /** @internal */ get stereo() { return this._stereo ?? (this._stereo = new _StereoAudio(this._subGraph)); } /** @internal */ dispose() { super.dispose(); this._spatial = null; this._stereo = null; this._subGraph.dispose(); this.engine._removeNode(this); } /** @internal */ getClassName() { return "_WebAudioStreamingSound"; } _createInstance() { return new _WebAudioStreamingSoundInstance(this, this._options); } _connect(node) { const connected = super._connect(node); if (!connected) { return false; } // If the wrapped node is not available now, it will be connected later by the subgraph. if (node._inNode) { this._outNode?.connect(node._inNode); } return true; } _disconnect(node) { const disconnected = super._disconnect(node); if (!disconnected) { return false; } if (node._inNode) { this._outNode?.disconnect(node._inNode); } return true; } _initSpatialProperty() { if (!this._spatial) { this._spatial = new _SpatialWebAudio(this._subGraph, this._spatialAutoUpdate, this._spatialMinUpdateTime); } return this._spatial; } } _WebAudioStreamingSound._SubGraph = class extends _WebAudioBusAndSoundSubGraph { get _downstreamNodes() { return this._owner._downstreamNodes ?? null; } get _upstreamNodes() { return this._owner._upstreamNodes ?? null; } }; /** @internal */ class _WebAudioStreamingSoundInstance extends _StreamingSoundInstance { constructor(sound, options) { super(sound); this._currentTimeChangedWhilePaused = false; this._enginePlayTime = Infinity; this._enginePauseTime = 0; this._isReady = false; this._isReadyPromise = new Promise((resolve, reject) => { this._resolveIsReadyPromise = resolve; this._rejectIsReadyPromise = reject; }); this._onCanPlayThrough = () => { this._isReady = true; this._resolveIsReadyPromise(this._mediaElement); this.onReadyObservable.notifyObservers(this); }; this._onEnded = () => { this.onEndedObservable.notifyObservers(this); this.dispose(); }; this._onError = (reason) => { this._setState(4 /* SoundState.FailedToStart */); this.onErrorObservable.notifyObservers(reason); this._rejectIsReadyPromise(reason); this.dispose(); }; this._onEngineStateChanged = () => { if (this.engine.state !== "running") { return; } if (this._options.loop && this.state === 2 /* SoundState.Starting */) { this.play(); } this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); }; this._onUserGesture = () => { this.play(); }; this._options = options; this._volumeNode = new GainNode(sound._audioContext); if (typeof sound._source === "string") { this._initFromUrl(sound._source); } else if (Array.isArray(sound._source)) { this._initFromUrls(sound._source); } else if (sound._source instanceof HTMLMediaElement) { this._initFromMediaElement(sound._source); } } /** @internal */ get currentTime() { if (this._state === 1 /* SoundState.Stopped */) { return 0; } const timeSinceLastStart = this._state === 5 /* SoundState.Paused */ ? 0 : this.engine.currentTime - this._enginePlayTime; return this._enginePauseTime + timeSinceLastStart + this._options.startOffset; } set currentTime(value) { const restart = this._state === 2 /* SoundState.Starting */ || this._state === 3 /* SoundState.Started */; if (restart) { this._mediaElement.pause(); this._setState(1 /* SoundState.Stopped */); } this._options.startOffset = value; if (restart) { this.play({ startOffset: value }); } else if (this._state === 5 /* SoundState.Paused */) { this._currentTimeChangedWhilePaused = true; } } get _outNode() { return this._volumeNode; } /** @internal */ get startTime() { if (this._state === 1 /* SoundState.Stopped */) { return 0; } return this._enginePlayTime; } /** @internal */ dispose() { super.dispose(); this.stop(); this._sourceNode?.disconnect(this._volumeNode); this._sourceNode = null; this._mediaElement.removeEventListener("error", this._onError); this._mediaElement.removeEventListener("ended", this._onEnded); this._mediaElement.removeEventListener("canplaythrough", this._onCanPlayThrough); for (const source of Array.from(this._mediaElement.children)) { this._mediaElement.removeChild(source); } this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); this.engine.userGestureObservable.removeCallback(this._onUserGesture); } /** @internal */ play(options = {}) { if (this._state === 3 /* SoundState.Started */) { return; } if (options.loop !== undefined) { this._options.loop = options.loop; } this._mediaElement.loop = this._options.loop; let startOffset = options.startOffset; if (this._currentTimeChangedWhilePaused) { startOffset = this._options.startOffset; this._currentTimeChangedWhilePaused = false; } else if (this._state === 5 /* SoundState.Paused */) { startOffset = this.currentTime + this._options.startOffset; } if (startOffset && startOffset > 0) { this._mediaElement.currentTime = startOffset; } this._volumeNode.gain.value = options.volume ?? 1; this._play(); } /** @internal */ pause() { if (this._state !== 2 /* SoundState.Starting */ && this._state !== 3 /* SoundState.Started */) { return; } this._setState(5 /* SoundState.Paused */); this._enginePauseTime += this.engine.currentTime - this._enginePlayTime; this._mediaElement.pause(); } /** @internal */ resume() { if (this._state === 5 /* SoundState.Paused */) { this.play(); } else if (this._currentTimeChangedWhilePaused) { this.play(); } } /** @internal */ stop() { if (this._state === 1 /* SoundState.Stopped */) { return; } this._stop(); } /** @internal */ getClassName() { return "_WebAudioStreamingSoundInstance"; } _connect(node) { const connected = super._connect(node); if (!connected) { return false; } // If the wrapped node is not available now, it will be connected later by the sound's subgraph. if (node instanceof _WebAudioStreamingSound && node._inNode) { this._outNode?.connect(node._inNode); } return true; } _disconnect(node) { const disconnected = super._disconnect(node); if (!disconnected) { return false; } if (node instanceof _WebAudioStreamingSound && node._inNode) { this._outNode?.disconnect(node._inNode); } return true; } _initFromMediaElement(mediaElement) { Tools.SetCorsBehavior(mediaElement.currentSrc, mediaElement); mediaElement.controls = false; mediaElement.loop = this._options.loop; mediaElement.preload = "auto"; mediaElement.addEventListener("canplaythrough", this._onCanPlayThrough, { once: true }); mediaElement.addEventListener("ended", this._onEnded, { once: true }); mediaElement.addEventListener("error", this._onError, { once: true }); mediaElement.load(); this._sourceNode = new MediaElementAudioSourceNode(this._sound._audioContext, { mediaElement: mediaElement }); this._sourceNode.connect(this._volumeNode); if (!this._connect(this._sound)) { throw new Error("Connect failed"); } this._mediaElement = mediaElement; } _initFromUrl(url) { const audio = new Audio(_CleanUrl(url)); this._initFromMediaElement(audio); } _initFromUrls(urls) { const audio = new Audio(); for (const url of urls) { const source = document.createElement("source"); source.src = _CleanUrl(url); audio.appendChild(source); } this._initFromMediaElement(audio); } _play() { this._setState(2 /* SoundState.Starting */); if (!this._isReady) { this._playWhenReady(); return; } if (this._state !== 2 /* SoundState.Starting */) { return; } if (this.engine.state === "running") { const result = this._mediaElement.play(); this._enginePlayTime = this.engine.currentTime; this._setState(3 /* SoundState.Started */); // It's possible that the play() method fails on Safari, even if the audio engine's state is "running". // This occurs when the audio context is paused by the system and resumed automatically by the audio engine // without a user interaction (e.g. when the Vision Pro exits and reenters immersive mode). result.catch(() => { this._setState(4 /* SoundState.FailedToStart */); if (this._options.loop) { this.engine.userGestureObservable.addOnce(this._onUserGesture); } }); } else if (this._options.loop) { this.engine.stateChangedObservable.add(this._onEngineStateChanged); } else { this.stop(); this._setState(4 /* SoundState.FailedToStart */); } } _playWhenReady() { this._isReadyPromise .then(() => { this._play(); }) .catch(() => { Logger.Error("Streaming sound instance failed to play"); this._setState(4 /* SoundState.FailedToStart */); }); } _stop() { this._mediaElement.pause(); this._setState(1 /* SoundState.Stopped */); this._onEnded(); this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); } } //# sourceMappingURL=webAudioStreamingSound.js.map