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.

633 lines (627 loc) 23.2 kB
import { A as AbstractSound, _ as _AbstractSoundInstance } from './abstractSoundInstance-LF0gFVmD.esm.js'; import { _ as _HasSpatialAudioOptions, a as _StereoAudio, b as _SpatialWebAudio, c as _WebAudioBusAndSoundSubGraph } from './spatialWebAudio-BHkDY-w9.esm.js'; import { a as _CleanUrl, b as _LoadArrayBufferFromUrlAsync, c as _FileExtensionRegex, d as _WebAudioParameterComponent } from './spatialWebAudioUpdaterComponent-jS7MKp73.esm.js'; import './index-MZPybX0H.esm.js'; import './abstractSoundSource-5MTb28Na.esm.js'; import './webAudioBaseSubGraph-BjGMAHX6.esm.js'; /** * Abstract class representing a static sound. * * A static sound has a sound buffer that is loaded into memory all at once. This allows it to have more capabilities * than a streaming sound, such as loop points and playback rate changes, but it also means that the sound must be * fully downloaded and decoded before it can be played, which may take a long time for sounds with long durations. * * To prevent downloading and decoding a sound multiple times, a sound's buffer can be shared with other sounds. * See {@link CreateSoundBufferAsync}, {@link StaticSoundBuffer} and {@link StaticSound.buffer} for more information. * * Static sounds are created by the {@link CreateSoundAsync} function. */ class StaticSound extends AbstractSound { constructor(name, engine, options) { super(name, engine, options); } /** * The amount of time to play the sound for, in seconds. Defaults to `0`. * - If less than or equal to `0`, the sound plays for its full duration. */ get duration() { return this._options.duration; } set duration(value) { this._options.duration = value; } /** * The start of the loop range, in seconds. Defaults to `0`. * - If less than or equal to `0`, the loop starts at the beginning of the sound. */ get loopStart() { return this._options.loopStart; } set loopStart(value) { this._options.loopStart = value; const it = this._instances.values(); for (let instance = it.next(); !instance.done; instance = it.next()) { instance.value.loopStart = value; } } /** * The end of the loop range, in seconds. Defaults to `0`. * - If less than or equal to `0`, the loop plays for the sound's full duration. */ get loopEnd() { return this._options.loopEnd; } set loopEnd(value) { this._options.loopEnd = value; const it = this._instances.values(); for (let instance = it.next(); !instance.done; instance = it.next()) { instance.value.loopEnd = value; } } /** * The pitch of the sound, in cents. Defaults to `0`. * - Gets combined with {@link playbackRate} to determine the final pitch. */ get pitch() { return this._options.pitch; } set pitch(value) { this._options.pitch = value; const it = this._instances.values(); for (let instance = it.next(); !instance.done; instance = it.next()) { instance.value.pitch = value; } } /** * The playback rate of the sound. Defaults to `1`. * - Gets combined with {@link pitch} to determine the final playback rate. */ get playbackRate() { return this._options.playbackRate; } set playbackRate(value) { this._options.playbackRate = value; const it = this._instances.values(); for (let instance = it.next(); !instance.done; instance = it.next()) { instance.value.playbackRate = value; } } /** * Plays the sound. * - Triggers `onEndedObservable` if played for the full duration and the `loop` option is not set. * @param options The options to use when playing the sound. Options set here override the sound's options. */ play(options = {}) { if (this.state === 5 /* SoundState.Paused */) { this.resume(options); return; } options.duration ??= this.duration; options.loop ??= this.loop; options.loopStart ??= this.loopStart; options.loopEnd ??= this.loopEnd; options.startOffset ??= this.startOffset; options.volume ??= 1; options.waitTime ??= 0; const instance = this._createInstance(); this._beforePlay(instance); instance.play(options); this._afterPlay(instance); this._stopExcessInstances(); } /** * Resumes the sound. * - Only options explicitly set here override the paused instance's current options; unset options keep their paused values so playback continues from where it was paused. * @param options The options to use when resuming the sound. */ resume(options) { super.resume(options); } /** * Stops the sound. * - Triggers `onEndedObservable` if the sound is playing. * @param options - The options to use when stopping the sound. */ stop(options = {}) { if (options.waitTime && 0 < options.waitTime) { this._setState(0 /* SoundState.Stopping */); } else { this._setState(1 /* SoundState.Stopped */); } if (!this._instances) { return; } for (const instance of Array.from(this._instances)) { instance.stop(options); } } } let StaticSoundBufferId = 1; /** * Abstract class representing a static sound buffer. * * A static sound buffer is a fully downloaded and decoded array of audio data that is ready to be played. * * Static sound buffers can be reused multiple times by different {@link StaticSound} instances. * * Static sound buffers are created by the {@link CreateSoundBufferAsync} function. * * @see {@link StaticSound.buffer} */ class StaticSoundBuffer { constructor(engine) { /** * The name of the sound buffer. */ this.name = `StaticSoundBuffer #${StaticSoundBufferId++}`; this.engine = engine; } } /** @internal */ class _StaticSoundInstance extends _AbstractSoundInstance { } /** @internal */ class _WebAudioStaticSound extends StaticSound { /** @internal */ constructor(name, engine, options) { super(name, engine, options); this._stereo = null; this._options = { autoplay: options.autoplay ?? false, duration: options.duration ?? 0, loop: options.loop ?? false, loopEnd: options.loopEnd ?? 0, loopStart: options.loopStart ?? 0, maxInstances: options.maxInstances ?? Infinity, pitch: options.pitch ?? 0, playbackRate: options.playbackRate ?? 1, startOffset: options.startOffset ?? 0, }; this._subGraph = new _WebAudioStaticSound._SubGraph(this); } /** @internal */ async _initAsync(source, options) { this._audioContext = this.engine._audioContext; if (source instanceof _WebAudioStaticSoundBuffer) { this._buffer = source; } else if (typeof source === "string" || Array.isArray(source) || source instanceof ArrayBuffer || source instanceof AudioBuffer) { this._buffer = (await this.engine.createSoundBufferAsync(source, options)); } if (options.outBus) { this.outBus = options.outBus; } else if (options.outBusAutoDefault !== false) { await this.engine.isReadyPromise; this.outBus = this.engine.defaultMainBus; } await this._subGraph.initAsync(options); if (_HasSpatialAudioOptions(options)) { this._initSpatialProperty(); } if (options.autoplay) { this.play(); } this.engine._addSound(this); } /** @internal */ get buffer() { return this._buffer; } /** @internal */ get _inNode() { return this._subGraph._inNode; } /** @internal */ get _outNode() { return this._subGraph._outNode; } /** @internal */ get stereo() { return this._stereo ?? (this._stereo = new _StereoAudio(this._subGraph)); } /** @internal */ async cloneAsync(options = null) { const clone = await this.engine.createSoundAsync(this.name, options?.cloneBuffer ? this.buffer.clone() : this.buffer, this._options); clone.outBus = options?.outBus ? options.outBus : this.outBus; return clone; } /** @internal */ dispose() { super.dispose(); this._stereo = null; this._subGraph.dispose(); this.engine._removeSound(this); } /** @internal */ getClassName() { return "_WebAudioStaticSound"; } _createInstance() { return new _WebAudioStaticSoundInstance(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; } _createSpatialProperty(autoUpdate, minUpdateTime) { return new _SpatialWebAudio(this._subGraph, autoUpdate, minUpdateTime); } _getOptions() { return this._options; } } _WebAudioStaticSound._SubGraph = class extends _WebAudioBusAndSoundSubGraph { get _downstreamNodes() { return this._owner._downstreamNodes ?? null; } get _upstreamNodes() { return this._owner._upstreamNodes ?? null; } }; /** @internal */ class _WebAudioStaticSoundBuffer extends StaticSoundBuffer { /** @internal */ constructor(engine) { super(engine); } async _initAsync(source, options) { if (source instanceof AudioBuffer) { this._audioBuffer = source; } else if (typeof source === "string") { await this._initFromUrlAsync(source); } else if (Array.isArray(source)) { await this._initFromUrlsAsync(source, options.skipCodecCheck ?? false); } else if (source instanceof ArrayBuffer) { await this._initFromArrayBufferAsync(source); } } /** @internal */ get channelCount() { return this._audioBuffer.numberOfChannels; } /** @internal */ get duration() { return this._audioBuffer.duration; } /** @internal */ get length() { return this._audioBuffer.length; } /** @internal */ get sampleRate() { return this._audioBuffer.sampleRate; } /** @internal */ clone(options = null) { const audioBuffer = new AudioBuffer({ length: this._audioBuffer.length, numberOfChannels: this._audioBuffer.numberOfChannels, sampleRate: this._audioBuffer.sampleRate, }); for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) { audioBuffer.copyToChannel(this._audioBuffer.getChannelData(i), i); } const buffer = new _WebAudioStaticSoundBuffer(this.engine); buffer._audioBuffer = audioBuffer; buffer.name = options?.name ? options.name : this.name; return buffer; } async _initFromArrayBufferAsync(arrayBuffer) { this._audioBuffer = await this.engine._audioContext.decodeAudioData(arrayBuffer); } async _initFromUrlAsync(url) { url = _CleanUrl(url); const { data } = await _LoadArrayBufferFromUrlAsync(url); await this._initFromArrayBufferAsync(data); } async _initFromUrlsAsync(urls, skipCodecCheck) { for (const url of urls) { if (skipCodecCheck) { // eslint-disable-next-line no-await-in-loop await this._initFromUrlAsync(url); } else { const matches = url.match(_FileExtensionRegex); const format = matches?.at(1); if (format && this.engine.isFormatValid(format)) { try { // eslint-disable-next-line no-await-in-loop await this._initFromUrlAsync(url); } catch { if (format && 0 < format.length) { this.engine.flagInvalidFormat(format); } } } } if (this._audioBuffer) { break; } } } } /** @internal */ class _WebAudioStaticSoundInstance extends _StaticSoundInstance { constructor(sound, options) { super(sound); this._enginePlayTime = 0; this._enginePauseTime = 0; this._isConnected = false; this._pitch = null; this._playbackRate = null; this._sourceNode = null; this._onEnded = () => { this._enginePlayTime = 0; if (this._state !== 5 /* SoundState.Paused */) { this.onEndedObservable.notifyObservers(this); } this._deinitSourceNode(); }; 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._options = options; this._volumeNode = new GainNode(sound._audioContext); this._initSourceNode(); } /** @internal */ dispose() { super.dispose(); this._pitch?.dispose(); this._playbackRate?.dispose(); this.stop(); this._deinitSourceNode(); this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); } /** @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) { // Stop source node without sending `onEndedObservable` so instance's `dispose` function is not called. const sourceNode = this._sourceNode; this._deinitSourceNode(); sourceNode?.stop(); this._state = 1 /* SoundState.Stopped */; } // Seeking sets an absolute position that is fully described by `startOffset`, so any playback time accumulated // across previous pause/resume cycles must be cleared. Otherwise the stale value leaks into the `currentTime` // getter and the next `pause()` calculation, offsetting them by the total paused duration. this._enginePauseTime = 0; this._options.startOffset = value; if (restart) { this.play(); } } get _outNode() { return this._volumeNode; } /** @internal */ set loop(value) { this._options.loop = value; if (this._sourceNode) { this._sourceNode.loop = value; } } /** @internal */ set loopStart(value) { this._options.loopStart = value; if (this._sourceNode) { this._sourceNode.loopStart = value; } } /** @internal */ set loopEnd(value) { this._options.loopEnd = value; if (this._sourceNode) { this._sourceNode.loopEnd = value; } } /** @internal */ set pitch(value) { this._pitch?.setTargetValue(value); } /** @internal */ set playbackRate(value) { this._playbackRate?.setTargetValue(value); } /** @internal */ get startTime() { if (this._state === 1 /* SoundState.Stopped */) { return 0; } return this._enginePlayTime; } /** @internal */ getClassName() { return "_WebAudioStaticSoundInstance"; } /** @internal */ play(options = {}) { if (this._state === 3 /* SoundState.Started */) { return; } if (options.duration !== undefined) { this._options.duration = options.duration; } if (options.loop !== undefined) { this._options.loop = options.loop; } if (options.loopStart !== undefined) { this._options.loopStart = options.loopStart; } if (options.loopEnd !== undefined) { this._options.loopEnd = options.loopEnd; } if (options.startOffset !== undefined) { this._options.startOffset = options.startOffset; } let startOffset = this._options.startOffset; if (this._state === 5 /* SoundState.Paused */) { const duration = this._sound.buffer.duration; const elapsed = this._enginePauseTime; if (this._options.loop) { // Web Audio only loops over `[loopStart, loopEnd)` when they describe a valid sub-range; otherwise it // loops over the whole buffer. Mirror that here (matching the `AudioBufferSourceNode` playback algorithm) // to compute the correct resume position inside the loop region. let loopStart = 0; let loopEnd = duration; if (this._options.loopStart >= 0 && this._options.loopEnd > 0 && this._options.loopStart < this._options.loopEnd) { loopStart = this._options.loopStart; loopEnd = Math.min(this._options.loopEnd, duration); } // Time spent before the playback position first reaches `loopEnd` and wraps back to `loopStart`. const timeToLoopEnd = loopEnd - startOffset; if (elapsed < timeToLoopEnd) { startOffset += elapsed; } else { startOffset = loopStart + ((elapsed - timeToLoopEnd) % (loopEnd - loopStart)); } } else { startOffset += elapsed; startOffset %= duration; } } this._enginePlayTime = this.engine.currentTime + (options.waitTime ?? 0); // Only override the volume when explicitly provided so resuming a paused instance keeps its current volume. if (options.volume !== undefined) { this._volumeNode.gain.value = options.volume; } this._initSourceNode(); if (this.engine.state === "running") { this._setState(3 /* SoundState.Started */); this._sourceNode?.start(this._enginePlayTime, startOffset, this._options.duration > 0 ? this._options.duration : undefined); } else if (this._options.loop) { this._setState(2 /* SoundState.Starting */); this.engine.stateChangedObservable.add(this._onEngineStateChanged); } } /** @internal */ pause() { if (this._state !== 3 /* SoundState.Started */ && this._state !== 2 /* SoundState.Starting */) { return; } const wasStarted = this._state === 3 /* SoundState.Started */; this._setState(5 /* SoundState.Paused */); this._enginePauseTime += this.engine.currentTime - this._enginePlayTime; if (wasStarted) { this._sourceNode?.stop(); } else { this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); } this._deinitSourceNode(); } /** @internal */ resume(options = {}) { if (this._state === 5 /* SoundState.Paused */) { this.play(options); } } /** @internal */ stop(options = {}) { if (this._state === 1 /* SoundState.Stopped */) { return; } if (this._state === 3 /* SoundState.Started */) { const engineStopTime = this.engine.currentTime + (options.waitTime ?? 0); this._sourceNode?.stop(engineStopTime); } if (options.waitTime === undefined || options.waitTime <= 0) { this._setState(1 /* SoundState.Stopped */); this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); } } _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 _WebAudioStaticSound && node._inNode) { this._outNode?.connect(node._inNode); this._isConnected = true; } return true; } _disconnect(node) { const disconnected = super._disconnect(node); if (!disconnected) { return false; } if (node instanceof _WebAudioStaticSound && node._inNode) { this._outNode?.disconnect(node._inNode); this._isConnected = false; } return true; } _deinitSourceNode() { if (!this._sourceNode) { return; } if (this._isConnected && !this._disconnect(this._sound)) { throw new Error("Disconnect failed"); } this._sourceNode.disconnect(this._volumeNode); this._sourceNode.removeEventListener("ended", this._onEnded); this._sourceNode = null; } _initSourceNode() { if (!this._sourceNode) { this._sourceNode = new AudioBufferSourceNode(this._sound._audioContext, { buffer: this._sound.buffer._audioBuffer }); this._sourceNode.addEventListener("ended", this._onEnded, { once: true }); this._sourceNode.connect(this._volumeNode); if (!this._connect(this._sound)) { throw new Error("Connect failed"); } this._pitch = new _WebAudioParameterComponent(this.engine, this._sourceNode.detune); this._playbackRate = new _WebAudioParameterComponent(this.engine, this._sourceNode.playbackRate); } const node = this._sourceNode; node.detune.value = this._sound.pitch; node.loop = this._options.loop; node.loopEnd = this._options.loopEnd; node.loopStart = this._options.loopStart; node.playbackRate.value = this._sound.playbackRate; } } export { _WebAudioStaticSound, _WebAudioStaticSoundBuffer }; //# sourceMappingURL=webAudioStaticSound-CGPqNRqx.esm.js.map