Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audio Zones #4399

Merged
merged 10 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 163 additions & 81 deletions src/components/audio-params.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,60 @@
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 });
export const DISTANCE_MODEL_OPTIONS = ["linear", "inverse", "exponential"];

export const SourceType = Object.freeze({
MEDIA_VIDEO: 0,
AVATAR_AUDIO_SOURCE: 1,
AVATAR_RIG: 2,
AUDIO_TARGET: 3,
AUDIO_ZONE: 4
});

export const AudioType = {
Stereo: "stereo",
PannerNode: "pannernode"
};

export const DistanceModelType = {
Linear: "linear",
Inverse: "inverse",
Exponential: "exponential"
};

export const AvatarAudioDefaults = Object.freeze({
DISTANCE_MODEL: DistanceModelType.Inverse,
ROLLOFF_FACTOR: 2,
REF_DISTANCE: 1,
MAX_DISTANCE: 10000,
INNER_ANGLE: 180,
OUTER_ANGLE: 360,
OUTER_GAIN: 0,
VOLUME: 1.0
});

export const MediaAudioDefaults = Object.freeze({
DISTANCE_MODEL: DistanceModelType.Inverse,
ROLLOFF_FACTOR: 1,
REF_DISTANCE: 1,
MAX_DISTANCE: 10000,
INNER_ANGLE: 360,
OUTER_ANGLE: 0,
OUTER_GAIN: 0,
VOLUME: 0.5
});

export const TargetAudioDefaults = Object.freeze({
DISTANCE_MODEL: DistanceModelType.Inverse,
ROLLOFF_FACTOR: 5,
REF_DISTANCE: 8,
MAX_DISTANCE: 10000,
INNER_ANGLE: 170,
OUTER_ANGLE: 300,
OUTER_GAIN: 0.3,
VOLUME: 1.0
});

const MUTE_DELAY_SECS = 1;

Expand All @@ -20,18 +71,23 @@ const distanceModels = {
};

