@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.63 → 1.0.64

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/README.md CHANGED
@@ -78,7 +78,7 @@ sdk.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
78
78
  ### Web Audio Algorithms
79
79
  - **Coordinate normalization** – Unreal sends centimeters; `SpatialAudioManager` auto-detects large values and converts to meters once.
80
80
  - **Orientation math** – `setListenerFromLSD()` builds forward/right/up vectors from camera/LookAt to keep the listener aligned with head movement.
81
- - **Dynamic distance gain** – `updateSpatialAudio()` measures distance from listener → source and applies a smooth rolloff curve, so distant avatars fade to silence.
81
+ - **Dynamic distance gain** – `updateSpatialAudio()` measures distance from listener → source and applies a quadratic rolloff curve (0.5m-10m range). Voices gradually fade from 100% (0.5m clear) to complete silence at 10m+. Distance calculated from listener's HEAD position to participant's standing position.
82
82
  - **Noise handling** – the AudioWorklet denoiser now runs an adaptive multi-band gate (per W3C AudioWorklet guidance) before the high/low-pass filters, stripping constant HVAC/fan noise even when the speaker is close. A newly added silence gate mutes tracks entirely after ~250 ms of sub-noise-floor energy, eliminating hiss during dead air without touching spatial cues.
83
83
 
84
84
  #### Noise-Cancellation Stack (What’s Included)
