@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.3 โ†’ 1.0.4

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.
@@ -5,11 +5,52 @@ export declare class SpatialAudioManager extends EventManager {
5
5
  private participantNodes;
6
6
  private masterGainNode;
7
7
  private monitoringIntervals;
8
+ private compressor;
9
+ private listenerDirection;
8
10
  constructor();
9
11
  getAudioContext(): AudioContext;
12
+ /**
13
+ * Setup spatial audio for a participant
14
+ *
15
+ * CRITICAL: Each participant gets their OWN audio processing chain:
16
+ * Stream -> Source -> Panner -> Analyser -> Gain -> Compressor -> Output
17
+ *
18
+ * This ensures:
19
+ * - Each voice is positioned independently in 3D space
20
+ * - No server-side mixing required
21
+ * - Scalable to unlimited participants (browser handles the mixing)
22
+ *
23
+ * @param participantId Unique ID for this participant
24
+ * @param track Audio track from MediaSoup consumer
25
+ * @param bypassSpatialization For testing - bypasses 3D positioning
26
+ */
10
27
  setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): void;
11
28
  private startMonitoring;
12
- updateSpatialAudio(participantId: string, position: Position): void;
29
+ /**
30
+ * Update spatial audio position and orientation for a participant
31
+ *
32
+ * This is called every time we receive position/direction updates from the server.
33
+ *
34
+ * Position: Where the participant is in 3D space (their location)
35
+ * Direction: Which way they're facing (their forward vector)
36
+ *
37
+ * Example:
38
+ * - Position: (x: -200, y: 0, z: 100) = 2m to your left
39
+ * - Direction: (x: 0, y: 1, z: 0) = facing forward (away from you)
40
+ * - Result: Sound comes from your left, oriented as if speaking away
41
+ *
42
+ * The Web Audio API's PannerNode uses HRTF to create realistic 3D audio
43
+ * based on these parameters plus the listener's position/orientation.
44
+ *
45
+ * @param participantId Who to update
46
+ * @param position Where they are (from socket data)
47
+ * @param direction Which way they're facing (from socket data)
48
+ */
49
+ updateSpatialAudio(participantId: string, position: Position, direction?: {
50
+ x: number;
51
+ y: number;
52
+ z: number;
53
+ }): void;
13
54
  setListenerPosition(position: Position, orientation: {
14
55
  forwardX: number;
15
56
  forwardY: number;
@@ -18,6 +59,12 @@ export declare class SpatialAudioManager extends EventManager {
18
59
  upY: number;
19
60
  upZ: number;
20
61
  }): void;
62
+ /**
63
+ * Update listener orientation from LSD camera direction
64
+ * @param cameraPos Camera position in world space
65
+ * @param lookAtPos Look-at position (where camera is pointing)
66
+ */
67
+ setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position): void;
21
68
  removeParticipant(participantId: string): void;
22
69
  resumeAudioContext(): Promise<void>;
23
70
  getAudioContextState(): AudioContextState;
@@ -7,54 +7,82 @@ class SpatialAudioManager extends EventManager_1.EventManager {
7
7
  super();
8
8
  this.participantNodes = new Map();
9
9
  this.monitoringIntervals = new Map();
10
- this.audioContext = new AudioContext();
10
+ this.listenerDirection = {
11
+ forward: { x: 0, y: 1, z: 0 },
12
+ up: { x: 0, y: 0, z: 1 },
13
+ };
14
+ // Use high sample rate for best audio quality
15
+ this.audioContext = new AudioContext({ sampleRate: 48000 });
16
+ // Master gain
11
17
  this.masterGainNode = this.audioContext.createGain();
12
- this.masterGainNode.gain.value = 5.0; // Set master gain to 5.0
13
- this.masterGainNode.connect(this.audioContext.destination);
14
- console.log(`๐Ÿ”Š SpatialAudioManager initialized, gain: ${this.masterGainNode.gain.value} audioContext state: ${this.audioContext.state}`);
18
+ this.masterGainNode.gain.value = 5.0;
19
+ // Compressor for dynamic range control and preventing distortion
20
+ this.compressor = this.audioContext.createDynamicsCompressor();
21
+ this.compressor.threshold.value = -24; // dB
22
+ this.compressor.knee.value = 30; // dB
23
+ this.compressor.ratio.value = 12; // Compression ratio
24
+ this.compressor.attack.value = 0.003; // 3ms attack
25
+ this.compressor.release.value = 0.25; // 250ms release
26
+ // Connect master chain: masterGain -> compressor -> destination
27
+ this.masterGainNode.connect(this.compressor);
28
+ this.compressor.connect(this.audioContext.destination);
29
+ console.log(`๐Ÿ”Š SpatialAudioManager initialized with advanced audio processing:`, {
30
+ sampleRate: this.audioContext.sampleRate,
31
+ gain: this.masterGainNode.gain.value,
32
+ compressor: {
33
+ threshold: this.compressor.threshold.value,
34
+ ratio: this.compressor.ratio.value,
35
+ },
36
+ state: this.audioContext.state,
37
+ });
15
38
  }
