@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.12 β†’ 1.0.13

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
@@ -13,7 +13,7 @@ It mirrors the production SDK used by Odyssey V2 and ships ready-to-drop into an
13
13
  - 🧭 **Accurate pose propagation** – `updatePosition()` streams listener pose to the SFU while `participant-position-updated` keeps the local store in sync.
14
14
  - 🎧 **Studio-grade spatial audio** – each remote participant gets a dedicated Web Audio graph: denoiser β†’ high-pass β†’ low-pass β†’ HRTF `PannerNode` β†’ adaptive gain β†’ master compressor.
15
15
  - πŸŽ₯ **Camera-ready streams** – video tracks are exposed separately so UI layers can render muted `<video>` tags while audio stays inside Web Audio.
16
- - πŸŽ™οΈ **Clean microphone uplink** – optional `enhanceOutgoingAudioTrack` helper runs mic input through denoiser + EQ + compressor before hitting the SFU.
16
+ - πŸŽ™οΈ **Clean microphone uplink (opt‑in)** – when `outboundTuning.enabled=true`, `enhanceOutgoingAudioTrack` runs mic input through denoiser + EQ + compressor before hitting the SFU.
17
17
  - πŸ” **EventEmitter contract** – subscribe to `room-joined`, `consumer-created`, `participant-position-updated`, etc., without touching Socket.IO directly.
18
18
 
19
19
  ## Quick Start
@@ -81,6 +81,7 @@ sdk.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
81
81
  - **Orientation math** – `setListenerFromLSD()` builds forward/right/up vectors from camera/LookAt to keep the listener aligned with head movement.
82
82
  - **Dynamic distance gain** – `updateSpatialAudio()` measures distance from listener β†’ source and applies a smooth rolloff curve, so distant avatars fade to silence.
83
83
  - **Noise handling** – optional AudioWorklet denoiser plus high/low-pass filters trim rumble & hiss before HRTF processing.
84
+ - **Dynamic gate (opt-in)** – enable via `noiseGate.enabled=true` to let the SDK automatically clamp remote tracks when they’re idle.
84
85
 
85
86
  #### How Spatial Audio Is Built
86
87
  1. **Telemetry ingestion** – each LSD packet is passed through `setListenerFromLSD(listenerPos, cameraPos, lookAtPos)` so the Web Audio listener matches the player’s real head/camera pose.
@@ -90,11 +91,12 @@ sdk.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
90
91
  4. **Distance-aware gain** – the manager stores the latest listener pose and computes the Euclidean distance to each remote participant on every update. A custom rolloff curve adjusts gain before the compressor, giving the β€œsomeone on my left / far away” perception without blowing out master levels.
91
92
  5. **Left/right rendering** – because the panner uses `panningModel = "HRTF"`, browsers feed the processed signal into the user’s audio hardware with head-related transfer functions, producing natural interaural time/intensity differences.
92
93
 
93
- #### How Microphone Audio Is Tuned Before Sending
94
+ #### How Microphone Audio Is Tuned Before Sending (Opt-In)
95
+ > Disabled by default. Enable via `new SpatialAudioManager({ outboundTuning: { enabled: true } })`.
94
96
  1. **Hardware constraints first** – the SDK requests `noiseSuppression`, `echoCancellation`, and `autoGainControl` on the raw `MediaStreamTrack` (plus Chromium-specific `goog*` flags).
95
97
  2. **Web Audio pre-flight** – `enhanceOutgoingAudioTrack(track)` clones the mic into a dedicated `AudioContext` and chain: `Denoiser β†’ 50/60β€―Hz notches β†’ Low-shelf rumble cut β†’ High-pass (95β€―Hz) β†’ Low-pass (7.2β€―kHz) β†’ High-shelf tame β†’ Presence boost β†’ Dynamics compressor β†’ Adaptive gate`.
96
98
  3. **Adaptive gate** – a lightweight RMS monitor clamps the gate gain when only background hiss remains, but opens instantly when speech energy rises.
97
- 4. **Clean stream to SFU** – the processed track is what you pass to `produceTrack`, so every participant receives the filtered audio (and your local store uses the same track for mute toggles).
99
+ 4. **Clean stream to SFU** – the processed track is what you pass to `produceTrack`, so every participant receives the filtered audio (and your local store uses the same track for mute toggles). Toggle the feature off to fall back to raw WebRTC audio instantly.
98
100
 
99
101
  ## Video Flow (Capture ↔ Rendering)
100
102
 
@@ -12,9 +12,17 @@ type DenoiserOptions = {
12
12
  noiseFloor?: number;
13
13
  release?: number;
14
14
  };
15
+ type NoiseGateOptions = {
16
+ enabled?: boolean;
17
+ };
18
+ type OutboundTuningOptions = {
19
+ enabled?: boolean;
20
+ };
15
21
  type SpatialAudioOptions = {
16
22
  distance?: SpatialAudioDistanceConfig;
17
23
  denoiser?: DenoiserOptions;
24
+ noiseGate?: NoiseGateOptions;
25
+ outboundTuning?: OutboundTuningOptions;
18
26
  };