AFRAME.registerComponent("audio-params", {
multiple: true,
schema: {
enabled: { default: true },
debuggable: { 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 },
coneInnerAngle: { default: AvatarAudioDefaults.INNER_ANGLE },
coneOuterAngle: { default: AvatarAudioDefaults.OUTER_ANGLE },
coneOuterGain: { default: AvatarAudioDefaults.OUTER_GAIN },
clippingEnabled: { default: CLIPPING_THRESHOLD_ENABLED },
clippingThreshold: { default: CLIPPING_THRESHOLD_DEFAULT },
prevGain: { default: 1.0 },
preClipGain: { default: 1.0 },
isClipped: { default: false },
gain: { default: 1.0 },
sourceType: { default: -1 },
Expand All @@ -41,6 +97,7 @@ AFRAME.registerComponent("audio-params", {
},

init() {
this.audioRef = null;
this.avatarRigPosition = new THREE.Vector3();
this.avatarAudio = {
panner: {
Expand Down Expand Up @@ -86,23 +143,20 @@ AFRAME.registerComponent("audio-params", {
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;
} else if (this.el.components["audio-zone"]) {
this.data.sourceType = SourceType.AUDIO_ZONE;
}
this.audioSettings = this.el.sceneEl.systems["hubs-systems"].audioSettingsSystem.audioSettings;
this.avatarRigObj = document.getElementById("avatar-rig").querySelector(".camera").object3D;
},

remove() {
Expand All @@ -112,20 +166,27 @@ AFRAME.registerComponent("audio-params", {
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);
}
},

update() {
if (this.audioRef) {
this.audioRef.setDistanceModel(this.data.distanceModel);
this.audioRef.setRolloffFactor(this.data.rolloffFactor);
this.audioRef.setRefDistance(this.data.refDistance);
this.audioRef.setMaxDistance(this.data.maxDistance);
this.audioRef.panner.coneInnerAngle = this.data.coneInnerAngle;
this.audioRef.panner.coneOuterAngle = this.data.coneOuterAngle;
this.audioRef.panner.coneOuterGain = this.data.coneOuterGain;
this.data.gain !== undefined && this.updateGain(this.data.gain);
}
},

tick() {
const audio = this.audio();
const audio = this.getAudio();
if (audio) {
if (audio.updateMatrixWorld) {
audio.updateMatrixWorld(true);
}
this.data.position = new THREE.Vector3(
audio.panner.positionX.value,
audio.panner.positionY.value,
Expand Down Expand Up @@ -157,45 +218,66 @@ AFRAME.registerComponent("audio-params", {
}
},

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;
}
getAudio: (function() {
const worldQuat = new THREE.Quaternion();

return function() {
switch (this.data.sourceType) {
case SourceType.AVATAR_RIG: {
// Create fake parametes for the avatar rig as it doens't have an audio source.
const audioParams = this.audioSettings;
this.avatarRigObj.updateMatrixWorld(true);
this.avatarRigObj.getWorldPosition(this.avatarRigPosition);
this.avatarRigOrientation.set(0, 0, -1);
this.avatarRigObj.getWorldQuaternion(worldQuat);
this.avatarRigOrientation.applyQuaternion(worldQuat);
return {
panner: {
orientationX: {
value: this.avatarRigOrientation.x
},
orientationY: {
value: this.avatarRigOrientation.y
},
orientationZ: {
value: this.avatarRigOrientation.z
},
positionX: {
value: this.avatarRigPosition.x
},
positionY: {
value: this.avatarRigPosition.y
},
positionZ: {
value: this.avatarRigPosition.z
},
distanceModel: audioParams.avatarDistanceModel,
maxDistance: audioParams.avatarMaxDistance,
refDistance: audioParams.avatarRefDistance,
rolloffFactor: audioParams.avatarRolloffFactor,
coneInnerAngle: audioParams.avatarConeInnerAngle,
coneOuterAngle: audioParams.avatarConeOuterAngle,
coneOuterGain: audioParams.avatarConeOuterGain
}
};
}
case SourceType.MEDIA_VIDEO:
case SourceType.AVATAR_AUDIO_SOURCE:
case SourceType.AUDIO_TARGET: {
return this.audioRef?.panner ? this.audioRef : null;
}
}

return null;
};
})(),

setAudio(audio) {
this.audioRef = audio;
},

enableNormalizer() {
const audio = this.audio();
const audio = this.getAudio();
if (audio) {
const avatarAudioSource = this.el.components["avatar-audio-source"];
if (avatarAudioSource) {
Expand Down Expand Up @@ -231,54 +313,54 @@ AFRAME.registerComponent("audio-params", {

clipGain(gain) {
if (!this.data.isClipped) {
const audio = this.audio();
const audio = this.getAudio();
this.data.isClipped = true;
this.data.prevGain = this.data.gain;
this.data.preClipGain = this.data.gain;
this.data.gain = gain;
this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain;
this.data.gain = Math.max(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();
const audio = this.getAudio();
this.data.isClipped = false;
this.data.gain = this.data.prevGain;
this.data.gain = this.data.gain === 0 ? 0.001 : this.data.gain;
this.data.gain = this.data.preClipGain;
this.data.gain = Math.max(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);
},
const audio = this.getAudio();
if (!audio) return;

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;
this.data.gain = Math.max(0.001, newGain);
let gainFilter;
switch (this.data.sourceType) {
case SourceType.MEDIA_VIDEO: {
gainFilter = this.el.components["media-video"].getGainFilter();
break;
}
case SourceType.AVATAR_AUDIO_SOURCE: {
gainFilter = this.el.components["avatar-audio-source"].getGainFilter();
break;
}
case SourceType.AUDIO_TARGET: {
gainFilter = this.el.components["audio-target"].getGainFilter();
break;
}
}
const volumeModifier = (globalVolume !== undefined ? globalVolume : 100) / 100;
let newGain = volumeModifier * volume;
const { audioOutputMode } = window.APP.store.state.preferences;
if (audioOutputMode === "audio") {
newGain = this.data.gain * Math.min(1, 10 / Math.max(1, this.data.squaredDistance));
this.data.gain = this.data.gain * Math.min(1, 10 / Math.max(1, this.data.squaredDistance));
}
this.updateGain(newGain);
gainFilter?.gain.exponentialRampToValueAtTime(this.data.gain, audio.context.currentTime + MUTE_DELAY_SECS);
},

clippingUpdated({ clippingEnabled, clippingThreshold }) {
updateClipping() {
const { clippingEnabled, clippingThreshold } = window.APP.store.state.preferences;
this.data.clippingEnabled = clippingEnabled !== undefined ? clippingEnabled : CLIPPING_THRESHOLD_ENABLED;
this.data.clippingThreshold = clippingThreshold !== undefined ? clippingThreshold : CLIPPING_THRESHOLD_DEFAULT;
}
Expand Down
Loading