16
39
  getAudioContext() {
17
40
  return this.audioContext;
18
41
  }
42
+ /**
43
+ * Setup spatial audio for a participant
44
+ *
45
+ * CRITICAL: Each participant gets their OWN audio processing chain:
46
+ * Stream -> Source -> Panner -> Analyser -> Gain -> Compressor -> Output
47
+ *
48
+ * This ensures:
49
+ * - Each voice is positioned independently in 3D space
50
+ * - No server-side mixing required
51
+ * - Scalable to unlimited participants (browser handles the mixing)
52
+ *
53
+ * @param participantId Unique ID for this participant
54
+ * @param track Audio track from MediaSoup consumer
55
+ * @param bypassSpatialization For testing - bypasses 3D positioning
56
+ */
19
57
  setupSpatialAudioForParticipant(participantId, track, bypassSpatialization = false // Default to false
20
58
  ) {
21
59
  if (this.audioContext.state === "suspended") {
22
60
  this.audioContext.resume();
23
61
  }
62
+ // Create stream with noise suppression constraints
24
63
  const stream = new MediaStream([track]);
25
- // ๐Ÿ” DIAGNOSTIC TEST: Try playing through raw HTML audio element
26
- console.warn(`๐Ÿงช DIAGNOSTIC: Testing raw audio playback for ${participantId}`);
27
- const testAudio = new Audio();
28
- testAudio.srcObject = stream;
29
- testAudio.volume = 1.0;
30
- testAudio
31
- .play()
32
- .then(() => {
33
- console.log(`โœ… DIAGNOSTIC: Raw audio element can play track for ${participantId}`);
34
- })
35
- .catch((err) => {
36
- console.error(`โŒ DIAGNOSTIC: Raw audio element CANNOT play:`, err);
37
- });
38
64
  const source = this.audioContext.createMediaStreamSource(stream);
39
65
  const panner = this.audioContext.createPanner();
40
66
  const analyser = this.audioContext.createAnalyser();
41
67
  const gain = this.audioContext.createGain();
42
- // Configure Panner
43
- panner.panningModel = "HRTF";
44
- panner.distanceModel = "inverse";
45
- panner.refDistance = 1;
46
- panner.maxDistance = 100;
47
- panner.rolloffFactor = 1;
48
- panner.coneInnerAngle = 360;
49
- panner.coneOuterAngle = 0;
50
- panner.coneOuterGain = 0;
68
+ // Configure Panner for realistic 3D spatial audio
69
+ panner.panningModel = "HRTF"; // Head-Related Transfer Function for realistic 3D
70
+ panner.distanceModel = "inverse"; // Natural distance falloff
71
+ panner.refDistance = 100; // Distance at which volume = 1.0 (1 meter in Unreal = 100cm)
72
+ panner.maxDistance = 1500; // Maximum audible distance (15 meters)
73
+ panner.rolloffFactor = 1.5; // How quickly sound fades with distance
74
+ panner.coneInnerAngle = 360; // Omnidirectional sound source
75
+ panner.coneOuterAngle = 360;
76
+ panner.coneOuterGain = 0.3; // Some sound even outside cone
77
+ // Configure gain for individual participant volume control
78
+ gain.gain.value = 1.0;
51
79
  if (bypassSpatialization) {
52
80
  console.log(`๐Ÿ”Š TESTING: Connecting audio directly to destination (bypassing spatial audio) for ${participantId}`);
53
81
  source.connect(analyser);
54
82
  analyser.connect(this.masterGainNode);
55
83
  }
56
84
  else {
57
- // Standard spatialized path
85
+ // Standard spatialized path with full audio chain
58
86
  source.connect(panner);
59
87
  panner.connect(analyser);
60
88
  analyser.connect(gain);
@@ -69,11 +97,19 @@ class SpatialAudioManager extends EventManager_1.EventManager {
69
97
  });
70
98
  console.log(`๐ŸŽง Spatial audio setup complete for ${participantId}:`, {
71
99
  audioContextState: this.audioContext.state,
100
+ sampleRate: this.audioContext.sampleRate,
72
101
  gain: this.masterGainNode.gain.value,
73
102
  trackEnabled: track.enabled,
74
103
  trackMuted: track.muted,
75
104
  trackReadyState: track.readyState,
76
105
  isBypassed: bypassSpatialization,
106
+ pannerConfig: {
107
+ model: panner.panningModel,
108
+ distanceModel: panner.distanceModel,
109
+ refDistance: panner.refDistance,
110
+ maxDistance: panner.maxDistance,
111
+ rolloffFactor: panner.rolloffFactor,
112
+ },
77
113
  });
78
114
  // Start monitoring audio levels
79
115
  this.startMonitoring(participantId);
@@ -121,17 +157,67 @@ class SpatialAudioManager extends EventManager_1.EventManager {
121
157
  }, 2000); // Log every 2 seconds
