@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.75 → 1.0.78

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.
@@ -135,14 +135,19 @@ export declare class SpatialAudioManager extends EventManager {
135
135
  upZ: number;
136
136
  }): void;
137
137
  /**
138
- * Update listener orientation from LSD camera direction
138
+ * POSITION-ONLY MODE: Set listener HEAD position (no direction needed)
139
139
  * IMPORTANT: Uses CAMERA position (head) as listener, not body position!
140
140
  *
141
141
  * @param listenerPos Player body position (for reference, not used as listener)
142
142
  * @param cameraPos Camera/HEAD position - THIS is the actual listener position for audio
143
- * @param lookAtPos Look-at position (where camera is pointing)
143
+ * @param lookAtPos Look-at position (where camera is pointing) - stored but not used for panning
144
+ * @param rot Rotation data (pitch, yaw, roll) - stored but not used for panning
144
145
  */
145
- setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position): void;
146
+ setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position, rot?: {
147
+ x: number;
148
+ y: number;
149
+ z: number;
150
+ }): void;
146
151
  private applyListenerTransform;
147
152
  removeParticipant(participantId: string): void;
148
153
  resumeAudioContext(): Promise<void>;
@@ -172,6 +177,13 @@ export declare class SpatialAudioManager extends EventManager {
172
177
  */
173
178
  private computeHeadPosition;
174
179
  /**
180
+ * Calculate stereo panning based on X-axis position difference ONLY
181
+ * @param dx Horizontal difference (speaker.x - listener.x)
182
+ * @returns { left: 0-100, right: 0-100 }
183
+ */
184
+ private calculatePositionBasedPanning;
185
+ /**
186
+ * OLD METHOD - Kept for reference but not used in position-only mode
175
187
  * Calculate angle between listener and sound source in degrees (0-360)
176
188
  * 0° = front, 90° = right, 180° = back, 270° = left
177
189
  */
@@ -122,6 +122,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
122
122
  const monoGainR = this.audioContext.createGain();
123
123
  monoGainL.gain.value = 0.5;
124
124
  monoGainR.gain.value = 0.5;
125
+ // CRITICAL: Convert mono back to stereo for StereoPanner
126
+ // monoMerger outputs 1 channel, but StereoPanner needs 2 channels
127
+ const stereoUpmixer = this.audioContext.createChannelMerger(2);
125
128
  const analyser = this.audioContext.createAnalyser();
126
129
  const gain = this.audioContext.createGain();
127
130
  const proximityGain = this.audioContext.createGain();
@@ -228,13 +231,16 @@ class SpatialAudioManager extends EventManager_1.EventManager {
228
231
  voiceBandFilter.connect(lowpassFilter);
229
232
  lowpassFilter.connect(dynamicLowpass);
230
233
  dynamicLowpass.connect(proximityGain);
231
- // Base routing (always): proximityGain -> mono downmix -> analyser
234
+ // Base routing (always): proximityGain -> mono downmix -> stereo upmix -> analyser
232
235
  proximityGain.connect(monoSplitter);
233
236
  monoSplitter.connect(monoGainL, 0);
234
237
  monoSplitter.connect(monoGainR, 1);
235
238
  monoGainL.connect(monoMerger, 0, 0);
236
239
  monoGainR.connect(monoMerger, 0, 0);
237
- monoMerger.connect(analyser);
240
+ // Convert mono to stereo (same signal on both channels) for StereoPanner
241
+ monoMerger.connect(stereoUpmixer, 0, 0); // mono -> left channel
242
+ monoMerger.connect(stereoUpmixer, 0, 1); // mono -> right channel
243
+ stereoUpmixer.connect(analyser);
238
244
  // Output routing depends on spatialization mode:
239
245
  // - Spatial: analyser -> stereoPanner -> gain -> master
240
246
  // - Non-spatial: analyser -> gain -> master
@@ -254,6 +260,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
254
260
  monoGainL,
255
261
  monoGainR,
256
262
  monoMerger,
263
+ stereoUpmixer,
257
264
  analyser,
258
265
  gain,
259
266
  proximityGain,
@@ -348,17 +355,20 @@ class SpatialAudioManager extends EventManager_1.EventManager {
348
355
  return;
349
356
  }
350
357
  if (nodes?.panner) {
351
- // CRITICAL FIX: position is BODY position (feet), not head position
352
- // Compute speaker's head/mouth position for accurate spatial audio
358
+ // POSITION-ONLY SPATIAL AUDIO
359
+ // Compute speaker's head position from body position
353
360
  const normalizedBodyPosition = this.normalizePositionUnits(position);
354
361
  const speakerHeadPosition = this.computeHeadPosition(normalizedBodyPosition);
355
- const listenerPos = this.listenerPosition; // This is HEAD position (from setListenerFromLSD)
356
- // Calculate distance (in meters) - from listener HEAD to speaker HEAD
362
+ const listenerPos = this.listenerPosition; // This is listener HEAD position
363
+ // 🎧 [SDK Rx] Log received data
364
+ console.log(`🎧 [SDK Rx] ${participantId.substring(0, 8)} ` +
365
+ `bodyPos=(${normalizedBodyPosition.x.toFixed(2)}, ${normalizedBodyPosition.y.toFixed(2)}, ${normalizedBodyPosition.z.toFixed(2)}) ` +
366
+ `rot=(${direction?.x.toFixed(1) || 'N/A'}, ${direction?.y.toFixed(1) || 'N/A'}, ${direction?.z.toFixed(1) || 'N/A'})`);
367
+ // Calculate 3D distance (includes all axes)
357
368
  const distance = this.getDistanceBetween(listenerPos, speakerHeadPosition);
358
- // Calculate angle between listener HEAD and speaker HEAD
359
- const angle = this.calculateAngle(listenerPos, speakerHeadPosition, this.listenerDirection.forward);
360
- // Calculate stereo panning based on angle
361
- const panning = this.calculatePanning(angle);
369
+ // Calculate panning based ONLY on X-axis position difference
370
+ const dx = speakerHeadPosition.x - listenerPos.x;
371
+ const panning = this.calculatePositionBasedPanning(dx);
362
372
  // Calculate logarithmic gain based on distance
363
373
  const gain = this.calculateLogarithmicGain(distance);
364
374
  // Apply panning
@@ -383,46 +393,15 @@ class SpatialAudioManager extends EventManager_1.EventManager {
383
393
  // Log every 1000ms (1 second) for easier debugging
384
394
  if (now - lastLog > 1000) {
385
395
  this.lastLogTime.set(participantId, now);
396
+ // Position-based spatial audio log (already calculated dx above)
386
397
  const panValue = ((panning.right - panning.left) / 100).toFixed(2);
387
- const dx = speakerHeadPosition.x - listenerPos.x;
388
- const dz = speakerHeadPosition.z - listenerPos.z;
389
- const fwd = this.listenerDirection.forward;
390
- const fLen = Math.hypot(fwd.x, fwd.z);
391
- const fx = fLen > 1e-4 ? fwd.x / fLen : 0;
392
- const fz = fLen > 1e-4 ? fwd.z / fLen : 1;
393
- const vLen = Math.hypot(dx, dz);
394
- const nx = vLen > 1e-4 ? dx / vLen : 0;
395
- const nz = vLen > 1e-4 ? dz / vLen : 1;
396
- const dot = fx * nx + fz * nz;
397
- const cross = fx * nz - fz * nx;
398
- // Right vector in X/Z plane (clockwise rotation of forward)
399
- const rx = fz;
400
- const rz = -fx;
401
- const dotRight = rx * nx + rz * nz;
402
- const dotForward = dot;
403
- const side = Math.abs(dotRight) < 0.15 && dotForward > 0.35
404
- ? "front"
405
- : Math.abs(dotRight) < 0.15 && dotForward < -0.35
406
- ? "back"
407
- : dotRight > 0
408
- ? "right"
409
- : "left";
410
- let rawDeg = (Math.atan2(cross, dot) * 180) / Math.PI;
411
- if (rawDeg < 0)
412
- rawDeg += 360;
413
- const computedAngle = (360 - rawDeg) % 360;
414
- // Avoid console.group() because some consoles hide/collapse it.
415
398
  console.log(`🎧 SPATIAL AUDIO [${participantId.substring(0, 8)}] ` +
416
- `dist=${distance.toFixed(2)}m angle=${angle.toFixed(0)}° ` +
399
+ `dist=${distance.toFixed(2)}m ` +
400
+ `dx=${dx.toFixed(2)}m ` +
417
401
  `pan(L=${panning.left.toFixed(0)}%,R=${panning.right.toFixed(0)}%,v=${panValue}) ` +
418
402
  `gain=${gain.toFixed(0)}% ` +
419
- `fwdXZ=(${fx.toFixed(2)},${fz.toFixed(2)}) ` +
420
- `dXZ=(${dx.toFixed(2)},${dz.toFixed(2)}) ` +
421
- `dotF=${dotForward.toFixed(2)} dotR=${dotRight.toFixed(2)} side=${side} ` +
422
- `raw=${rawDeg.toFixed(0)}° calc=${computedAngle.toFixed(0)}° ` +
423
- `you=(${listenerPos.x.toFixed(1)},${listenerPos.y.toFixed(1)},${listenerPos.z.toFixed(1)}) ` +
424
- `them=(${speakerHeadPosition.x.toFixed(1)},${speakerHeadPosition.y.toFixed(1)},${speakerHeadPosition.z.toFixed(1)}) ` +
425
- `body=(${normalizedBodyPosition.x.toFixed(1)},${normalizedBodyPosition.y.toFixed(1)},${normalizedBodyPosition.z.toFixed(1)})`);
403
+ `you=(${listenerPos.x.toFixed(2)},${listenerPos.y.toFixed(2)},${listenerPos.z.toFixed(2)}) ` +
404
+ `them=(${speakerHeadPosition.x.toFixed(2)},${speakerHeadPosition.y.toFixed(2)},${speakerHeadPosition.z.toFixed(2)})`);
426
405
  }
427
406
  }
428
407
  }
@@ -455,65 +434,77 @@ class SpatialAudioManager extends EventManager_1.EventManager {
455
434
  this.applyListenerTransform(normalizedPosition, orientation);
456
435
  }
457
436
  /**
458
- * Update listener orientation from LSD camera direction
437
+ * POSITION-ONLY MODE: Set listener HEAD position (no direction needed)
459
438
  * IMPORTANT: Uses CAMERA position (head) as listener, not body position!
460
439
  *
461
440
  * @param listenerPos Player body position (for reference, not used as listener)
462
441
  * @param cameraPos Camera/HEAD position - THIS is the actual listener position for audio
463
- * @param lookAtPos Look-at position (where camera is pointing)
442
+ * @param lookAtPos Look-at position (where camera is pointing) - stored but not used for panning
443
+ * @param rot Rotation data (pitch, yaw, roll) - stored but not used for panning
464
444
  */
465
- setListenerFromLSD(listenerPos, cameraPos, lookAtPos) {
445
+ setListenerFromLSD(listenerPos, cameraPos, lookAtPos, rot) {
466
446
  // USE CAMERA POSITION AS LISTENER (head position, not body!)
467
- const normalizedListener = this.normalizePositionUnits(cameraPos); // ✅ Changed from listenerPos
468
- const normalizedCamera = this.normalizePositionUnits(cameraPos);
469
- const normalizedLookAt = this.normalizePositionUnits(lookAtPos);
470
- // Calculate forward vector (from camera to look-at point)
471
- const forwardX = normalizedLookAt.x - normalizedCamera.x;
472
- const forwardY = normalizedLookAt.y - normalizedCamera.y;
473
- const forwardZ = normalizedLookAt.z - normalizedCamera.z;
474
- // Normalize forward vector
475
- const forwardLen = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ);
476
- if (forwardLen < 0.001) {
477
- return;
478
- }
479
- const fwdX = forwardX / forwardLen;
480
- const fwdY = forwardY / forwardLen;
481
- const fwdZ = forwardZ / forwardLen;
482
- // Calculate right vector (cross product of world up and forward)
483
- // Web Audio API uses Y-up coordinate system, Unreal uses Z-up
484
- // We need to transform: Unreal (X,Y,Z) -> WebAudio (X,Z,-Y)
485
- const worldUp = { x: 0, y: 1, z: 0 }; // Web Audio Y-up
486
- const rightX = worldUp.y * fwdZ - worldUp.z * fwdY;
487
- const rightY = worldUp.z * fwdX - worldUp.x * fwdZ;
488
- const rightZ = worldUp.x * fwdY - worldUp.y * fwdX;
489
- const rightLen = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ);
490
- if (rightLen < 0.001) {
491
- // Forward is parallel to world up, use fallback
447
+ const normalizedListener = this.normalizePositionUnits(cameraPos);
448
+ // Store listener position (used for X-axis panning and distance calculation)
449
+ this.listenerPosition = normalizedListener;
450
+ // 📍 [SDK Listener] Log listener position update
451
+ console.log(`📍 [SDK Listener] ` +
452
+ `pos=(${normalizedListener.x.toFixed(2)}, ${normalizedListener.y.toFixed(2)}, ${normalizedListener.z.toFixed(2)}) ` +
453
+ `rot=(${rot?.x.toFixed(1) || 'N/A'}, ${rot?.y.toFixed(1) || 'N/A'}, ${rot?.z.toFixed(1) || 'N/A'})`);
454
+ // Position-only mode: No forward vector calculations needed
455
+ // Panning is calculated purely from X-axis position difference in updateSpatialAudio()
456
+ // LEGACY CODE BELOW - Kept for reference but not executed
457
+ if (false) {
458
+ const normalizedCamera = this.normalizePositionUnits(cameraPos);
459
+ const normalizedLookAt = this.normalizePositionUnits(lookAtPos);
460
+ // Calculate forward vector (from camera to look-at point)
461
+ const forwardX = normalizedLookAt.x - normalizedCamera.x;
462
+ const forwardY = normalizedLookAt.y - normalizedCamera.y;
463
+ const forwardZ = normalizedLookAt.z - normalizedCamera.z;
464
+ // Normalize forward vector
465
+ const forwardLen = Math.sqrt(forwardX * forwardX + forwardY * forwardY + forwardZ * forwardZ);
466
+ if (forwardLen < 0.001) {
467
+ return;
468
+ }
469
+ const fwdX = forwardX / forwardLen;
470
+ const fwdY = forwardY / forwardLen;
471
+ const fwdZ = forwardZ / forwardLen;
472
+ // Calculate right vector (cross product of world up and forward)
473
+ // Web Audio API uses Y-up coordinate system, Unreal uses Z-up
474
+ // We need to transform: Unreal (X,Y,Z) -> WebAudio (X,Z,-Y)
475
+ const worldUp = { x: 0, y: 1, z: 0 }; // Web Audio Y-up
476
+ const rightX = worldUp.y * fwdZ - worldUp.z * fwdY;
477
+ const rightY = worldUp.z * fwdX - worldUp.x * fwdZ;
478
+ const rightZ = worldUp.x * fwdY - worldUp.y * fwdX;
479
+ const rightLen = Math.sqrt(rightX * rightX + rightY * rightY + rightZ * rightZ);
480
+ if (rightLen < 0.001) {
481
+ // Forward is parallel to world up, use fallback
482
+ this.applyListenerTransform(normalizedListener, {
483
+ forwardX: fwdX,
484
+ forwardY: fwdY,
485
+ forwardZ: fwdZ,
486
+ upX: 0,
487
+ upY: 1,
488
+ upZ: 0,
489
+ });
490
+ return;
491
+ }
492
+ const rX = rightX / rightLen;
493
+ const rY = rightY / rightLen;
494
+ const rZ = rightZ / rightLen;
495
+ // Calculate true up vector (cross product of forward and right)
496
+ const upX = fwdY * rZ - fwdZ * rY;
497
+ const upY = fwdZ * rX - fwdX * rZ;
498
+ const upZ = fwdX * rY - fwdY * rX;
492
499
  this.applyListenerTransform(normalizedListener, {
493
500
  forwardX: fwdX,
494
501
  forwardY: fwdY,
495
502
  forwardZ: fwdZ,
496
- upX: 0,
497
- upY: 1,
498
- upZ: 0,
503
+ upX,
504
+ upY,
505
+ upZ,
499
506
  });
500
- return;
501
- }
502
- const rX = rightX / rightLen;
503
- const rY = rightY / rightLen;
504
- const rZ = rightZ / rightLen;
505
- // Calculate true up vector (cross product of forward and right)
506
- const upX = fwdY * rZ - fwdZ * rY;
507
- const upY = fwdZ * rX - fwdX * rZ;
508
- const upZ = fwdX * rY - fwdY * rX;
509
- this.applyListenerTransform(normalizedListener, {
510
- forwardX: fwdX,
511
- forwardY: fwdY,
512
- forwardZ: fwdZ,
513
- upX,
514
- upY,
515
- upZ,
516
- });
507
+ } // End of legacy code block
517
508
  }