19
27
  export declare class SpatialAudioManager extends EventManager {
20
28
  private audioContext;
@@ -31,6 +39,8 @@ export declare class SpatialAudioManager extends EventManager {
31
39
  private stabilityState;
32
40
  private outgoingProcessors;
33
41
  private listenerDirection;
42
+ private noiseGateEnabled;
43
+ private outboundTuningEnabled;
34
44
  constructor(options?: SpatialAudioOptions);
35
45
  getAudioContext(): AudioContext;
36
46
  /**
@@ -17,6 +17,8 @@ class SpatialAudioManager extends EventManager_1.EventManager {
17
17
  up: { x: 0, y: 0, z: 1 },
18
18
  };
19
19
  this.options = this.resolveOptions(options);
20
+ this.noiseGateEnabled = this.options.noiseGate?.enabled ?? false;
21
+ this.outboundTuningEnabled = this.options.outboundTuning?.enabled ?? false;
20
22
  // Use high sample rate for best audio quality
21
23
  this.audioContext = new AudioContext({ sampleRate: 48000 });
22
24
  // Master gain
@@ -126,16 +128,20 @@ class SpatialAudioManager extends EventManager_1.EventManager {
126
128
  }
127
129
  currentNode.connect(highpassFilter);
128
130
  highpassFilter.connect(lowpassFilter);
129
- lowpassFilter.connect(noiseGate);
131
+ let postFilterNode = lowpassFilter;
132
+ if (this.noiseGateEnabled) {
133
+ lowpassFilter.connect(noiseGate);
134
+ postFilterNode = noiseGate;
135
+ }
130
136
  if (bypassSpatialization) {
131
137
  console.log(`πŸ”Š TESTING: Connecting audio directly to destination (bypassing spatial audio) for ${participantId}`);
132
- noiseGate.connect(analyser);
138
+ postFilterNode.connect(analyser);
133
139
  analyser.connect(this.masterGainNode);
134
140
  }
135
141
  else {
136
142
  // Standard spatialized path with full audio chain
137
- // Audio Chain: source -> filters -> noiseGate -> panner -> analyser -> gain -> masterGain -> compressor -> destination
138
- noiseGate.connect(panner);
143
+ // Audio Chain: source -> filters -> (optional gate) -> panner -> analyser -> gain -> masterGain -> compressor -> destination
144
+ postFilterNode.connect(panner);
139
145
  panner.connect(analyser);
140
146
  analyser.connect(gain);
141
147
  gain.connect(this.masterGainNode);
@@ -156,7 +162,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
156
162
  targetGain: 1,
157
163
  networkMuted: false,
158
164
  });
159
- if (typeof track.onmute !== "undefined") {
165
+ if (this.noiseGateEnabled && typeof track.onmute !== "undefined") {
160
166
  track.onmute = () => this.handleTrackStability(participantId, true);
161
167
  track.onunmute = () => this.handleTrackStability(participantId, false);
162
168
  }
@@ -176,11 +182,13 @@ class SpatialAudioManager extends EventManager_1.EventManager {
176
182
  rolloffFactor: panner.rolloffFactor,
177
183
  },
178
184
  });
179
- // Start monitoring audio levels
180
- this.startMonitoring(participantId);
185
+ // Start monitoring audio levels if gate enabled
186
+ if (this.noiseGateEnabled) {
187
+ this.startMonitoring(participantId);
188
+ }
181
189
  }
182
190
  async enhanceOutgoingAudioTrack(track) {
183
- if (track.kind !== "audio") {
191
+ if (track.kind !== "audio" || !this.outboundTuningEnabled) {
184
192
  return track;
185
193
  }
186
194
  const existingProcessor = Array.from(this.outgoingProcessors.values()).find((processor) => processor.originalTrack === track);
@@ -305,6 +313,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
305
313
  return processedTrack;
306
314
  }
307
315
  startMonitoring(participantId) {
316
+ if (!this.noiseGateEnabled) {
317
+ return;
318
+ }
308
319
  const nodes = this.participantNodes.get(participantId);
309
320
  if (!nodes)
310
321
  return;
@@ -380,6 +391,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
380
391
  this.monitoringIntervals.set(participantId, interval);
381
392
  }
382
393
  handleTrackStability(participantId, muted) {
394
+ if (!this.noiseGateEnabled) {
395
+ return;
396
+ }
383
397
  const nodes = this.participantNodes.get(participantId);
384
398
  if (!nodes)
385
399
  return;
@@ -729,12 +743,6 @@ class SpatialAudioManager extends EventManager_1.EventManager {
729
743
  clearInterval(processor.monitor);
730
744
  processor.processedTrack.removeEventListener("ended", processor.cleanupListener);
731
745
  processor.originalTrack.removeEventListener("ended", processor.cleanupListener);
732
- try {
733
- processor.originalTrack.stop();
734
- }
735
- catch (error) {
736
- console.warn("⚠️ Unable to stop original track during cleanup", error);
737
- }
738
746
  processor.destinationStream.getTracks().forEach((t) => t.stop());
739
747
  processor.sourceStream.getTracks().forEach((t) => t.stop());
740
748
  processor.context.close();
@@ -843,6 +851,12 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
843
851
  noiseFloor: 0.004,
844
852
  release: 0.18,
845
853
  };
854
+ const noiseGateDefaults = {
855
+ enabled: false,
856
+ };
857
+ const outboundDefaults = {
858
+ enabled: false,
859
+ };
846
860
  return {
847
861
  distance: {
848
862
  refDistance: options?.distance?.refDistance ?? distanceDefaults.refDistance,
@@ -856,6 +870,12 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
856
870
  noiseFloor: options?.denoiser?.noiseFloor ?? denoiserDefaults.noiseFloor,
857
871
  release: options?.denoiser?.release ?? denoiserDefaults.release,
858
872
  },
873
+ noiseGate: {
874
+ enabled: options?.noiseGate?.enabled ?? noiseGateDefaults.enabled,
875
+ },
876
+ outboundTuning: {
877
+ enabled: options?.outboundTuning?.enabled ?? outboundDefaults.enabled,
878
+ },
859
879
  };
860
880
  }
861
881
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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",