@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.18 → 1.0.19

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
@@ -79,7 +79,7 @@ sdk.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
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
81
  - **Dynamic distance gain** – `updateSpatialAudio()` measures distance from listener → source and applies a smooth rolloff curve, so distant avatars fade to silence.
82
- - **Noise handling** – optional AudioWorklet denoiser plus high/low-pass filters trim rumble & hiss before HRTF processing.
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.
83
83
 
84
84
  #### How Spatial Audio Is Built
85
85
  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.
@@ -11,6 +11,12 @@ type DenoiserOptions = {
11
11
  threshold?: number;
12
12
  noiseFloor?: number;
13
13
  release?: number;
14
+ attack?: number;
15
+ holdMs?: number;
16
+ maxReduction?: number;
17
+ hissCut?: number;
18
+ expansionRatio?: number;
19
+ learnRate?: number;
14
20
  };
15
21
  type SpatialAudioOptions = {
16
22
  distance?: SpatialAudioDistanceConfig;
@@ -25,7 +31,6 @@ export declare class SpatialAudioManager extends EventManager {
25
31
  private options;
26
32
  private denoiseWorkletReady;
27
33
  private denoiseWorkletUrl?;
28
- private denoiserWasmBytes?;
29
34
  private listenerPosition;
30
35
  private listenerInitialized;
31
36
  private listenerDirection;
@@ -82,9 +82,12 @@ class SpatialAudioManager extends EventManager_1.EventManager {
82
82
  threshold: this.options.denoiser?.threshold,
83
83
  noiseFloor: this.options.denoiser?.noiseFloor,
84
84
  release: this.options.denoiser?.release,
85
- wasmBytes: this.denoiserWasmBytes
86
- ? this.denoiserWasmBytes.slice(0)
87
- : null,
85
+ attack: this.options.denoiser?.attack,
86
+ holdMs: this.options.denoiser?.holdMs,
87
+ maxReduction: this.options.denoiser?.maxReduction,
88
+ hissCut: this.options.denoiser?.hissCut,
89
+ expansionRatio: this.options.denoiser?.expansionRatio,
90
+ learnRate: this.options.denoiser?.learnRate,
88
91
  },
89
92
  });
90
93
  }
@@ -577,47 +580,101 @@ class SpatialAudioManager extends EventManager_1.EventManager {
577
580
  super();
578
581
  const cfg = (options && options.processorOptions) || {};
579
582
  this.enabled = cfg.enabled !== false;
580
- this.threshold = typeof cfg.threshold === 'number' ? cfg.threshold : 0.012;
581
- this.noiseFloor = typeof cfg.noiseFloor === 'number' ? cfg.noiseFloor : 0.004;
582
- this.release = typeof cfg.release === 'number' ? cfg.release : 0.18;
583
- this.smoothedLevel = this.noiseFloor;
583
+ this.threshold = this._sanitize(cfg.threshold, 0.003, 0.05, 0.012);
584
+ this.noiseFloor = this._sanitize(cfg.noiseFloor, 0.0005, 0.05, 0.004);
585
+ this.attack = this._sanitize(cfg.attack, 0.01, 0.9, 0.35);
586
+ this.release = this._sanitize(cfg.release, 0.01, 0.9, 0.18);
587
+ this.holdSamples = Math.max(
588
+ 8,
589
+ Math.round(
590
+ sampleRate * this._sanitize(cfg.holdMs, 10, 400, 110) / 1000
591
+ )
592
+ );
593
+ this.maxReduction = this._sanitize(cfg.maxReduction, 0.1, 0.95, 0.85);
594
+ this.hissCut = this._sanitize(cfg.hissCut, 0, 1, 0.45);
595
+ this.expansionRatio = this._sanitize(cfg.expansionRatio, 1.1, 4, 1.8);
596
+ this.learnRate = this._sanitize(cfg.learnRate, 0.001, 0.3, 0.08);
597
+ this.channelState = [];
598
+ this.hfAlpha = Math.exp(-2 * Math.PI * 3200 / sampleRate);
599
+ }
600
+
601
+ _sanitize(value, min, max, fallback) {
602
+ if (typeof value !== 'number' || !isFinite(value)) {
603
+ return fallback;
604
+ }
605
+ return Math.min(max, Math.max(min, value));
606
+ }
607
+
608
+ _ensureState(index) {
609
+ if (!this.channelState[index]) {
610
+ this.channelState[index] = {
611
+ envelope: this.noiseFloor,
612
+ noise: this.noiseFloor,
613
+ gain: 1,
614
+ quietSamples: 0,
615
+ lpState: 0,
616
+ };
617
+ }
618
+ return this.channelState[index];
584
619
  }