@@ -181,13 +181,13 @@ export declare class SpatialAudioManager extends EventManager {
181
181
  /**
182
182
  * Calculate gain based on distance using logarithmic scale
183
183
  * Distance range: 0.5m to 5m
184
- * Gain range: 100% to 0%
184
+ * Gain range: 100% to 20% (never goes to 0 for better audibility)
185
185
  * Uses quadratic equation for human ear perception
186
186
  */
187
187
  private calculateLogarithmicGain;
188
188
  /**
189
- * Apply stereo panning to participant audio
190
- * Converts panning percentages to StereoPanner values
189
+ * Apply stereo panning to participant audio using StereoPannerNode
190
+ * This provides STABLE left-right panning without jitter
191
191
  */
192
192
  private applyStereoPanning;
193
193
  private ensureDenoiseWorklet;
@@ -99,6 +99,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
99
99
  const stream = new MediaStream([track]);
100
100
  const source = this.audioContext.createMediaStreamSource(stream);
101
101
  const panner = this.audioContext.createPanner();
102
+ const stereoPanner = this.audioContext.createStereoPanner(); // For stable L/R panning
102
103
  const analyser = this.audioContext.createAnalyser();
103
104
  const gain = this.audioContext.createGain();
104
105
  const proximityGain = this.audioContext.createGain();
@@ -181,13 +182,13 @@ class SpatialAudioManager extends EventManager_1.EventManager {
181
182
  panner.coneOuterAngle = 360;
182
183
  panner.coneOuterGain = 0.3; // Some sound even outside cone
183
184
  // Configure gain for individual participant volume control
184
- gain.gain.value = 1.0;
185
+ gain.gain.value = 1.5; // Boost initial gain (was 1.0)
185
186
  // ADD COMPRESSOR: Prevents sudden peaks and "pops" when speaking starts
186
187
  // This is KEY to eliminating onset artifacts
187
188
  const participantCompressor = this.audioContext.createDynamicsCompressor();
188
- participantCompressor.threshold.value = -24; // Start compressing at -24dB
189
+ participantCompressor.threshold.value = -30; // Higher threshold (less compression)
189
190
  participantCompressor.knee.value = 10; // Smooth knee for natural sound
190
- participantCompressor.ratio.value = 3; // 3:1 compression ratio
191
+ participantCompressor.ratio.value = 2; // 2:1 gentle ratio (was 3:1)
191
192
  participantCompressor.attack.value = 0.003; // 3ms fast attack for transients
192
193
  participantCompressor.release.value = 0.15; // 150ms release for natural decay
193
194
  let currentNode = source;
@@ -210,9 +211,10 @@ class SpatialAudioManager extends EventManager_1.EventManager {
210
211
  analyser.connect(this.masterGainNode);
211
212
  }
212
213
  else {
213
- // Standard spatialized path with full audio chain
214
- // Audio Chain: source -> filters -> panner -> analyser -> gain -> masterGain -> compressor -> destination
215
- proximityGain.connect(panner);
214
+ // Standard spatialized path with stereo panner
215
+ // Audio Chain: source -> compressor -> filters -> stereoPanner -> panner -> analyser -> gain -> masterGain -> compressor -> destination
216
+ proximityGain.connect(stereoPanner); // Stereo panner for stable L/R
217
+ stereoPanner.connect(panner); // Then 3D panner for distance
216
218
  panner.connect(analyser);
217
219
  analyser.connect(gain);
218
220
  gain.connect(this.masterGainNode);
@@ -220,6 +222,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
220
222
  this.participantNodes.set(participantId, {
221
223
  source,
222
224
  panner,
225
+ stereoPanner,
223
226
  analyser,
224
227
  gain,
225
228
  proximityGain,
@@ -330,11 +333,10 @@ class SpatialAudioManager extends EventManager_1.EventManager {
330
333
  // Use exponentialRampToValueAtTime for smoother, more natural transitions
331
334
  // This prevents the "pop" when someone starts speaking
332
335
  const currentTime = this.audioContext.currentTime;
333
- const rampTime = 0.08; // 80ms smooth ramp (was 50ms - increased for gentler onset)
336
+ const rampTime = 0.08; // 80ms smooth ramp
334
337
  // Ensure we never ramp to exactly 0 (causes issues)
335
- const targetGain = Math.max(0.001, gainValue);
338
+ const targetGain = Math.max(0.2, gainValue); // Minimum 20% gain (was 0.001)
336
339
  nodes.gain.gain.setTargetAtTime(targetGain, currentTime, rampTime);
337
- nodes.proximityGain.gain.setTargetAtTime(targetGain, currentTime, rampTime);
338
340
  // Update 3D position for PannerNode (still used for vertical positioning)
339
341
  nodes.panner.positionY.setValueAtTime(normalizedPosition.y, this.audioContext.currentTime);
340
342
  nodes.panner.positionZ.setValueAtTime(normalizedPosition.z, this.audioContext.currentTime);
@@ -747,32 +749,31 @@ class SpatialAudioManager extends EventManager_1.EventManager {
747
749
  /**
748
750
  * Calculate gain based on distance using logarithmic scale
749
751
  * Distance range: 0.5m to 5m
750
- * Gain range: 100% to 0%
752
+ * Gain range: 100% to 20% (never goes to 0 for better audibility)
751
753
  * Uses quadratic equation for human ear perception
752
754
  */
753
755
  calculateLogarithmicGain(distance) {
754
- const minDistance = 0.5; // meters
755
- const maxDistance = 5.0; // meters
756
+ const minDistance = 0.5; // meters - clear voice starts here
757
+ const maxDistance = 10.0; // meters - complete silence beyond this
756
758
  // Clamp distance
757
759
  if (distance <= minDistance)
758
- return 100;
760
+ return 100; // Full volume at 0.5m or closer
759
761
  if (distance >= maxDistance)
760
- return 0;
762
+ return 0; // Complete silence at 10m or beyond
761
763
  // Normalize distance to 0-1 range
762
764
  const normalizedDistance = (distance - minDistance) / (maxDistance - minDistance);
763
765
  // Apply quadratic falloff for natural perception
764
- // gain = 100 * (1 - x²)
765
- // This creates a logarithmic-like curve that sounds linear to human ear
766
+ // gain = 100 * (1 - x²) - gradual fade from 100% to 0%
766
767
  const gain = 100 * Math.pow(1 - normalizedDistance, 2);
767
- return Math.max(0, Math.min(100, gain));
768
+ return Math.max(0, Math.min(100, gain)); // Clamp between 0-100%
768
769
  }
769
770
  /**
770
- * Apply stereo panning to participant audio
771
- * Converts panning percentages to StereoPanner values
771
+ * Apply stereo panning to participant audio using StereoPannerNode
772
+ * This provides STABLE left-right panning without jitter
772
773
  */
773
774
  applyStereoPanning(participantId, panning) {
774
775
  const nodes = this.participantNodes.get(participantId);
775
- if (!nodes?.panner)
776
+ if (!nodes?.stereoPanner)
776
777
  return;
777
778
  // Convert left/right percentages to pan value (-1 to +1)
778
779
  // If left=100, right=0 → pan = -1 (full left)
@@ -785,9 +786,10 @@ class SpatialAudioManager extends EventManager_1.EventManager {
785
786
  if (leftRatio + rightRatio > 0) {
786
787
  panValue = (rightRatio - leftRatio);
787
788
  }
788
- // Adjust X position for left-right panning (-1 = left, +1 = right)
789
+ // Use StereoPannerNode for stable, glitch-free panning
790
+ // This is MUCH more stable than manipulating PannerNode.positionX
789
791
  const currentTime = this.audioContext.currentTime;
790
- nodes.panner.positionX.setTargetAtTime(panValue * 5, currentTime, 0.05);
792
+ nodes.stereoPanner.pan.setTargetAtTime(panValue, currentTime, 0.05);
791
793
  }
792
794
  async ensureDenoiseWorklet() {
793
795
  if (!this.isDenoiserEnabled()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.63",
3
+ "version": "1.0.64",
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",