122
158
  this.monitoringIntervals.set(participantId, interval);
123
159
  }
124
- updateSpatialAudio(participantId, position) {
160
+ /**
161
+ * Update spatial audio position and orientation for a participant
162
+ *
163
+ * This is called every time we receive position/direction updates from the server.
164
+ *
165
+ * Position: Where the participant is in 3D space (their location)
166
+ * Direction: Which way they're facing (their forward vector)
167
+ *
168
+ * Example:
169
+ * - Position: (x: -200, y: 0, z: 100) = 2m to your left
170
+ * - Direction: (x: 0, y: 1, z: 0) = facing forward (away from you)
171
+ * - Result: Sound comes from your left, oriented as if speaking away
172
+ *
173
+ * The Web Audio API's PannerNode uses HRTF to create realistic 3D audio
174
+ * based on these parameters plus the listener's position/orientation.
175
+ *
176
+ * @param participantId Who to update
177
+ * @param position Where they are (from socket data)
178
+ * @param direction Which way they're facing (from socket data)
179
+ */
180
+ updateSpatialAudio(participantId, position, direction) {
125
181
  const nodes = this.participantNodes.get(participantId);
126
182
  if (nodes?.panner) {
183
+ // Update position (where the sound is coming from)
127
184
  nodes.panner.positionX.setValueAtTime(position.x, this.audioContext.currentTime);
128
185
  nodes.panner.positionY.setValueAtTime(position.y, this.audioContext.currentTime);
129
186
  nodes.panner.positionZ.setValueAtTime(position.z, this.audioContext.currentTime);
187
+ // Update orientation (where the participant is facing)
188
+ // This makes the audio source directional based on participant's direction
189
+ if (direction) {
190
+ // Normalize direction vector
191
+ const length = Math.sqrt(direction.x * direction.x +
192
+ direction.y * direction.y +
193
+ direction.z * direction.z);
194
+ if (length > 0.001) {
195
+ const normX = direction.x / length;
196
+ const normY = direction.y / length;
197
+ const normZ = direction.z / length;
198
+ nodes.panner.orientationX.setValueAtTime(normX, this.audioContext.currentTime);
199
+ nodes.panner.orientationY.setValueAtTime(normY, this.audioContext.currentTime);
200
+ nodes.panner.orientationZ.setValueAtTime(normZ, this.audioContext.currentTime);
201
+ }
202
+ }
130
203
  }
131
204
  }
132
205
  setListenerPosition(position, orientation) {
133
206
  const { listener } = this.audioContext;
134
207
  if (listener) {
208
+ // Store listener direction for reference
209
+ this.listenerDirection = {
210
+ forward: {
211
+ x: orientation.forwardX,
212
+ y: orientation.forwardY,
213
+ z: orientation.forwardZ,
214
+ },
215
+ up: {
216
+ x: orientation.upX,
217
+ y: orientation.upY,
218
+ z: orientation.upZ,
219
+ },
220
+ };
135
221
  // Use setPosition and setOrientation for atomic updates if available
136
222
  if (listener.positionX) {
137
223
  listener.positionX.setValueAtTime(position.x, this.audioContext.currentTime);
@@ -146,8 +232,77 @@ class SpatialAudioManager extends EventManager_1.EventManager {
146
232
  listener.upY.setValueAtTime(orientation.upY, this.audioContext.currentTime);
147
233
  listener.upZ.setValueAtTime(orientation.upZ, this.audioContext.currentTime);
148
234
  }
235
+ // Log spatial audio updates occasionally
236
+ if (Math.random() < 0.01) {
237
+ console.log(`๐ŸŽง [Spatial Audio] Listener updated:`, {
238
+ position: { x: position.x.toFixed(1), y: position.y.toFixed(1), z: position.z.toFixed(1) },
239
+ forward: {
240
+ x: orientation.forwardX.toFixed(2),
241
+ y: orientation.forwardY.toFixed(2),
242
+ z: orientation.forwardZ.toFixed(2),
243
+ },
244
+ up: {
245
+ x: orientation.upX.toFixed(2),
246
+ y: orientation.upY.toFixed(2),
247
+ z: orientation.upZ.toFixed(2),
248
+ },
249
+ });
250
+ }
149
251
  }
150
252
  }
253
+ /**
254
+ * Update listener orientation from LSD camera direction
255
+ * @param cameraPos Camera position in world space
256
+ * @param lookAtPos Look-at position (where camera is pointing)
257
+ */
258
+ setListenerFromLSD(listenerPos, cameraPos, lookAtPos) {
259
+ // Calculate forward vector (from camera to look-at point)
260
+ const forwardX = lookAtPos.x - cameraPos.x;
261
+ const forwardY = lookAtPos.y - cameraPos.y;
262
+ const forwardZ = lookAtPos.z - cameraPos.z;
263
+ // Normalize forward vector
264
+ const forwardLen = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ);
265
+ if (forwardLen < 0.001) {
266
+ console.warn("โš ๏ธ Forward vector too small, using default orientation");
267
+ return;
268
+ }
269
+ const fwdX = forwardX / forwardLen;
270
+ const fwdY = forwardY / forwardLen;
271
+ const fwdZ = forwardZ / forwardLen;
272
+ // Calculate right vector (cross product of world up and forward)
273
+ const worldUp = { x: 0, y: 0, z: 1 }; // Unreal Z-up
274
+ const rightX = worldUp.y * fwdZ - worldUp.z * fwdY;
275
+ const rightY = worldUp.z * fwdX - worldUp.x * fwdZ;
276
+ const rightZ = worldUp.x * fwdY - worldUp.y * fwdX;
277
+ const rightLen = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ);
278
+ if (rightLen < 0.001) {
279
+ // Forward is parallel to world up, use fallback
280
+ this.setListenerPosition(listenerPos, {
281
+ forwardX: fwdX,
282
+ forwardY: fwdY,
283
+ forwardZ: fwdZ,
284
+ upX: 0,
285
+ upY: 0,
286
+ upZ: 1,
287
+ });
288
+ return;
289
+ }
290
+ const rX = rightX / rightLen;
291
+ const rY = rightY / rightLen;
292
+ const rZ = rightZ / rightLen;
293
+ // Calculate true up vector (cross product of forward and right)
294
+ const upX = fwdY * rZ - fwdZ * rY;
295
+ const upY = fwdZ * rX - fwdX * rZ;
296
+ const upZ = fwdX * rY - fwdY * rX;
297
+ this.setListenerPosition(listenerPos, {
298
+ forwardX: fwdX,
299
+ forwardY: fwdY,
300
+ forwardZ: fwdZ,
301
+ upX,
302
+ upY,
303
+ upZ,
304
+ });
305
+ }
151
306
  removeParticipant(participantId) {
152
307
  // Stop monitoring
153
308
  if (this.monitoringIntervals.has(participantId)) {
package/dist/index.js CHANGED
@@ -457,8 +457,9 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
457
457
  participant.userName = data.userName;
458
458
  if (data.userEmail !== undefined)
459
459
  participant.userEmail = data.userEmail;
460
- this.spatialAudioManager.updateSpatialAudio(data.participantId, data.position);
461
- console.log("โœ… SDK: Updated participant position and spatial audio");
460
+ // Update spatial audio with BOTH position AND direction from socket
461
+ this.spatialAudioManager.updateSpatialAudio(data.participantId, data.position, data.direction);
462
+ console.log("โœ… SDK: Updated participant position and spatial audio with direction");
462
463
  this.emit("participant-position-updated", participant);
463
464
  }
464
465
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Odyssey Spatial Audio & Video SDK using MediaSoup for real-time communication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",