518
509
  applyListenerTransform(normalizedPosition, orientation) {
519
510
  const { listener } = this.audioContext;
@@ -563,6 +554,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
563
554
  nodes.monoGainL.disconnect();
564
555
  nodes.monoGainR.disconnect();
565
556
  nodes.monoMerger.disconnect();
557
+ nodes.stereoUpmixer.disconnect();
566
558
  nodes.stereoPanner.disconnect();
567
559
  nodes.analyser.disconnect();
568
560
  nodes.gain.disconnect();
@@ -760,6 +752,27 @@ class SpatialAudioManager extends EventManager_1.EventManager {
760
752
  };
761
753
  }
762
754
  /**
755
+ * Calculate stereo panning based on X-axis position difference ONLY
756
+ * @param dx Horizontal difference (speaker.x - listener.x)
757
+ * @returns { left: 0-100, right: 0-100 }
758
+ */
759
+ calculatePositionBasedPanning(dx) {
760
+ const threshold = 0.1; // 10cm threshold for "centered"
761
+ if (dx > threshold) {
762
+ // Speaker is to the RIGHT → Pan RIGHT
763
+ return { left: 0, right: 100 };
764
+ }
765
+ else if (dx < -threshold) {
766
+ // Speaker is to the LEFT → Pan LEFT
767
+ return { left: 100, right: 0 };
768
+ }
769
+ else {
770
+ // Centered (same X position)
771
+ return { left: 100, right: 100 };
772
+ }
773
+ }
774
+ /**
775
+ * OLD METHOD - Kept for reference but not used in position-only mode
763
776
  * Calculate angle between listener and sound source in degrees (0-360)
764
777
  * 0° = front, 90° = right, 180° = back, 270° = left
765
778
  */
