@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SpatialAudioManager.d.ts +34 -2
- package/dist/SpatialAudioManager.js +308 -64
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import { EventManager } from "./EventManager";
|
|
2
2
|
import { Position } from "./types";
|
|
3
|
+
type SpatialAudioDistanceConfig = {
|
|
4
|
+
refDistance?: number;
|
|
5
|
+
maxDistance?: number;
|
|
6
|
+
rolloffFactor?: number;
|
|
7
|
+
unit?: "auto" | "meters" | "centimeters";
|
|
8
|
+
};
|
|
9
|
+
type DenoiserOptions = {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
threshold?: number;
|
|
12
|
+
noiseFloor?: number;
|
|
13
|
+
release?: number;
|
|
14
|
+
};
|
|
15
|
+
type SpatialAudioOptions = {
|
|
16
|
+
distance?: SpatialAudioDistanceConfig;
|
|
17
|
+
denoiser?: DenoiserOptions;
|
|
18
|
+
};
|
|
3
19
|
export declare class SpatialAudioManager extends EventManager {
|
|
4
20
|
private audioContext;
|
|
5
21
|
private participantNodes;
|
|
6
22
|
private masterGainNode;
|
|
7
23
|
private monitoringIntervals;
|
|
8
24
|
private compressor;
|
|
25
|
+
private options;
|
|
26
|
+
private denoiseWorkletReady;
|
|
27
|
+
private denoiseWorkletUrl?;
|
|
28
|
+
private denoiserWasmBytes?;
|
|
29
|
+
private listenerPosition;
|
|
30
|
+
private listenerInitialized;
|
|
9
31
|
private listenerDirection;
|
|
10
|
-
constructor();
|
|
32
|
+
constructor(options?: SpatialAudioOptions);
|
|
11
33
|
getAudioContext(): AudioContext;
|
|
12
34
|
/**
|
|
13
35
|
* Setup spatial audio for a participant
|
|
@@ -24,7 +46,7 @@ export declare class SpatialAudioManager extends EventManager {
|
|
|
24
46
|
* @param track Audio track from MediaSoup consumer
|
|
25
47
|
* @param bypassSpatialization For testing - bypasses 3D positioning
|
|
26
48
|
*/
|
|
27
|
-
setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): void
|
|
49
|
+
setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): Promise<void>;
|
|
28
50
|
private startMonitoring;
|
|
29
51
|
/**
|
|
30
52
|
* Update spatial audio position and orientation for a participant
|
|
@@ -65,7 +87,17 @@ export declare class SpatialAudioManager extends EventManager {
|
|
|
65
87
|
* @param lookAtPos Look-at position (where camera is pointing)
|
|
66
88
|
*/
|
|
67
89
|
setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position): void;
|
|
90
|
+
private applyListenerTransform;
|
|
68
91
|
removeParticipant(participantId: string): void;
|
|
69
92
|
resumeAudioContext(): Promise<void>;
|
|
70
93
|
getAudioContextState(): AudioContextState;
|
|
94
|
+
private getDistanceConfig;
|
|
95
|
+
private applySpatialBoostIfNeeded;
|
|
96
|
+
private getDistanceBetween;
|
|
97
|
+
private calculateDistanceGain;
|
|
98
|
+
private normalizePositionUnits;
|
|
99
|
+
private isDenoiserEnabled;
|
|
100
|
+
private ensureDenoiseWorklet;
|
|
101
|
+
private resolveOptions;
|
|
71
102
|
}
|
|
103
|
+
export {};
|
|
@@ -3,14 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SpatialAudioManager = void 0;
|
|
4
4
|
const EventManager_1 = require("./EventManager");
|
|
5
5
|
class SpatialAudioManager extends EventManager_1.EventManager {
|
|
6
|
-
constructor() {
|
|
6
|
+
constructor(options) {
|
|
7
7
|
super();
|
|
8
8
|
this.participantNodes = new Map();
|
|
9
9
|
this.monitoringIntervals = new Map();
|
|
10
|
+
this.denoiseWorkletReady = null;
|
|
11
|
+
this.listenerPosition = { x: 0, y: 0, z: 0 };
|
|
12
|
+
this.listenerInitialized = false;
|
|
10
13
|
this.listenerDirection = {
|
|
11
14
|
forward: { x: 0, y: 1, z: 0 },
|
|
12
15
|
up: { x: 0, y: 0, z: 1 },
|
|
13
16
|
};
|
|
17
|
+
this.options = this.resolveOptions(options);
|
|
14
18
|
// Use high sample rate for best audio quality
|
|
15
19
|
this.audioContext = new AudioContext({ sampleRate: 48000 });
|
|
16
20
|
// Master gain
|
|
@@ -54,10 +58,10 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
54
58
|
* @param track Audio track from MediaSoup consumer
|
|
55
59
|
* @param bypassSpatialization For testing - bypasses 3D positioning
|
|
56
60
|
*/
|
|
57
|
-
setupSpatialAudioForParticipant(participantId, track, bypassSpatialization = false // Default to false
|
|
61
|
+
async setupSpatialAudioForParticipant(participantId, track, bypassSpatialization = false // Default to false
|
|
58
62
|
) {
|
|
59
63
|
if (this.audioContext.state === "suspended") {
|
|
60
|
-
this.audioContext.resume();
|
|
64
|
+
await this.audioContext.resume();
|
|
61
65
|
}
|
|
62
66
|
// Create stream with noise suppression constraints
|
|
63
67
|
const stream = new MediaStream([track]);
|
|
@@ -65,6 +69,29 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
65
69
|
const panner = this.audioContext.createPanner();
|
|
66
70
|
const analyser = this.audioContext.createAnalyser();
|
|
67
71
|
const gain = this.audioContext.createGain();
|
|
72
|
+
let denoiseNode;
|
|
73
|
+
if (this.isDenoiserEnabled() && typeof this.audioContext.audioWorklet !== "undefined") {
|
|
74
|
+
try {
|
|
75
|
+
await this.ensureDenoiseWorklet();
|
|
76
|
+
denoiseNode = new AudioWorkletNode(this.audioContext, "odyssey-denoise", {
|
|
77
|
+
numberOfInputs: 1,
|
|
78
|
+
numberOfOutputs: 1,
|
|
79
|
+
processorOptions: {
|
|
80
|
+
enabled: this.options.denoiser?.enabled !== false,
|
|
81
|
+
threshold: this.options.denoiser?.threshold,
|
|
82
|
+
noiseFloor: this.options.denoiser?.noiseFloor,
|
|
83
|
+
release: this.options.denoiser?.release,
|
|
84
|
+
wasmBytes: this.denoiserWasmBytes
|
|
85
|
+
? this.denoiserWasmBytes.slice(0)
|
|
86
|
+
: null,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.warn("⚠️ Failed to initialize denoiser worklet. Falling back to raw audio.", error);
|
|
92
|
+
denoiseNode = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
68
95
|
// Create BiquadFilter nodes for static/noise reduction
|
|
69
96
|
// Based on: https://tagdiwalaviral.medium.com/struggles-of-noise-reduction-in-rtc-part-2-2526f8179442
|
|
70
97
|
const highpassFilter = this.audioContext.createBiquadFilter();
|
|
@@ -76,28 +103,32 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
76
103
|
lowpassFilter.frequency.value = 7500; // Below 8kHz to avoid flat/muffled sound
|
|
77
104
|
lowpassFilter.Q.value = 1.0; // Quality factor
|
|
78
105
|
// Configure Panner for realistic 3D spatial audio
|
|
106
|
+
const distanceConfig = this.getDistanceConfig();
|
|
79
107
|
panner.panningModel = "HRTF"; // Head-Related Transfer Function for realistic 3D
|
|
80
108
|
panner.distanceModel = "inverse"; // Natural distance falloff
|
|
81
|
-
panner.refDistance =
|
|
82
|
-
panner.maxDistance =
|
|
83
|
-
panner.rolloffFactor = 1.
|
|
109
|
+
panner.refDistance = distanceConfig.refDistance ?? 1.2;
|
|
110
|
+
panner.maxDistance = distanceConfig.maxDistance ?? 30;
|
|
111
|
+
panner.rolloffFactor = distanceConfig.rolloffFactor ?? 1.35; // How quickly sound fades with distance
|
|
84
112
|
panner.coneInnerAngle = 360; // Omnidirectional sound source
|
|
85
113
|
panner.coneOuterAngle = 360;
|
|
86
114
|
panner.coneOuterGain = 0.3; // Some sound even outside cone
|
|
87
115
|
// Configure gain for individual participant volume control
|
|
88
116
|
gain.gain.value = 1.0;
|
|
117
|
+
let currentNode = source;
|
|
118
|
+
if (denoiseNode) {
|
|
119
|
+
currentNode.connect(denoiseNode);
|
|
120
|
+
currentNode = denoiseNode;
|
|
121
|
+
}
|
|
122
|
+
currentNode.connect(highpassFilter);
|
|
123
|
+
highpassFilter.connect(lowpassFilter);
|
|
89
124
|
if (bypassSpatialization) {
|
|
90
125
|
console.log(`🔊 TESTING: Connecting audio directly to destination (bypassing spatial audio) for ${participantId}`);
|
|
91
|
-
source.connect(highpassFilter);
|
|
92
|
-
highpassFilter.connect(lowpassFilter);
|
|
93
126
|
lowpassFilter.connect(analyser);
|
|
94
127
|
analyser.connect(this.masterGainNode);
|
|
95
128
|
}
|
|
96
129
|
else {
|
|
97
130
|
// Standard spatialized path with full audio chain
|
|
98
131
|
// Audio Chain: source -> filters -> panner -> analyser -> gain -> masterGain -> compressor -> destination
|
|
99
|
-
source.connect(highpassFilter);
|
|
100
|
-
highpassFilter.connect(lowpassFilter);
|
|
101
132
|
lowpassFilter.connect(panner);
|
|
102
133
|
panner.connect(analyser);
|
|
103
134
|
analyser.connect(gain);
|
|
@@ -110,6 +141,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
110
141
|
gain,
|
|
111
142
|
highpassFilter,
|
|
112
143
|
lowpassFilter,
|
|
144
|
+
denoiseNode,
|
|
113
145
|
stream,
|
|
114
146
|
});
|
|
115
147
|
console.log(`🎧 Spatial audio setup complete for ${participantId}:`, {
|
|
@@ -197,10 +229,13 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
197
229
|
updateSpatialAudio(participantId, position, direction) {
|
|
198
230
|
const nodes = this.participantNodes.get(participantId);
|
|
199
231
|
if (nodes?.panner) {
|
|
232
|
+
const distanceConfig = this.getDistanceConfig();
|
|
233
|
+
const normalizedPosition = this.normalizePositionUnits(position);
|
|
234
|
+
const targetPosition = this.applySpatialBoostIfNeeded(normalizedPosition);
|
|
200
235
|
// Update position (where the sound is coming from)
|
|
201
|
-
nodes.panner.positionX.setValueAtTime(
|
|
202
|
-
nodes.panner.positionY.setValueAtTime(
|
|
203
|
-
nodes.panner.positionZ.setValueAtTime(
|
|
236
|
+
nodes.panner.positionX.setValueAtTime(targetPosition.x, this.audioContext.currentTime);
|
|
237
|
+
nodes.panner.positionY.setValueAtTime(targetPosition.y, this.audioContext.currentTime);
|
|
238
|
+
nodes.panner.positionZ.setValueAtTime(targetPosition.z, this.audioContext.currentTime);
|
|
204
239
|
// Update orientation (where the participant is facing)
|
|
205
240
|
// This makes the audio source directional based on participant's direction
|
|
206
241
|
if (direction) {
|
|
@@ -217,66 +252,36 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
217
252
|
nodes.panner.orientationZ.setValueAtTime(normZ, this.audioContext.currentTime);
|
|
218
253
|
}
|
|
219
254
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
y: orientation.forwardY,
|
|
230
|
-
z: orientation.forwardZ,
|
|
231
|
-
},
|
|
232
|
-
up: {
|
|
233
|
-
x: orientation.upX,
|
|
234
|
-
y: orientation.upY,
|
|
235
|
-
z: orientation.upZ,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
// Use setPosition and setOrientation for atomic updates if available
|
|
239
|
-
if (listener.positionX) {
|
|
240
|
-
listener.positionX.setValueAtTime(position.x, this.audioContext.currentTime);
|
|
241
|
-
listener.positionY.setValueAtTime(position.y, this.audioContext.currentTime);
|
|
242
|
-
listener.positionZ.setValueAtTime(position.z, this.audioContext.currentTime);
|
|
243
|
-
}
|
|
244
|
-
if (listener.forwardX) {
|
|
245
|
-
listener.forwardX.setValueAtTime(orientation.forwardX, this.audioContext.currentTime);
|
|
246
|
-
listener.forwardY.setValueAtTime(orientation.forwardY, this.audioContext.currentTime);
|
|
247
|
-
listener.forwardZ.setValueAtTime(orientation.forwardZ, this.audioContext.currentTime);
|
|
248
|
-
listener.upX.setValueAtTime(orientation.upX, this.audioContext.currentTime);
|
|
249
|
-
listener.upY.setValueAtTime(orientation.upY, this.audioContext.currentTime);
|
|
250
|
-
listener.upZ.setValueAtTime(orientation.upZ, this.audioContext.currentTime);
|
|
251
|
-
}
|
|
252
|
-
// Log spatial audio updates occasionally
|
|
253
|
-
if (Math.random() < 0.01) {
|
|
254
|
-
console.log(`🎧 [Spatial Audio] Listener updated:`, {
|
|
255
|
-
position: { x: position.x.toFixed(1), y: position.y.toFixed(1), z: position.z.toFixed(1) },
|
|
256
|
-
forward: {
|
|
257
|
-
x: orientation.forwardX.toFixed(2),
|
|
258
|
-
y: orientation.forwardY.toFixed(2),
|
|
259
|
-
z: orientation.forwardZ.toFixed(2),
|
|
260
|
-
},
|
|
261
|
-
up: {
|
|
262
|
-
x: orientation.upX.toFixed(2),
|
|
263
|
-
y: orientation.upY.toFixed(2),
|
|
264
|
-
z: orientation.upZ.toFixed(2),
|
|
265
|
-
},
|
|
255
|
+
const listenerPos = this.listenerPosition;
|
|
256
|
+
const distance = this.getDistanceBetween(listenerPos, targetPosition);
|
|
257
|
+
const distanceGain = this.calculateDistanceGain(distanceConfig, distance);
|
|
258
|
+
nodes.gain.gain.setTargetAtTime(distanceGain, this.audioContext.currentTime, 0.05);
|
|
259
|
+
if (Math.random() < 0.02) {
|
|
260
|
+
console.log("🎚️ [Spatial Audio] Distance gain", {
|
|
261
|
+
participantId,
|
|
262
|
+
distance: distance.toFixed(2),
|
|
263
|
+
gain: distanceGain.toFixed(2),
|
|
266
264
|
});
|
|
267
265
|
}
|
|
268
266
|
}
|
|
269
267
|
}
|
|
268
|
+
setListenerPosition(position, orientation) {
|
|
269
|
+
const normalizedPosition = this.normalizePositionUnits(position);
|
|
270
|
+
this.applyListenerTransform(normalizedPosition, orientation);
|
|
271
|
+
}
|
|
270
272
|
/**
|
|
271
273
|
* Update listener orientation from LSD camera direction
|
|
272
274
|
* @param cameraPos Camera position in world space
|
|
273
275
|
* @param lookAtPos Look-at position (where camera is pointing)
|
|
274
276
|
*/
|
|
275
277
|
setListenerFromLSD(listenerPos, cameraPos, lookAtPos) {
|
|
278
|
+
const normalizedListener = this.normalizePositionUnits(listenerPos);
|
|
279
|
+
const normalizedCamera = this.normalizePositionUnits(cameraPos);
|
|
280
|
+
const normalizedLookAt = this.normalizePositionUnits(lookAtPos);
|
|
276
281
|
// Calculate forward vector (from camera to look-at point)
|
|
277
|
-
const forwardX =
|
|
278
|
-
const forwardY =
|
|
279
|
-
const forwardZ =
|
|
282
|
+
const forwardX = normalizedLookAt.x - normalizedCamera.x;
|
|
283
|
+
const forwardY = normalizedLookAt.y - normalizedCamera.y;
|
|
284
|
+
const forwardZ = normalizedLookAt.z - normalizedCamera.z;
|
|
280
285
|
// Normalize forward vector
|
|
281
286
|
const forwardLen = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ);
|
|
282
287
|
if (forwardLen < 0.001) {
|
|
@@ -294,7 +299,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
294
299
|
const rightLen = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ);
|
|
295
300
|
if (rightLen < 0.001) {
|
|
296
301
|
// Forward is parallel to world up, use fallback
|
|
297
|
-
this.
|
|
302
|
+
this.applyListenerTransform(normalizedListener, {
|
|
298
303
|
forwardX: fwdX,
|
|
299
304
|
forwardY: fwdY,
|
|
300
305
|
forwardZ: fwdZ,
|
|
@@ -311,7 +316,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
311
316
|
const upX = fwdY * rZ - fwdZ * rY;
|
|
312
317
|
const upY = fwdZ * rX - fwdX * rZ;
|
|
313
318
|
const upZ = fwdX * rY - fwdY * rX;
|
|
314
|
-
this.
|
|
319
|
+
this.applyListenerTransform(normalizedListener, {
|
|
315
320
|
forwardX: fwdX,
|
|
316
321
|
forwardY: fwdY,
|
|
317
322
|
forwardZ: fwdZ,
|
|
@@ -320,6 +325,58 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
320
325
|
upZ,
|
|
321
326
|
});
|
|
322
327
|
}
|
|
328
|
+
applyListenerTransform(normalizedPosition, orientation) {
|
|
329
|
+
const { listener } = this.audioContext;
|
|
330
|
+
if (!listener) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.listenerPosition = { ...normalizedPosition };
|
|
334
|
+
this.listenerInitialized = true;
|
|
335
|
+
this.listenerDirection = {
|
|
336
|
+
forward: {
|
|
337
|
+
x: orientation.forwardX,
|
|
338
|
+
y: orientation.forwardY,
|
|
339
|
+
z: orientation.forwardZ,
|
|
340
|
+
},
|
|
341
|
+
up: {
|
|
342
|
+
x: orientation.upX,
|
|
343
|
+
y: orientation.upY,
|
|
344
|
+
z: orientation.upZ,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
if (listener.positionX) {
|
|
348
|
+
listener.positionX.setValueAtTime(normalizedPosition.x, this.audioContext.currentTime);
|
|
349
|
+
listener.positionY.setValueAtTime(normalizedPosition.y, this.audioContext.currentTime);
|
|
350
|
+
listener.positionZ.setValueAtTime(normalizedPosition.z, this.audioContext.currentTime);
|
|
351
|
+
}
|
|
352
|
+
if (listener.forwardX) {
|
|
353
|
+
listener.forwardX.setValueAtTime(orientation.forwardX, this.audioContext.currentTime);
|
|
354
|
+
listener.forwardY.setValueAtTime(orientation.forwardY, this.audioContext.currentTime);
|
|
355
|
+
listener.forwardZ.setValueAtTime(orientation.forwardZ, this.audioContext.currentTime);
|
|
356
|
+
listener.upX.setValueAtTime(orientation.upX, this.audioContext.currentTime);
|
|
357
|
+
listener.upY.setValueAtTime(orientation.upY, this.audioContext.currentTime);
|
|
358
|
+
listener.upZ.setValueAtTime(orientation.upZ, this.audioContext.currentTime);
|
|
359
|
+
}
|
|
360
|
+
if (Math.random() < 0.01) {
|
|
361
|
+
console.log(`🎧 [Spatial Audio] Listener updated:`, {
|
|
362
|
+
position: {
|
|
363
|
+
x: normalizedPosition.x.toFixed(2),
|
|
364
|
+
y: normalizedPosition.y.toFixed(2),
|
|
365
|
+
z: normalizedPosition.z.toFixed(2),
|
|
366
|
+
},
|
|
367
|
+
forward: {
|
|
368
|
+
x: orientation.forwardX.toFixed(2),
|
|
369
|
+
y: orientation.forwardY.toFixed(2),
|
|
370
|
+
z: orientation.forwardZ.toFixed(2),
|
|
371
|
+
},
|
|
372
|
+
up: {
|
|
373
|
+
x: orientation.upX.toFixed(2),
|
|
374
|
+
y: orientation.upY.toFixed(2),
|
|
375
|
+
z: orientation.upZ.toFixed(2),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
323
380
|
removeParticipant(participantId) {
|
|
324
381
|
// Stop monitoring
|
|
325
382
|
if (this.monitoringIntervals.has(participantId)) {
|
|
@@ -332,6 +389,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
332
389
|
nodes.panner.disconnect();
|
|
333
390
|
nodes.analyser.disconnect();
|
|
334
391
|
nodes.gain.disconnect();
|
|
392
|
+
if (nodes.denoiseNode) {
|
|
393
|
+
nodes.denoiseNode.disconnect();
|
|
394
|
+
}
|
|
335
395
|
nodes.stream.getTracks().forEach((track) => track.stop());
|
|
336
396
|
this.participantNodes.delete(participantId);
|
|
337
397
|
console.log(`🗑️ Removed participant ${participantId} from spatial audio.`);
|
|
@@ -346,5 +406,189 @@ class SpatialAudioManager extends EventManager_1.EventManager {
|
|
|
346
406
|
getAudioContextState() {
|
|
347
407
|
return this.audioContext.state;
|
|
348
408
|
}
|
|
409
|
+
getDistanceConfig() {
|
|
410
|
+
return {
|
|
411
|
+
refDistance: this.options.distance?.refDistance ?? 1.2,
|
|
412
|
+
maxDistance: this.options.distance?.maxDistance ?? 30,
|
|
413
|
+
rolloffFactor: this.options.distance?.rolloffFactor ?? 1.35,
|
|
414
|
+
unit: this.options.distance?.unit ?? "auto",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
applySpatialBoostIfNeeded(position) {
|
|
418
|
+
if (!this.listenerInitialized) {
|
|
419
|
+
return position;
|
|
420
|
+
}
|
|
421
|
+
const boost = (this.options.distance?.rolloffFactor || 1) * 0.85;
|
|
422
|
+
if (!isFinite(boost) || boost <= 1.01) {
|
|
423
|
+
return position;
|
|
424
|
+
}
|
|
425
|
+
const listener = this.listenerPosition;
|
|
426
|
+
return {
|
|
427
|
+
x: listener.x + (position.x - listener.x) * boost,
|
|
428
|
+
y: listener.y + (position.y - listener.y) * Math.min(boost, 1.2),
|
|
429
|
+
z: listener.z + (position.z - listener.z) * boost,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
getDistanceBetween(a, b) {
|
|
433
|
+
const dx = b.x - a.x;
|
|
434
|
+
const dy = b.y - a.y;
|
|
435
|
+
const dz = b.z - a.z;
|
|
436
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
437
|
+
}
|
|
438
|
+
calculateDistanceGain(config, distance) {
|
|
439
|
+
if (!this.listenerInitialized) {
|
|
440
|
+
return 1;
|
|
441
|
+
}
|
|
442
|
+
if (distance <= config.refDistance) {
|
|
443
|
+
return 1;
|
|
444
|
+
}
|
|
445
|
+
if (distance >= config.maxDistance) {
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
const normalized = (distance - config.refDistance) /
|
|
449
|
+
Math.max(config.maxDistance - config.refDistance, 0.001);
|
|
450
|
+
const shaped = Math.pow(Math.max(0, 1 - normalized), Math.max(1.2, config.rolloffFactor * 1.05));
|
|
451
|
+
return Math.min(1, Math.max(0.01, shaped));
|
|
452
|
+
}
|
|
453
|
+
normalizePositionUnits(position) {
|
|
454
|
+
const distanceConfig = this.getDistanceConfig();
|
|
455
|
+
if (distanceConfig.unit === "meters") {
|
|
456
|
+
return { ...position };
|
|
457
|
+
}
|
|
458
|
+
if (distanceConfig.unit === "centimeters") {
|
|
459
|
+
return {
|
|
460
|
+
x: position.x / 100,
|
|
461
|
+
y: position.y / 100,
|
|
462
|
+
z: position.z / 100,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const maxAxis = Math.max(Math.abs(position.x), Math.abs(position.y), Math.abs(position.z));
|
|
466
|
+
if (maxAxis > 50) {
|
|
467
|
+
// Likely centimeters coming from server
|
|
468
|
+
return {
|
|
469
|
+
x: position.x / 100,
|
|
470
|
+
y: position.y / 100,
|
|
471
|
+
z: position.z / 100,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return { ...position };
|
|
475
|
+
}
|
|
476
|
+
isDenoiserEnabled() {
|
|
477
|
+
return this.options.denoiser?.enabled !== false;
|
|
478
|
+
}
|
|
479
|
+
async ensureDenoiseWorklet() {
|
|
480
|
+
if (!this.isDenoiserEnabled()) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (!("audioWorklet" in this.audioContext)) {
|
|
484
|
+
console.warn("⚠️ AudioWorklet not supported in this browser. Disabling denoiser.");
|
|
485
|
+
this.options.denoiser = {
|
|
486
|
+
...(this.options.denoiser || {}),
|
|
487
|
+
enabled: false,
|
|
488
|
+
};
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (this.denoiseWorkletReady) {
|
|
492
|
+
return this.denoiseWorkletReady;
|
|
493
|
+
}
|
|
494
|
+
const processorSource = `class OdysseyDenoiseProcessor extends AudioWorkletProcessor {
|
|
495
|
+
constructor(options) {
|
|
496
|
+
super();
|
|
497
|
+
const cfg = (options && options.processorOptions) || {};
|
|
498
|
+
this.enabled = cfg.enabled !== false;
|
|
499
|
+
this.threshold = typeof cfg.threshold === 'number' ? cfg.threshold : 0.012;
|
|
500
|
+
this.noiseFloor = typeof cfg.noiseFloor === 'number' ? cfg.noiseFloor : 0.004;
|
|
501
|
+
this.release = typeof cfg.release === 'number' ? cfg.release : 0.18;
|
|
502
|
+
this.smoothedLevel = this.noiseFloor;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
process(inputs, outputs) {
|
|
506
|
+
const input = inputs[0];
|
|
507
|
+
const output = outputs[0];
|
|
508
|
+
if (!input || input.length === 0 || !output || output.length === 0) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (let channel = 0; channel < input.length; channel++) {
|
|
513
|
+
const inChannel = input[channel];
|
|
514
|
+
const outChannel = output[channel];
|
|
515
|
+
if (!inChannel || !outChannel) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let sum = 0;
|
|
520
|
+
for (let i = 0; i < inChannel.length; i++) {
|
|
521
|
+
const sample = inChannel[i];
|
|
522
|
+
sum += sample * sample;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const rms = Math.sqrt(sum / inChannel.length);
|
|
526
|
+
this.smoothedLevel += (rms - this.smoothedLevel) * this.release;
|
|
527
|
+
const dynamicThreshold = Math.max(
|
|
528
|
+
this.noiseFloor,
|
|
529
|
+
this.threshold * 0.6 + this.smoothedLevel * 0.4
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (!this.enabled || rms >= dynamicThreshold) {
|
|
533
|
+
for (let i = 0; i < inChannel.length; i++) {
|
|
534
|
+
outChannel[i] = inChannel[i];
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
for (let i = 0; i < inChannel.length; i++) {
|
|
538
|
+
outChannel[i] = 0;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
|
|
548
|
+
`;
|
|
549
|
+
const blob = new Blob([processorSource], {
|
|
550
|
+
type: "application/javascript",
|
|
551
|
+
});
|
|
552
|
+
this.denoiseWorkletUrl = URL.createObjectURL(blob);
|
|
553
|
+
this.denoiseWorkletReady = this.audioContext.audioWorklet
|
|
554
|
+
.addModule(this.denoiseWorkletUrl)
|
|
555
|
+
.catch((error) => {
|
|
556
|
+
console.error("❌ Failed to register denoise worklet", error);
|
|
557
|
+
this.options.denoiser = {
|
|
558
|
+
...(this.options.denoiser || {}),
|
|
559
|
+
enabled: false,
|
|
560
|
+
};
|
|
561
|
+
throw error;
|
|
562
|
+
});
|
|
563
|
+
return this.denoiseWorkletReady;
|
|
564
|
+
}
|
|
565
|
+
resolveOptions(options) {
|
|
566
|
+
const distanceDefaults = {
|
|
567
|
+
refDistance: 1.2,
|
|
568
|
+
maxDistance: 30,
|
|
569
|
+
rolloffFactor: 1.35,
|
|
570
|
+
unit: "auto",
|
|
571
|
+
};
|
|
572
|
+
const denoiserDefaults = {
|
|
573
|
+
enabled: true,
|
|
574
|
+
threshold: 0.012,
|
|
575
|
+
noiseFloor: 0.004,
|
|
576
|
+
release: 0.18,
|
|
577
|
+
};
|
|
578
|
+
return {
|
|
579
|
+
distance: {
|
|
580
|
+
refDistance: options?.distance?.refDistance ?? distanceDefaults.refDistance,
|
|
581
|
+
maxDistance: options?.distance?.maxDistance ?? distanceDefaults.maxDistance,
|
|
582
|
+
rolloffFactor: options?.distance?.rolloffFactor ?? distanceDefaults.rolloffFactor,
|
|
583
|
+
unit: options?.distance?.unit ?? distanceDefaults.unit,
|
|
584
|
+
},
|
|
585
|
+
denoiser: {
|
|
586
|
+
enabled: options?.denoiser?.enabled ?? denoiserDefaults.enabled,
|
|
587
|
+
threshold: options?.denoiser?.threshold ?? denoiserDefaults.threshold,
|
|
588
|
+
noiseFloor: options?.denoiser?.noiseFloor ?? denoiserDefaults.noiseFloor,
|
|
589
|
+
release: options?.denoiser?.release ?? denoiserDefaults.release,
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
349
593
|
}
|
|
350
594
|
exports.SpatialAudioManager = SpatialAudioManager;
|
package/dist/index.js
CHANGED
|
@@ -425,7 +425,7 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
|
|
|
425
425
|
else {
|
|
426
426
|
console.log("🎧 SDK: Setting up spatial audio for REMOTE participant", participant.participantId);
|
|
427
427
|
// Setup spatial audio with full 3D positioning
|
|
428
|
-
this.spatialAudioManager.setupSpatialAudioForParticipant(participant.participantId, track, false // Enable spatial audio
|
|
428
|
+
await this.spatialAudioManager.setupSpatialAudioForParticipant(participant.participantId, track, false // Enable spatial audio
|
|
429
429
|
);
|
|
430
430
|
// Update spatial audio position for this participant
|
|
431
431
|
this.spatialAudioManager.updateSpatialAudio(participant.participantId, data.position);
|
package/package.json
CHANGED