diff --git a/src/components/audio-params.js b/src/components/audio-params.js new file mode 100644 index 0000000000..24088619a2 --- /dev/null +++ b/src/components/audio-params.js @@ -0,0 +1,285 @@ +import { Vector3 } from "three"; +import { AudioNormalizer } from "../utils/audio-normalizer"; +import { CLIPPING_THRESHOLD_ENABLED, CLIPPING_THRESHOLD_DEFAULT } from "../react-components/preferences-screen"; +import { AvatarAudioDefaults, DISTANCE_MODEL_OPTIONS } from "../systems/audio-settings-system"; + +export const SourceType = Object.freeze({ MEDIA_VIDEO: 0, AVATAR_AUDIO_SOURCE: 1, AVATAR_RIG: 2, AUDIO_TARGET: 3 }); + +const MUTE_DELAY_SECS = 1; + +const distanceModels = { + linear: function(distance, rolloffFactor, refDistance, maxDistance) { + return 1.0 - rolloffFactor * ((distance - refDistance) / (maxDistance - refDistance)); + }, + inverse: function(distance, rolloffFactor, refDistance) { + return refDistance / (refDistance + rolloffFactor * (Math.max(distance, refDistance) - refDistance)); + }, + exponential: function(distance, rolloffFactor, refDistance) { + return Math.pow(Math.max(distance, refDistance) / refDistance, -rolloffFactor); + } +}; + +AFRAME.registerComponent("audio-params", { + schema: { + enabled: { default: true }, + isLocal: { default: false }, + position: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, + orientation: { type: "vec3", default: { x: 0, y: 0, z: 0 } }, + distanceModel: { default: AvatarAudioDefaults.DISTANCE_MODEL, oneOf: [DISTANCE_MODEL_OPTIONS] }, + rolloffFactor: { default: AvatarAudioDefaults.ROLLOFF_FACTOR }, + refDistance: { default: AvatarAudioDefaults.REF_DISTANCE }, + maxDistance: { default: AvatarAudioDefaults.MAX_DISTANCE }, + clippingEnabled: { default: CLIPPING_THRESHOLD_ENABLED }, + clippingThreshold: { default: CLIPPING_THRESHOLD_DEFAULT }, + prevGain: { default: 1.0 }, + isClipped: { default: false }, + gain: { default: 1.0 }, + sourceType: { default: -1 }, + attenuation: { default: 1.0 }, + distance: { default: 0.0 }, + squaredDistance: { default: 0.0 } + }, + + init() { + this.avatarRigPosition = new THREE.Vector3(); + this.avatarAudio = { + panner: { + orientationX: { + value: 0 + }, + orientationY: { + value: 0 + }, + orientationZ: { + value: 0 + }, + positionX: { + value: 0 + }, + positionY: { + value: 0 + }, + positionZ: { + value: 0 + }, + distanceModel: AvatarAudioDefaults.DISTANCE_MODEL, + maxDistance: AvatarAudioDefaults.MAX_DISTANCE, + refDistance: AvatarAudioDefaults.REF_DISTANCE, + rolloffFactor: AvatarAudioDefaults.ROLLOFF_FACTOR, + coneInnerAngle: AvatarAudioDefaults.INNER_ANGLE, + coneOuterAngle: AvatarAudioDefaults.OUTER_ANGLE, + coneOuterGain: AvatarAudioDefaults.OUTER_GAIN + } + }; + this.avatarRigOrientation = new THREE.Vector3(0, 0, -1); + this.listenerPos = new THREE.Vector3(); + this.data.position = new Vector3(0.0, 0.0, 0.0); + this.data.orientation = new Vector3(0.0, 0.0, 0.0); + this.normalizer = null; + this.el.sceneEl?.systems["audio-debug"].registerSource(this); + if (!this.data.isLocal) { + this.el.sceneEl?.systems["hubs-systems"].gainSystem.registerSource(this); + } + + const { enableAudioClipping, audioClippingThreshold } = window.APP.store.state.preferences; + this.data.clippingEnabled = enableAudioClipping !== undefined ? enableAudioClipping : CLIPPING_THRESHOLD_ENABLED; + this.data.clippingThreshold = + audioClippingThreshold !== undefined ? audioClippingThreshold : CLIPPING_THRESHOLD_DEFAULT; + + this.onVolumeUpdated = this.volumeUpdated.bind(this); + this.onSourceSetAdded = this.sourceSetAdded.bind(this); + if (this.data.isLocal) { + this.data.sourceType = SourceType.AVATAR_RIG; + } else if (this.el.components["media-video"]) { + this.data.sourceType = SourceType.MEDIA_VIDEO; + this.data.gain = this.el.components["media-video"].data.volume; + this.el.addEventListener("media-volume-changed", this.onVolumeUpdated); + } else if (this.el.components["avatar-audio-source"]) { + this.data.sourceType = SourceType.AVATAR_AUDIO_SOURCE; + this.data.gain = + this.el.parentEl?.parentEl?.querySelector("[avatar-volume-controls]").components["avatar-volume-controls"]?.data + .volume || 1.0; + this.el.parentEl?.parentEl?.addEventListener("avatar-volume-changed", this.onVolumeUpdated); + } else if (this.el.components["audio-target"]) { + this.data.sourceType = SourceType.AUDIO_TARGET; + } + }, + + remove() { + this.normalizer = null; + this.el.sceneEl?.systems["audio-debug"].unregisterSource(this); + if (!this.data.isLocal) { + this.el.sceneEl?.systems["hubs-systems"].gainSystem.unregisterSource(this); + } + + this.el.removeEventListener("media-volume-changed", this.onVolumeUpdated); + this.el.parentEl?.parentEl?.removeEventListener("avatar-volume-changed", this.onVolumeUpdated); + + if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + this.el.components["avatar-audio-source"].el.removeEventListener("sound-source-set", this.onSourceSetAdded); + } + }, + + tick() { + const audio = this.audio(); + if (audio) { + if (audio.updateMatrixWorld) { + audio.updateMatrixWorld(true); + } + this.data.position = new THREE.Vector3( + audio.panner.positionX.value, + audio.panner.positionY.value, + audio.panner.positionZ.value + ); + this.data.orientation = new THREE.Vector3( + audio.panner.orientationX.value, + audio.panner.orientationY.value, + audio.panner.orientationZ.value + ); + this.data.distanceModel = audio.panner.distanceModel; + this.data.rolloffFactor = audio.panner.rolloffFactor; + this.data.refDistance = audio.panner.refDistance; + this.data.maxDistance = audio.panner.maxDistance; + this.data.coneInnerAngle = audio.panner.coneInnerAngle; + this.data.coneOuterAngle = audio.panner.coneOuterAngle; + this.data.coneOuterGain = audio.panner.coneOuterGain; + this.updateDistances(); + this.updateAttenuation(); + } + + if (this.normalizer !== null) { + this.normalizer.apply(); + } else { + // We one only enable the Normalizer for avatar-audio-source components + if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + this.enableNormalizer(); + } + } + }, + + audio() { + if (this.data.sourceType === SourceType.AVATAR_RIG) { + // Create fake parametes for the avatar rig as it doens't have an audio source. + const audioParams = this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.audioSettings; + const avatarRigObj = document.getElementById("avatar-rig").querySelector(".camera").object3D; + avatarRigObj.updateMatrixWorld(true); + avatarRigObj.getWorldPosition(this.avatarRigPosition); + this.avatarRigOrientation.set(0, 0, -1); + const worldQuat = new THREE.Quaternion(); + avatarRigObj.getWorldQuaternion(worldQuat); + this.avatarRigOrientation.applyQuaternion(worldQuat); + this.avatarAudio.panner.orientationX.value = this.avatarRigOrientation.x; + this.avatarAudio.panner.orientationY.value = this.avatarRigOrientation.y; + this.avatarAudio.panner.orientationZ.value = this.avatarRigOrientation.z; + this.avatarAudio.panner.positionX.value = this.avatarRigPosition.x; + this.avatarAudio.panner.positionY.value = this.avatarRigPosition.y; + this.avatarAudio.panner.positionZ.value = this.avatarRigPosition.z; + this.avatarAudio.panner.distanceModel = audioParams.avatarDistanceModel; + this.avatarAudio.panner.maxDistance = audioParams.avatarMaxDistance; + this.avatarAudio.panner.refDistance = audioParams.avatarRefDistance; + this.avatarAudio.panner.rolloffFactor = audioParams.avatarRolloffFactor; + this.avatarAudio.panner.coneInnerAngle = audioParams.avatarConeInnerAngle; + this.avatarAudio.panner.coneOuterAngle = audioParams.avatarConeOuterAngle; + this.avatarAudio.panner.coneOuterGain = audioParams.avatarConeOuterGainain; + return this.avatarAudio; + } else if (this.data.sourceType === SourceType.MEDIA_VIDEO) { + const audio = this.el.getObject3D("sound"); + return audio?.panner ? audio : null; + } else if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + const audio = this.el.getObject3D("avatar-audio-source"); + return audio?.panner ? audio : null; + } else if (this.data.sourceType === SourceType.AUDIO_TARGET) { + const audio = this.el.getObject3D("audio-target"); + return audio?.panner ? audio : null; + } + }, + + enableNormalizer() { + const audio = this.audio(); + if (audio) { + const avatarAudioSource = this.el.components["avatar-audio-source"]; + if (avatarAudioSource) { + this.normalizer = new AudioNormalizer(audio); + avatarAudioSource.el.addEventListener("sound-source-set", this.onSourceSetAdded); + } + } + }, + + sourceSetAdded() { + const avatarAudioSource = this.el.components["avatar-audio-source"]; + const audio = avatarAudioSource && avatarAudioSource.el.getObject3D(avatarAudioSource.attrName); + if (audio) { + this.normalizer = new AudioNormalizer(audio); + this.normalizer.apply(); + } + }, + + updateDistances() { + this.el.sceneEl.audioListener.getWorldPosition(this.listenerPos); + this.data.distance = this.data.position.distanceTo(this.listenerPos); + this.data.squaredDistance = this.data.position.distanceToSquared(this.listenerPos); + }, + + updateAttenuation() { + this.data.attenuation = distanceModels[this.data.distanceModel]( + this.data.distance, + this.data.rolloffFactor, + this.data.refDistance, + this.data.maxDistance + ); + }, + + clipGain(gain) { + if (!this.data.isClipped) { + const audio = this.audio(); + this.data.isClipped = true; + this.data.prevGain = this.data.gain; + this.data.gain = gain; + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio.gain.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + } + }, + + unclipGain() { + if (this.data.isClipped) { + const audio = this.audio(); + this.data.isClipped = false; + this.data.gain = this.data.prevGain; + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio?.gain?.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + } + }, + + updateGain(newGain) { + const audio = this.audio(); + if (this.data.isClipped) { + this.data.prevGain = newGain; + } else { + this.data.prevGain = this.data.gain; + this.data.gain = newGain; + } + this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain; + audio?.gain?.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS); + }, + + volumeUpdated({ detail: volume }) { + let globalVolume = 100; + const { audioOutputMode, globalVoiceVolume, globalMediaVolume } = window.APP.store.state.preferences; + if (this.data.sourceType === SourceType.MEDIA_VIDEO) { + globalVolume = globalMediaVolume; + } else if (this.data.sourceType === SourceType.AVATAR_AUDIO_SOURCE) { + globalVolume = globalVoiceVolume; + } + const volumeModifier = (globalVolume !== undefined ? globalVolume : 100) / 100; + let newGain = volumeModifier * volume; + if (audioOutputMode === "audio") { + newGain = this.data.gain * Math.min(1, 10 / Math.max(1, this.data.squaredDistance)); + } + this.updateGain(newGain); + }, + + clippingUpdated({ clippingEnabled, clippingThreshold }) { + this.data.clippingEnabled = clippingEnabled !== undefined ? clippingEnabled : CLIPPING_THRESHOLD_ENABLED; + this.data.clippingThreshold = clippingThreshold !== undefined ? clippingThreshold : CLIPPING_THRESHOLD_DEFAULT; + } +}); diff --git a/src/components/avatar-audio-source.js b/src/components/avatar-audio-source.js index 86cc294b02..c6e6d1c039 100644 --- a/src/components/avatar-audio-source.js +++ b/src/components/avatar-audio-source.js @@ -1,3 +1,5 @@ +import { AvatarAudioDefaults, TargetAudioDefaults, DISTANCE_MODEL_OPTIONS } from "../systems/audio-settings-system"; + const INFO_INIT_FAILED = "Failed to initialize avatar-audio-source."; const INFO_NO_NETWORKED_EL = "Could not find networked el."; const INFO_NO_OWNER = "Networked component has no owner."; @@ -45,23 +47,25 @@ function setPositionalAudioProperties(audio, settings) { audio.setMaxDistance(settings.maxDistance); audio.setRefDistance(settings.refDistance); audio.setRolloffFactor(settings.rolloffFactor); - audio.setDirectionalCone(settings.innerAngle, settings.outerAngle, settings.outerGain); + audio.panner.coneInnerAngle = settings.innerAngle; + audio.panner.coneOuterAngle = settings.outerAngle; + audio.panner.coneOuterGain = settings.outerGain; } AFRAME.registerComponent("avatar-audio-source", { schema: { positional: { default: true }, distanceModel: { - default: "inverse", - oneOf: ["linear", "inverse", "exponential"] + default: AvatarAudioDefaults.DISTANCE_MODEL, + oneOf: [DISTANCE_MODEL_OPTIONS] }, - maxDistance: { default: 10000 }, - refDistance: { default: 1 }, - rolloffFactor: { default: 1 }, + maxDistance: { default: AvatarAudioDefaults.MAX_DISTANCE }, + refDistance: { default: AvatarAudioDefaults.REF_DISTANCE }, + rolloffFactor: { default: AvatarAudioDefaults.ROLLOFF_FACTOR }, - innerAngle: { default: 360 }, - outerAngle: { default: 0 }, - outerGain: { default: 0 } + innerAngle: { default: AvatarAudioDefaults.INNER_ANGLE }, + outerAngle: { default: AvatarAudioDefaults.OUTER_ANGLE }, + outerGain: { default: AvatarAudioDefaults.OUTER_GAIN } }, createAudio: async function() { @@ -102,7 +106,7 @@ AFRAME.registerComponent("avatar-audio-source", { this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.registerAvatarAudioSource(this); // We subscribe to audio stream notifications for this peer to update the audio source // This could happen in case there is an ICE failure that requires a transport recreation. - NAF.connection.adapter.on("stream_updated", this._onStreamUpdated, this); + NAF.connection.adapter?.on("stream_updated", this._onStreamUpdated, this); this.createAudio(); }, @@ -266,20 +270,20 @@ AFRAME.registerComponent("audio-target", { positional: { default: true }, distanceModel: { - default: "inverse", - oneOf: ["linear", "inverse", "exponential"] + default: TargetAudioDefaults.DISTANCE_MODEL, + oneOf: [DISTANCE_MODEL_OPTIONS] }, - maxDistance: { default: 10000 }, - refDistance: { default: 8 }, - rolloffFactor: { default: 5 }, + maxDistance: { default: TargetAudioDefaults.MAX_DISTANCE }, + refDistance: { default: TargetAudioDefaults.REF_DISTANCE }, + rolloffFactor: { default: TargetAudioDefaults.ROLLOFF_FACTOR }, - innerAngle: { default: 170 }, - outerAngle: { default: 300 }, - outerGain: { default: 0.3 }, + innerAngle: { default: TargetAudioDefaults.INNER_ANGLE }, + outerAngle: { default: TargetAudioDefaults.OUTER_ANGLE }, + outerGain: { default: TargetAudioDefaults.OUTER_GAIN }, minDelay: { default: 0.01 }, maxDelay: { default: 0.13 }, - gain: { default: 1.0 }, + gain: { default: TargetAudioDefaults.VOLUME }, srcEl: { type: "selector" }, @@ -293,20 +297,20 @@ AFRAME.registerComponent("audio-target", { setTimeout(() => { this.connectAudio(); }, 0); + this.el.setAttribute("audio-params", this.data); }, remove: function() { this.destroyAudio(); + this.el.removeAttribute("audio-params"); }, createAudio: function() { const audioListener = this.el.sceneEl.audioListener; const audio = this.data.positional ? new THREE.PositionalAudio(audioListener) : new THREE.Audio(audioListener); - if (this.data.debug && this.data.positional) { + if (this.data.positional) { setPositionalAudioProperties(audio, this.data); - const helper = new THREE.PositionalAudioHelper(audio, this.data.refDistance, 16, 16); - audio.add(helper); } audio.setVolume(this.data.gain); @@ -340,5 +344,11 @@ AFRAME.registerComponent("audio-target", { audio.disconnect(); this.el.removeObject3D(this.attrName); + }, + + update() { + if (this.data.positional) { + setPositionalAudioProperties(this.audio, this.data); + } } }); diff --git a/src/components/avatar-volume-controls.js b/src/components/avatar-volume-controls.js index 7170874506..97810556a9 100644 --- a/src/components/avatar-volume-controls.js +++ b/src/components/avatar-volume-controls.js @@ -3,96 +3,6 @@ const MAX_VOLUME = 8; const SMALL_STEP = 1 / (VOLUME_LABELS.length / 2); const BIG_STEP = (MAX_VOLUME - 1) / (VOLUME_LABELS.length / 2); -// Inserts analyser and gain nodes after audio source. -// Analyses audio source volume and adjusts gain value -// to make it in a certain range. -class AudioNormalizer { - constructor(audio) { - this.audio = audio; - this.analyser = audio.context.createAnalyser(); - this.connected = false; - - // To analyse volume, 32 fftsize may be good enough - this.analyser.fftSize = 32; - this.gain = audio.context.createGain(); - this.timeData = new Uint8Array(this.analyser.frequencyBinCount); - this.volumes = []; - this.volumeSum = 0; - } - - apply() { - if (window.APP.store.state.preferences.audioNormalization) { - if (!this.connected) { - this.connect(); - } - } else { - if (this.connected) { - this.disconnect(); - } - return; - } - - // Adjusts volume in "a rule of the thumb" way - // Any better algorithm? - - // Regards the RMS of time-domain data as volume. - // Is this a right approach? - // Using the RMS of frequency-domain data would be another option. - this.analyser.getByteTimeDomainData(this.timeData); - const squareSum = this.timeData.reduce((sum, num) => sum + Math.pow(num - 128, 2), 0); - const volume = Math.sqrt(squareSum / this.analyser.frequencyBinCount); - const baseVolume = window.APP.store.state.preferences.audioNormalization; - - // Regards volume under certain threshold as "not speaking" and skips. - // I'm not sure if 0.4 is an appropriate threshold. - if (volume >= Math.min(0.4, baseVolume)) { - this.volumeSum += volume; - this.volumes.push(volume); - // Sees only recent volume history because there is a chance - // that a speaker changes their master input volume. - // I'm not sure if 600 is an appropriate number. - while (this.volumes.length > 600) { - this.volumeSum -= this.volumes.shift(); - } - // Adjusts volume after getting many enough volume history. - // I'm not sure if 60 is an appropriate number. - if (this.volumes.length >= 60) { - const averageVolume = this.volumeSum / this.volumes.length; - this.gain.gain.setTargetAtTime(baseVolume / averageVolume, this.audio.context.currentTime, 0.01); - } - } - } - - connect() { - // Hacks. THREE.Audio connects audio nodes when source is set. - // If audio is not played yet, THREE.Audio.setFilters() doesn't - // reset connections. Then manually caling .connect()/disconnect() here. - // This might be a bug of Three.js and should be fixed in Three.js side? - if (this.audio.source && !this.audio.isPlaying) { - this.audio.disconnect(); - } - const filters = this.audio.getFilters(); - filters.unshift(this.analyser, this.gain); - this.audio.setFilters(filters); - if (this.audio.source && !this.audio.isPlaying) { - this.audio.connect(); - } - this.connected = true; - } - - disconnect() { - if (this.audio.source && !this.audio.isPlaying) { - this.audio.disconnect(); - } - const filters = [this.analyser, this.gain]; - this.audio.setFilters(this.audio.getFilters().filter(filter => !filters.includes(filter))); - if (this.audio.source && !this.audio.isPlaying) { - this.audio.connect(); - } - this.connected = false; - } -} - AFRAME.registerComponent("avatar-volume-controls", { schema: { volume: { type: "number", default: 1.0 } @@ -118,7 +28,9 @@ AFRAME.registerComponent("avatar-volume-controls", { }, changeVolumeBy(v) { - this.el.setAttribute("avatar-volume-controls", "volume", THREE.Math.clamp(this.data.volume + v, 0, MAX_VOLUME)); + const vol = THREE.Math.clamp(this.data.volume + v, 0, MAX_VOLUME); + this.el.setAttribute("avatar-volume-controls", "volume", vol); + this.el.emit("avatar-volume-changed", vol); this.updateVolumeLabel(); }, @@ -132,43 +44,6 @@ AFRAME.registerComponent("avatar-volume-controls", { this.changeVolumeBy(-1 * step); }, - update: (function() { - const positionA = new THREE.Vector3(); - const positionB = new THREE.Vector3(); - return function update() { - const audio = this.avatarAudioSource && this.avatarAudioSource.el.getObject3D(this.avatarAudioSource.attrName); - if (!audio) { - return; - } - - if (!this.normalizer) { - this.normalizer = new AudioNormalizer(audio); - this.avatarAudioSource.el.addEventListener("sound-source-set", () => { - const audio = - this.avatarAudioSource && this.avatarAudioSource.el.getObject3D(this.avatarAudioSource.attrName); - if (audio) { - this.normalizer = new AudioNormalizer(audio); - this.normalizer.apply(); - } - }); - } - - this.normalizer.apply(); - - const { audioOutputMode, globalVoiceVolume } = window.APP.store.state.preferences; - const volumeModifier = (globalVoiceVolume !== undefined ? globalVoiceVolume : 100) / 100; - let gain = volumeModifier * this.data.volume; - if (audioOutputMode === "audio") { - this.avatarAudioSource.el.object3D.getWorldPosition(positionA); - this.el.sceneEl.audioListener.getWorldPosition(positionB); - const squaredDistance = positionA.distanceToSquared(positionB); - gain = gain * Math.min(1, 10 / Math.max(1, squaredDistance)); - } - - audio.gain.gain.value = gain; - }; - })(), - updateVolumeLabel() { const numBars = Math.min( VOLUME_LABELS.length - 1, @@ -177,19 +52,5 @@ AFRAME.registerComponent("avatar-volume-controls", { : Math.floor(VOLUME_LABELS.length / 2 + (this.data.volume - 1) / BIG_STEP) ); this.volumeLabel.setAttribute("text", "value", this.data.volume === 0 ? "Muted" : VOLUME_LABELS[numBars]); - }, - - tick() { - if (!this.avatarAudioSource && !this.searchFailed) { - // Walk up to Spine and then search down. - const sourceEl = this.el.parentNode.parentNode.querySelector("[avatar-audio-source]"); - if (!sourceEl || !sourceEl.components["avatar-audio-source"]) { - this.searchFailed = true; - return; - } - this.avatarAudioSource = sourceEl.components["avatar-audio-source"]; - } - - this.update(); } }); diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 04b187841d..185e92578a 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -171,6 +171,7 @@ AFRAME.registerComponent("media-loader", { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-pager"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.setAttribute("media-image", { src: "error" }); this.clearLoadingTimeout(); @@ -348,6 +349,7 @@ AFRAME.registerComponent("media-loader", { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-pager"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-image"); } @@ -460,6 +462,7 @@ AFRAME.registerComponent("media-loader", { linkedMediaElementAudioSource }) ); + this.el.setAttribute("audio-params", {}); if (this.el.components["position-at-border__freeze"]) { this.el.setAttribute("position-at-border__freeze", { isFlat: true }); } @@ -469,6 +472,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("image/")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( @@ -511,6 +515,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("application/pdf")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-image"); this.el.setAttribute( "media-pdf", @@ -544,6 +549,7 @@ AFRAME.registerComponent("media-loader", { ) { this.el.removeAttribute("media-image"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( @@ -577,6 +583,7 @@ AFRAME.registerComponent("media-loader", { } else if (contentType.startsWith("text/html")) { this.el.removeAttribute("gltf-model-plus"); this.el.removeAttribute("media-video"); + this.el.removeAttribute("audio-params"); this.el.removeAttribute("media-pdf"); this.el.removeAttribute("media-pager"); this.el.addEventListener( diff --git a/src/components/media-views.js b/src/components/media-views.js index f43d7a8831..03ad09e2f4 100644 --- a/src/components/media-views.js +++ b/src/components/media-views.js @@ -17,6 +17,7 @@ import { applyPersistentSync } from "../utils/permissions-utils"; import { refreshMediaMirror, getCurrentMirroredMedia } from "../utils/mirror-utils"; import { detect } from "detect-browser"; import semver from "semver"; +import { MediaAudioDefaults } from "../systems/audio-settings-system"; import qsTruthy from "../utils/qs_truthy"; @@ -255,17 +256,17 @@ AFRAME.registerComponent("media-video", { src: { type: "string" }, audioSrc: { type: "string" }, contentType: { type: "string" }, - volume: { type: "number", default: 0.5 }, + volume: { type: "number", default: MediaAudioDefaults.VOLUME }, loop: { type: "boolean", default: true }, audioType: { type: "string", default: "pannernode" }, hidePlaybackControls: { type: "boolean", default: false }, - distanceModel: { type: "string", default: "inverse" }, - rolloffFactor: { type: "number", default: 1 }, - refDistance: { type: "number", default: 1 }, - maxDistance: { type: "number", default: 10000 }, - coneInnerAngle: { type: "number", default: 360 }, - coneOuterAngle: { type: "number", default: 0 }, - coneOuterGain: { type: "number", default: 0 }, + distanceModel: { type: "string", default: MediaAudioDefaults.DISTANCE_MODEL }, + rolloffFactor: { type: "number", default: MediaAudioDefaults.ROLLOFF_FACTOR }, + refDistance: { type: "number", default: MediaAudioDefaults.REF_DISTANCE }, + maxDistance: { type: "number", default: MediaAudioDefaults.MAX_DISTANCE }, + coneInnerAngle: { type: "number", default: MediaAudioDefaults.INNER_ANGLE }, + coneOuterAngle: { type: "number", default: MediaAudioDefaults.OUTER_ANGLE }, + coneOuterGain: { type: "number", default: MediaAudioDefaults.OUTER_GAIN }, videoPaused: { type: "boolean" }, projection: { type: "string", default: "flat" }, time: { type: "number" }, @@ -289,8 +290,6 @@ AFRAME.registerComponent("media-video", { this.changeVolumeBy = this.changeVolumeBy.bind(this); this.togglePlaying = this.togglePlaying.bind(this); - this.distanceBasedAttenuation = 1; - this.lastUpdate = 0; this.videoMutedAt = 0; this.localSnapCount = 0; @@ -404,7 +403,9 @@ AFRAME.registerComponent("media-video", { }, changeVolumeBy(v) { - this.el.setAttribute("media-video", "volume", THREE.Math.clamp(this.data.volume + v, 0, 1)); + const vol = THREE.Math.clamp(this.data.volume + v, 0, 1); + this.el.setAttribute("media-video", "volume", vol); + this.el.emit("media-volume-changed", vol); this.updateVolumeLabel(); }, @@ -465,6 +466,7 @@ AFRAME.registerComponent("media-video", { if (this._ignorePauseStateChanges) return; this.el.setAttribute("media-video", "videoPaused", this.video.paused); + this.el.setAttribute("audio-params", { enabled: !this.video.paused }); if (this.networkedEl && NAF.utils.isMine(this.networkedEl)) { this.el.emit("owned-video-state-changed"); @@ -482,15 +484,6 @@ AFRAME.registerComponent("media-video", { this.tryUpdateVideoPlaybackState(this.data.videoPaused); } } - - // Volume is local, always update it - if (this.audio && window.APP.store.state.preferences.audioOutputMode !== "audio") { - const globalMediaVolume = - window.APP.store.state.preferences.globalMediaVolume !== undefined - ? window.APP.store.state.preferences.globalMediaVolume - : 100; - this.audio.gain.gain.value = (globalMediaVolume / 100) * this.data.volume; - } }, tryUpdateVideoPlaybackState(pause, currentTime) { @@ -554,7 +547,6 @@ AFRAME.registerComponent("media-video", { if (!disablePositionalAudio && this.data.audioType === "pannernode") { this.audio = new THREE.PositionalAudio(this.el.sceneEl.audioListener); this.setPositionalAudioProperties(); - this.distanceBasedAttenuation = 1; } else { this.audio = new THREE.Audio(this.el.sceneEl.audioListener); } @@ -862,6 +854,8 @@ AFRAME.registerComponent("media-video", { videoEl.onerror = failLoad; if (this.data.audioSrc) { + videoEl.muted = true; + // If there's an audio src, create an audio element to play it that we keep in sync // with the video while this component is active. audioEl = createVideoOrAudioEl("audio"); @@ -932,8 +926,6 @@ AFRAME.registerComponent("media-video", { }, tick: (() => { - const positionA = new THREE.Vector3(); - const positionB = new THREE.Vector3(); return function() { if (!this.video) return; @@ -977,20 +969,6 @@ AFRAME.registerComponent("media-video", { this.lastUpdate = now; } } - - if (this.audio) { - if (window.APP.store.state.preferences.audioOutputMode === "audio") { - this.el.object3D.getWorldPosition(positionA); - this.el.sceneEl.audioListener.getWorldPosition(positionB); - const distance = positionA.distanceTo(positionB); - this.distanceBasedAttenuation = Math.min(1, 10 / Math.max(1, distance * distance)); - const globalMediaVolume = - window.APP.store.state.preferences.globalMediaVolume !== undefined - ? window.APP.store.state.preferences.globalMediaVolume - : 100; - this.audio.gain.gain.value = (globalMediaVolume / 100) * this.data.volume * this.distanceBasedAttenuation; - } - } }; })(), diff --git a/src/components/player-info.js b/src/components/player-info.js index 374c046e5a..79c1afce7d 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -183,6 +183,8 @@ AFRAME.registerComponent("player-info", { el.setAttribute("emit-scene-event-on-remove", "event:action_end_video_sharing"); } } + + this.el.querySelector("[audio-params]")?.setAttribute("audio-params", { enabled: !this.data.muted }); }, handleModelError() { window.APP.store.resetToRandomDefaultAvatar(); diff --git a/src/hub.html b/src/hub.html index ce292d573b..d94a15dade 100644 --- a/src/hub.html +++ b/src/hub.html @@ -221,6 +221,7 @@