package/dist/index.d.ts CHANGED
@@ -48,6 +48,11 @@ export declare class OdysseySpatialComms extends EventManager {
48
48
  x: number;
49
49
  y: number;
50
50
  } | null;
51
+ rot?: {
52
+ x: number;
53
+ y: number;
54
+ z: number;
55
+ };
51
56
  }): void;
52
57
  updateMediaState(mediaState: MediaState): void;
53
58
  setListenerPosition(position: Position, orientation: {
@@ -58,7 +63,11 @@ export declare class OdysseySpatialComms extends EventManager {
58
63
  upY: number;
59
64
  upZ: number;
60
65
  }): void;
61
- setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position): void;
66
+ setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position, rot?: {
67
+ x: number;
68
+ y: number;
69
+ z: number;
70
+ }): void;
62
71
  private listenForEvents;
63
72
  /**
64
73
  * Send huddle invite to another participant
package/dist/index.js CHANGED
@@ -201,6 +201,7 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
201
201
  roomId: this.room.id,
202
202
  position,
203
203
  direction,
204
+ rot: spatialData?.rot,
204
205
  cameraDistance: spatialData?.cameraDistance,
205
206
  screenPos: spatialData?.screenPos,
206
207
  };
@@ -221,8 +222,8 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
221
222
  setListenerPosition(position, orientation) {
222
223
  this.spatialAudioManager.setListenerPosition(position, orientation);
223
224
  }
224
- setListenerFromLSD(listenerPos, cameraPos, lookAtPos) {
225
- this.spatialAudioManager.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
225
+ setListenerFromLSD(listenerPos, cameraPos, lookAtPos, rot) {
226
+ this.spatialAudioManager.setListenerFromLSD(listenerPos, cameraPos, lookAtPos, rot);
226
227
  }
227
228
  listenForEvents() {
228
229
  // CRITICAL: Register all-participants-update listener at initialization
@@ -453,8 +454,9 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
453
454
  const participantChannel = participant.currentChannel;
454
455
  const isInHuddle = participantChannel === "odyssey-huddle";
455
456
  if (!isInHuddle) {
456
- // Update spatial audio with BOTH position AND direction from socket
457
- this.spatialAudioManager.updateSpatialAudio(data.participantId, data.position, data.direction);
457
+ // Update spatial audio with position, direction, AND rotation
458
+ this.spatialAudioManager.updateSpatialAudio(data.participantId, data.position, data.rot || data.direction // Prefer rot, fallback to direction
459
+ );
458
460
  }
459
461
  this.emit("participant-position-updated", participant);
460
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.75",
3
+ "version": "1.0.78",
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",