585
620
 
586
621
  process(inputs, outputs) {
587
622
  const input = inputs[0];
588
623
  const output = outputs[0];
589
- if (!input || input.length === 0 || !output || output.length === 0) {
624
+ if (!input || !output) {
590
625
  return true;
591
626
  }
592
627
 
593
- for (let channel = 0; channel < input.length; channel++) {
628
+ for (let channel = 0; channel < output.length; channel++) {
594
629
  const inChannel = input[channel];
595
630
  const outChannel = output[channel];
596
631
  if (!inChannel || !outChannel) {
597
632
  continue;
598
633
  }
599
634
 
600
- let sum = 0;
635
+ if (!this.enabled) {
636
+ for (let i = 0; i < inChannel.length; i++) {
637
+ outChannel[i] = inChannel[i];
638
+ }
639
+ continue;
640
+ }
641
+
642
+ const state = this._ensureState(channel);
643
+
601
644
  for (let i = 0; i < inChannel.length; i++) {
602
645
  const sample = inChannel[i];
603
- sum += sample * sample;
604
- }
646
+ const magnitude = Math.abs(sample);
605
647
 
606
- const rms = Math.sqrt(sum / inChannel.length);
607
- this.smoothedLevel += (rms - this.smoothedLevel) * this.release;
608
- const dynamicThreshold = Math.max(
609
- this.noiseFloor,
610
- this.threshold * 0.6 + this.smoothedLevel * 0.4
611
- );
648
+ state.envelope += (magnitude - state.envelope) * this.attack;
612
649
 
613
- if (!this.enabled || rms >= dynamicThreshold) {
614
- for (let i = 0; i < inChannel.length; i++) {
615
- outChannel[i] = inChannel[i];
650
+ if (state.envelope < this.threshold) {
651
+ state.noise += (state.envelope - state.noise) * this.learnRate;
652
+ state.quietSamples++;
653
+ } else {
654
+ state.quietSamples = 0;
616
655
  }
617
- } else {
618
- for (let i = 0; i < inChannel.length; i++) {
619
- outChannel[i] = 0;
656
+
657
+ const ratio = state.noise / Math.max(state.envelope, 1e-6);
658
+ let gainTarget = 1 - Math.min(0.98, Math.pow(ratio, this.expansionRatio));
659
+ gainTarget = Math.max(0, Math.min(1, gainTarget));
660
+
661
+ if (state.quietSamples > this.holdSamples) {
662
+ gainTarget *= 1 - this.maxReduction;
620
663
  }
664
+
665
+ state.gain += (gainTarget - state.gain) * this.release;
666
+ let processed = sample * state.gain;
667
+
668
+ state.lpState = this.hfAlpha * state.lpState + (1 - this.hfAlpha) * processed;
669
+ const highComponent = processed - state.lpState;
670
+ const hissRatio = Math.min(
671
+ 1,
672
+ Math.abs(highComponent) / (Math.abs(state.lpState) + 1e-5)
673
+ );
674
+ const hissGain = 1 - hissRatio * this.hissCut;
675
+ processed = state.lpState + highComponent * hissGain;
676
+
677
+ outChannel[i] = processed;
621
678
  }
622
679
  }
623
680
 
@@ -655,6 +712,12 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
655
712
  threshold: 0.012,
656
713
  noiseFloor: 0.004,
657
714
  release: 0.18,
715
+ attack: 0.35,
716
+ holdMs: 110,
717
+ maxReduction: 0.85,
718
+ hissCut: 0.45,
719
+ expansionRatio: 1.8,
720
+ learnRate: 0.08,
658
721
  };
659
722
  return {
660
723
  distance: {
@@ -668,6 +731,12 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
668
731
  threshold: options?.denoiser?.threshold ?? denoiserDefaults.threshold,
669
732
  noiseFloor: options?.denoiser?.noiseFloor ?? denoiserDefaults.noiseFloor,
670
733
  release: options?.denoiser?.release ?? denoiserDefaults.release,
734
+ attack: options?.denoiser?.attack ?? denoiserDefaults.attack,
735
+ holdMs: options?.denoiser?.holdMs ?? denoiserDefaults.holdMs,
736
+ maxReduction: options?.denoiser?.maxReduction ?? denoiserDefaults.maxReduction,
737
+ hissCut: options?.denoiser?.hissCut ?? denoiserDefaults.hissCut,
738
+ expansionRatio: options?.denoiser?.expansionRatio ?? denoiserDefaults.expansionRatio,
739
+ learnRate: options?.denoiser?.learnRate ?? denoiserDefaults.learnRate,
671
740
  },
672
741
  };
673
742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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",