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.

1,255 lines (1,250 loc) 79.1 kB
import { a as EngineStore, A as AbstractEngine, ao as Scene, aZ as SceneComponentConstants, V as Vector3, M as Matrix, bR as PrecisionDate, O as Observable, y as Logger, bb as _RetryWithInterval, ba as _WarnImport, aY as Tools, aQ as GetBlobBufferSource, bo as unregisterGLTFExtension, bp as registerGLTFExtension } from './index-MZPybX0H.esm.js'; import { _ as _HasSpatialAudioOptions, d as _SpatialAudioDefaults } from './spatialWebAudio-BHkDY-w9.esm.js'; import { _WebAudioSoundSource } from './webAudioSoundSource-BpDGX_V9.esm.js'; import { _WebAudioStaticSound } from './webAudioStaticSound-CGPqNRqx.esm.js'; import { _WebAudioStreamingSound } from './webAudioStreamingSound-DVbUdywu.esm.js'; import { A as ArrayItem, b as GLTFLoader } from './glTFLoader.pure-BaIedO7s.esm.js'; import './webAudioBaseSubGraph-BjGMAHX6.esm.js'; import './spatialWebAudioUpdaterComponent-jS7MKp73.esm.js'; import './abstractSoundSource-5MTb28Na.esm.js'; import './abstractSoundInstance-LF0gFVmD.esm.js'; import './morphTargetManager-CK7B3gaT.esm.js'; import './bone.pure-CjsCww39.esm.js'; import './skeleton-D7Sw58cv.esm.js'; import './assetContainer-Bm0vsreJ.esm.js'; import './objectModelMapping-De5EKNEZ.esm.js'; import './spotLight.pure-CRdZC6Ci.esm.js'; /** * Composed of a frame, and an action function */ class AnimationEvent { /** * Initializes the animation event * @param frame The frame for which the event is triggered * @param action The event to perform when triggered * @param onlyOnce Specifies if the event should be triggered only once */ constructor( /** The frame for which the event is triggered **/ frame, /** The event to perform when triggered **/ action, /** Specifies if the event should be triggered only once**/ onlyOnce) { this.frame = frame; this.action = action; this.onlyOnce = onlyOnce; /** * Specifies if the animation event is done */ this.isDone = false; } /** @internal */ _clone() { return new AnimationEvent(this.frame, this.action, this.onlyOnce); } } /** * It could be useful to isolate your music & sounds on several tracks to better manage volume on a grouped instance of sounds. * It will be also used in a future release to apply effects on a specific track. * @see https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic#using-sound-tracks */ class SoundTrack { /** * Creates a new sound track. * @see https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic#using-sound-tracks * @param scene Define the scene the sound track belongs to * @param options */ constructor(scene, options = {}) { /** * The unique identifier of the sound track in the scene. */ this.id = -1; this._isInitialized = false; scene = scene || EngineStore.LastCreatedScene; if (!scene) { return; } this._scene = scene; this.soundCollection = []; this._options = options; if (!this._options.mainTrack && this._scene.soundTracks) { this._scene.soundTracks.push(this); this.id = this._scene.soundTracks.length - 1; } } _initializeSoundTrackAudioGraph() { if (AbstractEngine.audioEngine?.canUseWebAudio && AbstractEngine.audioEngine.audioContext) { this._outputAudioNode = AbstractEngine.audioEngine.audioContext.createGain(); this._outputAudioNode.connect(AbstractEngine.audioEngine.masterGain); if (this._options) { if (this._options.volume) { this._outputAudioNode.gain.value = this._options.volume; } } this._isInitialized = true; } } /** * Release the sound track and its associated resources */ dispose() { if (AbstractEngine.audioEngine && AbstractEngine.audioEngine.canUseWebAudio) { if (this._connectedAnalyser) { this._connectedAnalyser.stopDebugCanvas(); } while (this.soundCollection.length) { this.soundCollection[0].dispose(); } if (this._outputAudioNode) { this._outputAudioNode.disconnect(); } this._outputAudioNode = null; } } /** * Adds a sound to this sound track * @param sound define the sound to add * @ignoreNaming */ addSound(sound) { if (!this._isInitialized) { this._initializeSoundTrackAudioGraph(); } if (AbstractEngine.audioEngine?.canUseWebAudio && this._outputAudioNode) { sound.connectToSoundTrackAudioNode(this._outputAudioNode); } if (sound.soundTrackId !== undefined) { if (sound.soundTrackId === -1) { this._scene.mainSoundTrack.removeSound(sound); } else if (this._scene.soundTracks) { this._scene.soundTracks[sound.soundTrackId].removeSound(sound); } } this.soundCollection.push(sound); sound.soundTrackId = this.id; } /** * Removes a sound to this sound track * @param sound define the sound to remove * @ignoreNaming */ removeSound(sound) { const index = this.soundCollection.indexOf(sound); if (index !== -1) { this.soundCollection.splice(index, 1); } } /** * Set a global volume for the full sound track. * @param newVolume Define the new volume of the sound track */ setVolume(newVolume) { if (AbstractEngine.audioEngine?.canUseWebAudio && this._outputAudioNode) { this._outputAudioNode.gain.value = newVolume; } } /** * Switch the panning model to HRTF: * Renders a stereo output of higher quality than equalpower — it uses a convolution with measured impulse responses from human subjects. * @see https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic#creating-a-spatial-3d-sound */ switchPanningModelToHRTF() { if (AbstractEngine.audioEngine?.canUseWebAudio) { for (let i = 0; i < this.soundCollection.length; i++) { this.soundCollection[i].switchPanningModelToHRTF(); } } } /** * Switch the panning model to Equal Power: * Represents the equal-power panning algorithm, generally regarded as simple and efficient. equalpower is the default value. * @see https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic#creating-a-spatial-3d-sound */ switchPanningModelToEqualPower() { if (AbstractEngine.audioEngine?.canUseWebAudio) { for (let i = 0; i < this.soundCollection.length; i++) { this.soundCollection[i].switchPanningModelToEqualPower(); } } } /** * Connect the sound track to an audio analyser allowing some amazing * synchronization between the sounds/music and your visualization (VuMeter for instance). * @see https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic#using-the-analyser * @param analyser The analyser to connect to the engine */ connectToAnalyser(analyser) { if (this._connectedAnalyser) { this._connectedAnalyser.stopDebugCanvas(); } this._connectedAnalyser = analyser; if (AbstractEngine.audioEngine?.canUseWebAudio && this._outputAudioNode) { this._outputAudioNode.disconnect(); this._connectedAnalyser.connectAudioNodes(this._outputAudioNode, AbstractEngine.audioEngine.masterGain); } } } /** This file must only contain pure code and pure imports */ /** * Defines the sound scene component responsible to manage any sounds * in a given scene. * @deprecated please use AudioEngineV2 instead */ class AudioSceneComponent { /** * Gets whether audio is enabled or not. * Please use related enable/disable method to switch state. */ get audioEnabled() { return this._audioEnabled; } /** * Gets whether audio is outputting to headphone or not. * Please use the according Switch methods to change output. */ get headphone() { return this._headphone; } /** * Creates a new instance of the component for the given scene * @param scene Defines the scene to register the component in */ constructor(scene) { /** * The component name helpful to identify the component in the list of scene components. */ this.name = SceneComponentConstants.NAME_AUDIO; this._audioEnabled = true; this._headphone = false; /** * Gets or sets a refresh rate when using 3D audio positioning */ this.audioPositioningRefreshRate = 500; /** * Gets or Sets a custom listener position for all sounds in the scene * By default, this is the position of the first active camera */ this.audioListenerPositionProvider = null; /** * Gets or Sets a custom listener rotation for all sounds in the scene * By default, this is the rotation of the first active camera */ this.audioListenerRotationProvider = null; this._cachedCameraDirection = new Vector3(); this._cachedCameraPosition = new Vector3(); this._lastCheck = 0; this._invertMatrixTemp = new Matrix(); this._cameraDirectionTemp = new Vector3(); scene = scene || EngineStore.LastCreatedScene; if (!scene) { return; } this.scene = scene; scene.soundTracks = []; scene.sounds = []; } /** * Registers the component in a given scene */ register() { this.scene._afterRenderStage.registerStep(SceneComponentConstants.STEP_AFTERRENDER_AUDIO, this, this._afterRender); } /** * Rebuilds the elements related to this component in case of * context lost for instance. */ rebuild() { // Nothing to do here. (Not rendering related) } /** * Serializes the component data to the specified json object * @param serializationObject The object to serialize to */ serialize(serializationObject) { serializationObject.sounds = []; if (this.scene.soundTracks) { for (let index = 0; index < this.scene.soundTracks.length; index++) { const soundtrack = this.scene.soundTracks[index]; for (let soundId = 0; soundId < soundtrack.soundCollection.length; soundId++) { serializationObject.sounds.push(soundtrack.soundCollection[soundId].serialize()); } } } } /** * Adds all the elements from the container to the scene * @param container the container holding the elements */ addFromContainer(container) { if (!container.sounds) { return; } for (const sound of container.sounds) { sound.play(); sound.autoplay = true; this.scene.mainSoundTrack.addSound(sound); } } /** * Removes all the elements in the container from the scene * @param container contains the elements to remove * @param dispose if the removed element should be disposed (default: false) */ removeFromContainer(container, dispose = false) { if (!container.sounds) { return; } for (const sound of container.sounds) { sound.stop(); sound.autoplay = false; this.scene.mainSoundTrack.removeSound(sound); if (dispose) { sound.dispose(); } } } /** * Disposes the component and the associated resources. */ dispose() { const scene = this.scene; if (scene._mainSoundTrack) { scene.mainSoundTrack.dispose(); } if (scene.soundTracks) { for (let scIndex = 0; scIndex < scene.soundTracks.length; scIndex++) { scene.soundTracks[scIndex].dispose(); } } } /** * Disables audio in the associated scene. */ disableAudio() { const scene = this.scene; this._audioEnabled = false; if (AbstractEngine.audioEngine && AbstractEngine.audioEngine.audioContext) { // eslint-disable-next-line @typescript-eslint/no-floating-promises AbstractEngine.audioEngine.audioContext.suspend(); } let i; for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) { scene.mainSoundTrack.soundCollection[i].pause(); } if (scene.soundTracks) { for (i = 0; i < scene.soundTracks.length; i++) { for (let j = 0; j < scene.soundTracks[i].soundCollection.length; j++) { scene.soundTracks[i].soundCollection[j].pause(); } } } } /** * Enables audio in the associated scene. */ enableAudio() { const scene = this.scene; this._audioEnabled = true; if (AbstractEngine.audioEngine && AbstractEngine.audioEngine.audioContext) { // eslint-disable-next-line @typescript-eslint/no-floating-promises AbstractEngine.audioEngine.audioContext.resume(); } let i; for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) { if (scene.mainSoundTrack.soundCollection[i].isPaused) { scene.mainSoundTrack.soundCollection[i].play(); } } if (scene.soundTracks) { for (i = 0; i < scene.soundTracks.length; i++) { for (let j = 0; j < scene.soundTracks[i].soundCollection.length; j++) { if (scene.soundTracks[i].soundCollection[j].isPaused) { scene.soundTracks[i].soundCollection[j].play(); } } } } } /** * Switch audio to headphone output. */ switchAudioModeForHeadphones() { const scene = this.scene; this._headphone = true; scene.mainSoundTrack.switchPanningModelToHRTF(); if (scene.soundTracks) { for (let i = 0; i < scene.soundTracks.length; i++) { scene.soundTracks[i].switchPanningModelToHRTF(); } } } /** * Switch audio to normal speakers. */ switchAudioModeForNormalSpeakers() { const scene = this.scene; this._headphone = false; scene.mainSoundTrack.switchPanningModelToEqualPower(); if (scene.soundTracks) { for (let i = 0; i < scene.soundTracks.length; i++) { scene.soundTracks[i].switchPanningModelToEqualPower(); } } } _afterRender() { const now = PrecisionDate.Now; if (this._lastCheck && now - this._lastCheck < this.audioPositioningRefreshRate) { return; } this._lastCheck = now; const scene = this.scene; if (!this._audioEnabled || !scene._mainSoundTrack || !scene.soundTracks || (scene._mainSoundTrack.soundCollection.length === 0 && scene.soundTracks.length === 1)) { return; } const audioEngine = AbstractEngine.audioEngine; if (!audioEngine) { return; } if (audioEngine.audioContext) { let listeningCamera = scene.activeCamera; if (scene.activeCameras && scene.activeCameras.length > 0) { listeningCamera = scene.activeCameras[0]; } // A custom listener position provider was set // Use the users provided position instead of camera's if (this.audioListenerPositionProvider) { const position = this.audioListenerPositionProvider(); // Set the listener position audioEngine.audioContext.listener.setPosition(position.x || 0, position.y || 0, position.z || 0); // Check if there is a listening camera } else if (listeningCamera) { // Set the listener position to the listening camera global position if (!this._cachedCameraPosition.equals(listeningCamera.globalPosition)) { this._cachedCameraPosition.copyFrom(listeningCamera.globalPosition); audioEngine.audioContext.listener.setPosition(listeningCamera.globalPosition.x, listeningCamera.globalPosition.y, listeningCamera.globalPosition.z); } } // Otherwise set the listener position to 0, 0 ,0 else { // Set the listener position audioEngine.audioContext.listener.setPosition(0, 0, 0); } // A custom listener rotation provider was set // Use the users provided rotation instead of camera's if (this.audioListenerRotationProvider) { const rotation = this.audioListenerRotationProvider(); audioEngine.audioContext.listener.setOrientation(rotation.x || 0, rotation.y || 0, rotation.z || 0, 0, 1, 0); // Check if there is a listening camera } else if (listeningCamera) { // for VR cameras if (listeningCamera.rigCameras && listeningCamera.rigCameras.length > 0) { listeningCamera = listeningCamera.rigCameras[0]; } listeningCamera.getViewMatrix().invertToRef(this._invertMatrixTemp); Vector3.TransformNormalToRef(AudioSceneComponent._CameraDirection, this._invertMatrixTemp, this._cameraDirectionTemp); this._cameraDirectionTemp.normalize(); // To avoid some errors on GearVR if (!isNaN(this._cameraDirectionTemp.x) && !isNaN(this._cameraDirectionTemp.y) && !isNaN(this._cameraDirectionTemp.z)) { if (!this._cachedCameraDirection.equals(this._cameraDirectionTemp)) { this._cachedCameraDirection.copyFrom(this._cameraDirectionTemp); audioEngine.audioContext.listener.setOrientation(this._cameraDirectionTemp.x, this._cameraDirectionTemp.y, this._cameraDirectionTemp.z, 0, 1, 0); } } } // Otherwise set the listener rotation to 0, 0 ,0 else { // Set the listener position audioEngine.audioContext.listener.setOrientation(0, 0, 0, 0, 1, 0); } let i; for (i = 0; i < scene.mainSoundTrack.soundCollection.length; i++) { const sound = scene.mainSoundTrack.soundCollection[i]; if (sound.useCustomAttenuation) { sound.updateDistanceFromListener(); } } if (scene.soundTracks) { for (i = 0; i < scene.soundTracks.length; i++) { for (let j = 0; j < scene.soundTracks[i].soundCollection.length; j++) { const sound = scene.soundTracks[i].soundCollection[j]; if (sound.useCustomAttenuation) { sound.updateDistanceFromListener(); } } } } } } } AudioSceneComponent._CameraDirection = /*#__PURE__*/ new Vector3(0, 0, -1); let _Registered$1 = false; /** * Register side effects for audioSceneComponent. * Safe to call multiple times; only the first call has an effect. * @param soundClass The Sound class to register the component for */ function RegisterAudioSceneComponent(soundClass) { if (_Registered$1) { return; } _Registered$1 = true; Object.defineProperty(Scene.prototype, "mainSoundTrack", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } if (!this._mainSoundTrack) { this._mainSoundTrack = new SoundTrack(this, { mainTrack: true }); } return this._mainSoundTrack; }, enumerable: true, configurable: true, }); Scene.prototype.getSoundByName = function (name) { let index; for (index = 0; index < this.mainSoundTrack.soundCollection.length; index++) { if (this.mainSoundTrack.soundCollection[index].name === name) { return this.mainSoundTrack.soundCollection[index]; } } if (this.soundTracks) { for (let sdIndex = 0; sdIndex < this.soundTracks.length; sdIndex++) { for (index = 0; index < this.soundTracks[sdIndex].soundCollection.length; index++) { if (this.soundTracks[sdIndex].soundCollection[index].name === name) { return this.soundTracks[sdIndex].soundCollection[index]; } } } } return null; }; Object.defineProperty(Scene.prototype, "audioEnabled", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } return compo.audioEnabled; }, set: function (value) { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } if (value) { compo.enableAudio(); } else { compo.disableAudio(); } }, enumerable: true, configurable: true, }); Object.defineProperty(Scene.prototype, "headphone", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } return compo.headphone; }, set: function (value) { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } if (value) { compo.switchAudioModeForHeadphones(); } else { compo.switchAudioModeForNormalSpeakers(); } }, enumerable: true, configurable: true, }); Object.defineProperty(Scene.prototype, "audioListenerPositionProvider", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } return compo.audioListenerPositionProvider; }, set: function (value) { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } if (value && typeof value !== "function") { throw new Error("The value passed to [Scene.audioListenerPositionProvider] must be a function that returns a Vector3"); } else { compo.audioListenerPositionProvider = value; } }, enumerable: true, configurable: true, }); Object.defineProperty(Scene.prototype, "audioListenerRotationProvider", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } return compo.audioListenerRotationProvider; }, set: function (value) { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } if (value && typeof value !== "function") { throw new Error("The value passed to [Scene.audioListenerRotationProvider] must be a function that returns a Vector3"); } else { compo.audioListenerRotationProvider = value; } }, enumerable: true, configurable: true, }); Object.defineProperty(Scene.prototype, "audioPositioningRefreshRate", { get: function () { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } return compo.audioPositioningRefreshRate; }, set: function (value) { let compo = this._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(this); this._addComponent(compo); } compo.audioPositioningRefreshRate = value; }, enumerable: true, configurable: true, }); soundClass._SceneComponentInitialization = (scene) => { let compo = scene._getComponent(SceneComponentConstants.NAME_AUDIO); if (!compo) { compo = new AudioSceneComponent(scene); scene._addComponent(compo); } }; } /** This file must only contain pure code and pure imports */ const TmpRampOptions = { duration: 0, shape: "linear" /* AudioParameterRampShape.Linear */, }; const TmpPlayOptions = { duration: 0, startOffset: 0, waitTime: 0, }; const TmpStopOptions = { waitTime: 0, }; function D2r(degrees) { return (degrees * Math.PI) / 180; } function R2d(radians) { return (radians * 180) / Math.PI; } /** * Defines a sound that can be played in the application. * The sound can either be an ambient track or a simple sound played in reaction to a user action. * @see https://doc.babylonjs.com/legacy/audio */ class Sound { /** * The name of the sound in the scene. */ get name() { return this._soundV2.name; } set name(value) { this._soundV2.name = value; } /** * Does the sound autoplay once loaded. */ get autoplay() { return this._soundV2 instanceof _WebAudioSoundSource ? true : this._optionsV2.autoplay; } set autoplay(value) { this._optionsV2.autoplay = value; } /** * Does the sound loop after it finishes playing once. */ get loop() { return this._soundV2 instanceof _WebAudioSoundSource ? true : this._soundV2.loop; } set loop(value) { if (this._soundV2 instanceof _WebAudioSoundSource) { return; } if (this._soundV2) { this._soundV2.loop = value; } } /** * Is this sound currently played. */ get isPlaying() { return this._soundV2 instanceof _WebAudioSoundSource ? true : this._soundV2?.state === 3 /* SoundState.Started */ || (!this.isReady() && this._optionsV2.autoplay); } /** * Is this sound currently paused. */ get isPaused() { return this._soundV2 instanceof _WebAudioSoundSource ? false : this._soundV2.state === 5 /* SoundState.Paused */; } /** * Define the max distance the sound should be heard (intensity just became 0 at this point). * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ get maxDistance() { return this._optionsV2.spatialMaxDistance || 100; } set maxDistance(value) { this._optionsV2.spatialMaxDistance = value; if (this.useCustomAttenuation) { return; } if (this._soundV2) { this._initSpatial(); this._soundV2.spatial.maxDistance = value; } } /** * Define the distance attenuation model the sound will follow. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ get distanceModel() { return this._optionsV2.spatialDistanceModel || "linear"; } set distanceModel(value) { this._optionsV2.spatialDistanceModel = value; if (this._soundV2) { this._initSpatial(); this._soundV2.spatial.distanceModel = value; } } /** * Gets the current time for the sound. */ get currentTime() { return this._soundV2 instanceof _WebAudioSoundSource ? this._soundV2.engine.currentTime : this._soundV2.currentTime; } /** * Does this sound enables spatial sound. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ get spatialSound() { return this._soundV2?._isSpatial ?? false; } /** * Does this sound enables spatial sound. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ set spatialSound(newValue) { if (this._soundV2) { if (newValue) { this._initSpatial(); } else { this._soundV2._isSpatial = false; } } } get _onReady() { if (!this._onReadyObservable) { this._onReadyObservable = new Observable(); } return this._onReadyObservable; } /** * Create a sound and attach it to a scene * @param name Name of your sound * @param urlOrArrayBuffer Url to the sound to load async or ArrayBuffer, it also works with MediaStreams and AudioBuffers * @param scene defines the scene the sound belongs to * @param readyToPlayCallback Provide a callback function if you'd like to load your code once the sound is ready to be played * @param options Objects to provide with the current available options: autoplay, loop, volume, spatialSound, maxDistance, rolloffFactor, refDistance, distanceModel, panningModel, streaming */ constructor(name, urlOrArrayBuffer, scene, readyToPlayCallback = null, options) { /** * Does the sound use a custom attenuation curve to simulate the falloff * happening when the source gets further away from the camera. * @see https://doc.babylonjs.com/legacy/audio#creating-your-own-custom-attenuation-function */ this.useCustomAttenuation = false; /** * The sound track id this sound belongs to. */ this.soundTrackId = -1; /** * Define the reference distance the sound should be heard perfectly. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ this.refDistance = 1; /** * Define the roll off factor of spatial sounds. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ this.rolloffFactor = 1; /** * Gets or sets an object used to store user defined information for the sound. */ this.metadata = null; /** * Observable event when the current playing sound finishes. */ this.onEndedObservable = new Observable(); this._localDirection = new Vector3(1, 0, 0); this._volume = 1; this._isReadyToPlay = false; this._isDirectional = false; this._isOutputConnected = false; this._url = null; this._onReadyObservable = null; this._onReadyToPlay = () => { this._scene.mainSoundTrack.addSound(this); this._isReadyToPlay = true; this._readyToPlayCallback(); if (this._onReadyObservable) { this._onReadyObservable.notifyObservers(); } if (this._optionsV2.autoplay) { this.play(); } }; this._onended = () => { if (this.onended) { this.onended(); } this.onEndedObservable.notifyObservers(this); }; scene = scene || EngineStore.LastCreatedScene; if (!scene) { return; } this._scene = scene; RegisterAudioSceneComponent(Sound); Sound._SceneComponentInitialization(scene); this._readyToPlayCallback = readyToPlayCallback || (() => { }); // Default custom attenuation function is a linear attenuation // eslint-disable-next-line @typescript-eslint/no-unused-vars this._customAttenuationFunction = (currentVolume, currentDistance, maxDistance, refDistance, rolloffFactor) => { if (currentDistance < maxDistance) { return currentVolume * (1 - currentDistance / maxDistance); } else { return 0; } }; options = options || {}; const optionsV2 = { analyzerEnabled: false, autoplay: false, // `false` for now, but will be set to given option later duration: options.length || 0, loop: options.loop || false, loopEnd: 0, loopStart: 0, outBus: null, outBusAutoDefault: false, playbackRate: options.playbackRate || 1, pitch: 0, skipCodecCheck: options.skipCodecCheck || false, spatialDistanceModel: options.distanceModel, spatialEnabled: options.spatialSound, spatialMaxDistance: options.maxDistance, spatialMinDistance: options.refDistance, spatialRolloffFactor: options.rolloffFactor, stereoEnabled: false, startOffset: options.offset || 0, volume: options.volume ?? 1, }; this._volume = options.volume ?? 1; if (_HasSpatialAudioOptions(optionsV2)) { optionsV2.spatialAutoUpdate = false; optionsV2.spatialConeInnerAngle = _SpatialAudioDefaults.coneInnerAngle; optionsV2.spatialConeOuterAngle = _SpatialAudioDefaults.coneOuterAngle; optionsV2.spatialConeOuterVolume = _SpatialAudioDefaults.coneOuterVolume; optionsV2.spatialMinUpdateTime = 0; optionsV2.spatialOrientation = _SpatialAudioDefaults.orientation.clone(); optionsV2.spatialPanningModel = (this._scene.headphone ? "HRTF" : "equalpower"); optionsV2.spatialPosition = _SpatialAudioDefaults.position.clone(); optionsV2.spatialRotation = _SpatialAudioDefaults.rotation.clone(); optionsV2.spatialRotationQuaternion = _SpatialAudioDefaults.rotationQuaternion.clone(); if (optionsV2.spatialMaxDistance === undefined) { optionsV2.spatialMaxDistance = 100; } } this._optionsV2 = { ...optionsV2 }; this._optionsV2.autoplay = options.autoplay || false; this.useCustomAttenuation = options.useCustomAttenuation ?? false; if (this.useCustomAttenuation) { optionsV2.spatialMaxDistance = Number.MAX_VALUE; optionsV2.volume = 0; } let streaming = options?.streaming || false; const audioEngine = AbstractEngine.audioEngine; if (!audioEngine) { return; } const audioEngineV2 = AbstractEngine.audioEngine._v2; const createSoundV2 = () => { if (streaming) { const streamingOptionsV2 = { preloadCount: 0, ...optionsV2, }; const sound = new _WebAudioStreamingSound(name, audioEngineV2, streamingOptionsV2); // eslint-disable-next-line github/no-then void sound._initAsync(urlOrArrayBuffer, optionsV2).then(() => { // eslint-disable-next-line github/no-then void sound.preloadInstancesAsync(1).then(this._onReadyToPlay); }); return sound; } else { const sound = new _WebAudioStaticSound(name, audioEngineV2, optionsV2); // eslint-disable-next-line github/no-then void sound._initAsync(urlOrArrayBuffer, optionsV2).then(this._onReadyToPlay); return sound; } }; // If no parameter is passed then the setAudioBuffer should be called to prepare the sound. if (!urlOrArrayBuffer) { // Create the sound but don't call _initAsync on it, yet. Call it later when `setAudioBuffer` is called. this._soundV2 = new _WebAudioStaticSound(name, audioEngineV2, optionsV2); } else if (typeof urlOrArrayBuffer === "string") { this._url = urlOrArrayBuffer; this._soundV2 = createSoundV2(); } else if (urlOrArrayBuffer instanceof ArrayBuffer) { streaming = false; this._soundV2 = createSoundV2(); } else if (urlOrArrayBuffer instanceof HTMLMediaElement) { streaming = true; this._soundV2 = createSoundV2(); } else if (urlOrArrayBuffer instanceof MediaStream) { const node = new MediaStreamAudioSourceNode(audioEngineV2._audioContext, { mediaStream: urlOrArrayBuffer }); this._soundV2 = new _WebAudioSoundSource(name, node, audioEngineV2, optionsV2); // eslint-disable-next-line github/no-then void this._soundV2._initAsync(optionsV2).then(this._onReadyToPlay); } else if (urlOrArrayBuffer instanceof AudioBuffer) { streaming = false; this._soundV2 = createSoundV2(); } else if (Array.isArray(urlOrArrayBuffer)) { this._soundV2 = createSoundV2(); } if (!this._soundV2) { Logger.Error("Parameter must be a URL to the sound, an Array of URLs (.mp3 & .ogg) or an ArrayBuffer of the sound."); return; } if (!(this._soundV2 instanceof _WebAudioSoundSource)) { this._soundV2.onEndedObservable.add(this._onended); } } /** * Release the sound and its associated resources */ dispose() { if (this.isPlaying) { this.stop(); } this._isReadyToPlay = false; if (this.soundTrackId === -1) { this._scene.mainSoundTrack.removeSound(this); } else if (this._scene.soundTracks) { this._scene.soundTracks[this.soundTrackId].removeSound(this); } if (this._connectedTransformNode && this._registerFunc) { this._connectedTransformNode.unregisterAfterWorldMatrixUpdate(this._registerFunc); this._connectedTransformNode = null; } this._soundV2.dispose(); } /** * Gets if the sounds is ready to be played or not. * @returns true if ready, otherwise false */ isReady() { return this._isReadyToPlay; } /** * Get the current class name. * @returns current class name */ getClassName() { return "Sound"; } /** * Sets the data of the sound from an audiobuffer * @param audioBuffer The audioBuffer containing the data */ setAudioBuffer(audioBuffer) { if (this._isReadyToPlay) { return; } if (this._soundV2 instanceof _WebAudioStaticSound) { // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then this._soundV2._initAsync(audioBuffer, this._optionsV2).then(this._onReadyToPlay); } } /** * Updates the current sounds options such as maxdistance, loop... * @param options A JSON object containing values named as the object properties */ updateOptions(options) { if (options) { this.loop = options.loop ?? this.loop; this.maxDistance = options.maxDistance ?? this.maxDistance; this.useCustomAttenuation = options.useCustomAttenuation ?? this.useCustomAttenuation; this.rolloffFactor = options.rolloffFactor ?? this.rolloffFactor; this.refDistance = options.refDistance ?? this.refDistance; this.distanceModel = options.distanceModel ?? this.distanceModel; if (options.playbackRate !== undefined) { this.setPlaybackRate(options.playbackRate); } if (options.spatialSound !== undefined) { this.spatialSound = options.spatialSound; } if (options.volume !== undefined) { this.setVolume(options.volume); } if (this._soundV2 instanceof _WebAudioStaticSound) { let updated = false; if (options.offset !== undefined) { this._optionsV2.startOffset = options.offset; updated = true; } if (options.length !== undefined) { this._soundV2.duration = options.length; updated = true; } if (updated && this.isPaused) { this.stop(); } } this._updateSpatialParameters(); } } _updateSpatialParameters() { if (!this.spatialSound) { return; } const spatial = this._soundV2.spatial; if (this.useCustomAttenuation) { // Disable WebAudio attenuation. spatial.distanceModel = "linear"; spatial.minDistance = 1; spatial.maxDistance = Number.MAX_VALUE; spatial.rolloffFactor = 1; spatial.panningModel = "equalpower"; } else { spatial.distanceModel = this.distanceModel; spatial.minDistance = this.refDistance; spatial.maxDistance = this.maxDistance; spatial.rolloffFactor = this.rolloffFactor; spatial.panningModel = this._optionsV2.spatialPanningModel || "equalpower"; } } /** * Switch the panning model to HRTF: * Renders a stereo output of higher quality than equalpower — it uses a convolution with measured impulse responses from human subjects. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ switchPanningModelToHRTF() { if (this.spatialSound) { this._initSpatial(); this._soundV2.spatial.panningModel = "HRTF"; } } /** * Switch the panning model to Equal Power: * Represents the equal-power panning algorithm, generally regarded as simple and efficient. equalpower is the default value. * @see https://doc.babylonjs.com/legacy/audio#creating-a-spatial-3d-sound */ switchPanningModelToEqualPower() { if (this.spatialSound) { this._initSpatial(); this._soundV2.spatial.panningModel = "equalpower"; } } /** * Connect this sound to a sound track audio node like gain... * @param soundTrackAudioNode the sound track audio node to connect to */ connectToSoundTrackAudioNode(soundTrackAudioNode) { const outputNode = this._soundV2._outNode; if (outputNode) { if (this._isOutputConnected) { outputNode.disconnect(); } outputNode.connect(soundTrackAudioNode); this._isOutputConnected = true; } } /** * Transform this sound into a directional source * @param coneInnerAngle Size of the inner cone in degree * @param coneOuterAngle Size of the outer cone in degree * @param coneOuterGain Volume of the sound outside the outer cone (between 0.0 and 1.0) */ setDirectionalCone(coneInnerAngle, coneOuterAngle, coneOuterGain) { if (coneOuterAngle < coneInnerAngle) { Logger.Error("setDirectionalCone(): outer angle of the cone must be superior or equal to the inner angle."); return; } this._optionsV2.spatialConeInnerAngle = D2r(coneInnerAngle); this._optionsV2.spatialConeOuterAngle = D2r(coneOuterAngle); this._optionsV2.spatialConeOuterVolume = coneOuterGain; this._initSpatial(); this._soundV2.spatial.coneInnerAngle = this._optionsV2.spatialConeInnerAngle; this._soundV2.spatial.coneOuterAngle = this._optionsV2.spatialConeOuterAngle; this._soundV2.spatial.coneOuterVolume = coneOuterGain; this._isDirectional = true; if (this.isPlaying && this.loop) { this.stop(); this.play(0, this._optionsV2.startOffset, this._optionsV2.duration); } } /** * Gets or sets the inner angle for the directional cone. */ get directionalConeInnerAngle() { return R2d(typeof this._optionsV2.spatialConeInnerAngle === "number" ? this._optionsV2.spatialConeInnerAngle : _SpatialAudioDefaults.coneInnerAngle); } /** * Gets or sets the inner angle for the directional cone. */ set directionalConeInnerAngle(value) { value = D2r(value); if (value != this._optionsV2.spatialConeInnerAngle) { if (this.directionalConeOuterAngle < value) { Logger.Error("directionalConeInnerAngle: outer angle of the cone must be superior or equal to the inner angle."); return; } this._optionsV2.spatialConeInnerAngle = value; if (this.spatialSound) { this._initSpatial(); this._soundV2.spatial.coneInnerAngle = value; } } } /** * Gets or sets the outer angle for the directional cone. */ get directionalConeOuterAngle() { return R2d(typeof this._optionsV2.spatialConeOuterAngle === "number" ? this._optionsV2.spatialConeOuterAngle : _SpatialAudioDefaults.coneOuterAngle); } /** * Gets or sets the outer angle for the directional cone. */ set directionalConeOuterAngle(value) { value = D2r(value); if (value != this._optionsV2.spatialConeOuterAngle) { if (value < this.directionalConeInnerAngle) { Logger.Error("directionalConeOuterAngle: outer angle of the cone must be superior or equal to the inner angle."); return; } this._optionsV2.spatialConeOuterAngle = value; if (this.spatialSound) { this._initSpatial(); this._soundV2.spatial.coneOuterAngle = value; } } } /** * Sets the position of the emitter if spatial sound is enabled * @param newPosition Defines the new position */ setPosition(newPosition) { if (this._optionsV2.spatialPosition && newPosition.equals(this._optionsV2.spatialPosition)) { return; } if (!this._optionsV2.spatialPosition) { this._optionsV2.spatialPosition = Vector3.Zero(); } this._optionsV2.spatialPosition.copyFrom(newPosition); if (this.spatialSound && !isNaN(newPosition.x) && !isNaN(newPosition.y) && !isNaN(newPosition.z)) { this._initSpatial(); this._soundV2.spatial.position = newPosition; } } /** * Sets the local direction of the emitter if spatial sound is enabled * @param newLocalDirection Defines the new local direction */ setLocalDirectionToMesh(newLocalDirection) { this._localDirection = newLocalDirection; if (this._connectedTransformNode && this.isPlaying) { this._updateDirection(); } } _updateDirection() { if (!this._connectedTransformNode || !this.spatialSound) { return; } const mat = this._connectedTransformNode.getWorldMatrix(); const direction = Vector3.TransformNormal(this._localDirection, mat); direction.normalize(); this._initSpatial(); this._soundV2.spatial.orientation = direction; } _initSpatial() { this._soundV2._isSpatial = true; if (this._optionsV2.spatialDistanceModel === undefined) { this._optionsV2.spatialDistanceModel = "linear"; this._soundV2.spatial.distanceModel = "linear"; } if (this._optionsV2.spatialMaxDistance === undefined) { this._optionsV2.spatialMaxDistance = 100; this._soundV2.spatial.maxDistance = 100; } } /** @internal */ updateDistanceFromListener() { if (this._soundV2._outNode && this._connectedTransformNode && this.useCustomAttenuation && this._scene.activeCamera) { const distance = this._scene.audioListenerPositionProvider ? this._connectedTransformNode.position.subtract(this._scene.audioListenerPositionProvider()).length() : this._connectedTransformNode.getDistanceToCamera(this._scene.activeCamera); this._soundV2.volume = this._customAttenuationFunction(this._volume, distance, this.maxDistance, this.refDistance, this.rolloffFactor); } } /** * Sets a new custom attenuation function for the sound. * @param callback Defines the function used for the attenuation