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

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
@@ -80,6 +80,19 @@ sdk.setListenerFromLSD(listenerPos, cameraPos, lookAtPos);
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
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
+ ```ts
84
+ const sdk = new OdysseySpatialComms(serverUrl, {
85
+ denoiser: {
86
+ threshold: 0.0085,
87
+ maxReduction: 0.94,
88
+ hissCut: 0.7,
89
+ holdMs: 180,
90
+ voiceBoost: 0.7,
91
+ voiceSensitivity: 0.3,
92
+ },
93
+ });
94
+ ```
95
+ Tweak these knobs if you need even more “AirPods Pro” style isolation.
83
96
 
84
97
  #### How Spatial Audio Is Built
85
98
  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.
@@ -17,8 +17,10 @@ type DenoiserOptions = {
17
17
  hissCut?: number;
18
18
  expansionRatio?: number;
19
19
  learnRate?: number;
20
+ voiceBoost?: number;
21
+ voiceSensitivity?: number;
20
22
  };
21
- type SpatialAudioOptions = {
23
+ export type SpatialAudioOptions = {
22
24
  distance?: SpatialAudioDistanceConfig;
23
25
  denoiser?: DenoiserOptions;
24
26
  };
@@ -88,6 +88,8 @@ class SpatialAudioManager extends EventManager_1.EventManager {
88
88
  hissCut: this.options.denoiser?.hissCut,
89
89
  expansionRatio: this.options.denoiser?.expansionRatio,
90
90
  learnRate: this.options.denoiser?.learnRate,
91
+ voiceBoost: this.options.denoiser?.voiceBoost,
92
+ voiceSensitivity: this.options.denoiser?.voiceSensitivity,
91
93
  },
92
94
  });
93
95
  }
@@ -594,6 +596,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
594
596
  this.hissCut = this._sanitize(cfg.hissCut, 0, 1, 0.45);
595
597
  this.expansionRatio = this._sanitize(cfg.expansionRatio, 1.1, 4, 1.8);
596
598
  this.learnRate = this._sanitize(cfg.learnRate, 0.001, 0.3, 0.08);
599
+ this.voiceBoost = this._sanitize(cfg.voiceBoost, 0, 1, 0.6);
600
+ this.voiceSensitivity = this._sanitize(cfg.voiceSensitivity, 0.05, 0.9, 0.35);
601
+ this.historySize = 512;
597
602
  this.channelState = [];
598
603
  this.hfAlpha = Math.exp(-2 * Math.PI * 3200 / sampleRate);
599
604
  }
@@ -613,11 +618,66 @@ class SpatialAudioManager extends EventManager_1.EventManager {
613
618
  gain: 1,
614
619
  quietSamples: 0,
615
620
  lpState: 0,
621
+ history: new Float32Array(this.historySize),
622
+ historyIndex: 0,
623
+ historyFilled: 0,
624
+ tempBuffer: new Float32Array(this.historySize),
625
+ voiceConfidence: 0,
616
626
  };
617
627
  }
618
628
  return this.channelState[index];
619
629
  }
620
630
 
631
+ _pushHistory(state, sample) {
632
+ state.history[state.historyIndex] = sample;
633
+ state.historyIndex = (state.historyIndex + 1) % state.history.length;
634
+ if (state.historyFilled < state.history.length) {
635
+ state.historyFilled++;
636
+ }
637
+ }
638
+
639
+ _updateVoiceConfidence(state) {
640
+ if (state.historyFilled < state.history.length * 0.6) {
641
+ state.voiceConfidence += (0 - state.voiceConfidence) * 0.15;
642
+ return state.voiceConfidence;
643
+ }
644
+
645
+ const len = state.history.length;
646
+ let writeIndex = state.historyIndex;
647
+ for (let i = 0; i < len; i++) {
648
+ state.tempBuffer[i] = state.history[writeIndex];
649
+ writeIndex = (writeIndex + 1) % len;
650
+ }
651
+
652
+ const minLag = 30;
653
+ const maxLag = 240;
654
+ let best = 0;
655
+ for (let lag = minLag; lag <= maxLag; lag += 2) {
656
+ let sum = 0;
657
+ let energyA = 0;
658
+ let energyB = 0;
659
+ for (let i = lag; i < len; i++) {
660
+ const a = state.tempBuffer[i];
661
+ const b = state.tempBuffer[i - lag];
662
+ sum += a * b;
663
+ energyA += a * a;
664
+ energyB += b * b;
665
+ }
666
+ const denom = Math.sqrt(energyA * energyB) + 1e-8;
667
+ const corr = Math.abs(sum) / denom;
668
+ if (corr > best) {
669
+ best = corr;
670
+ }
671
+ }
672
+
673
+ const normalized = Math.max(
674
+ 0,
675
+ Math.min(1, (best - this.voiceSensitivity) / (1 - this.voiceSensitivity))
676
+ );
677
+ state.voiceConfidence += (normalized - state.voiceConfidence) * 0.2;
678
+ return state.voiceConfidence;
679
+ }
680
+
621
681
  process(inputs, outputs) {
622
682
  const input = inputs[0];
623
683
  const output = outputs[0];
@@ -640,9 +700,11 @@ class SpatialAudioManager extends EventManager_1.EventManager {
640
700
  }
641
701
 
642
702
  const state = this._ensureState(channel);
703
+ const speechPresence = this.voiceBoost * state.voiceConfidence;
643
704
 
644
705
  for (let i = 0; i < inChannel.length; i++) {
645
706
  const sample = inChannel[i];
707
+ this._pushHistory(state, sample);
646
708
  const magnitude = Math.abs(sample);
647
709
 
648
710
  state.envelope += (magnitude - state.envelope) * this.attack;
@@ -662,7 +724,13 @@ class SpatialAudioManager extends EventManager_1.EventManager {
662
724
  gainTarget *= 1 - this.maxReduction;
663
725
  }
664
726
 
665
- state.gain += (gainTarget - state.gain) * this.release;
727
+ const reductionFloor = 1 - this.maxReduction * (1 - Math.min(1, speechPresence * 0.85));
728
+ if (gainTarget < reductionFloor) {
729
+ gainTarget = reductionFloor;
730
+ }
731
+
732
+ const dynamicRelease = this.release * (speechPresence > 0.1 ? 0.6 : 1);
733
+ state.gain += (gainTarget - state.gain) * dynamicRelease;
666
734
  let processed = sample * state.gain;
667
735
 
668
736
  state.lpState = this.hfAlpha * state.lpState + (1 - this.hfAlpha) * processed;
@@ -671,11 +739,13 @@ class SpatialAudioManager extends EventManager_1.EventManager {
671
739
  1,
672
740
  Math.abs(highComponent) / (Math.abs(state.lpState) + 1e-5)
673
741
  );
674
- const hissGain = 1 - hissRatio * this.hissCut;
742
+ const hissGain = 1 - hissRatio * (this.hissCut * (1 - 0.4 * speechPresence));
675
743
  processed = state.lpState + highComponent * hissGain;
676
744
 
677
745
  outChannel[i] = processed;
678
746
  }
747
+
748
+ this._updateVoiceConfidence(state);
679
749
  }
680
750
 
681
751
  return true;
@@ -709,15 +779,17 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
709
779
  };
710
780
  const denoiserDefaults = {
711
781
  enabled: true,
712
- threshold: 0.012,
713
- noiseFloor: 0.004,
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,
782
+ threshold: 0.009,
783
+ noiseFloor: 0.0025,
784
+ release: 0.24,
785
+ attack: 0.25,
786
+ holdMs: 150,
787
+ maxReduction: 0.92,
788
+ hissCut: 0.62,
789
+ expansionRatio: 2.35,
790
+ learnRate: 0.06,
791
+ voiceBoost: 0.6,
792
+ voiceSensitivity: 0.35,
721
793
  };
722
794
  return {
723
795
  distance: {
@@ -737,6 +809,8 @@ registerProcessor('odyssey-denoise', OdysseyDenoiseProcessor);
737
809
  hissCut: options?.denoiser?.hissCut ?? denoiserDefaults.hissCut,
738
810
  expansionRatio: options?.denoiser?.expansionRatio ?? denoiserDefaults.expansionRatio,
739
811
  learnRate: options?.denoiser?.learnRate ?? denoiserDefaults.learnRate,
812
+ voiceBoost: options?.denoiser?.voiceBoost ?? denoiserDefaults.voiceBoost,
813
+ voiceSensitivity: options?.denoiser?.voiceSensitivity ?? denoiserDefaults.voiceSensitivity,
740
814
  },
741
815
  };
742
816
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EventManager } from "./EventManager";
2
+ import { SpatialAudioOptions } from "./SpatialAudioManager";
2
3
  import { Direction, MediaState, OdysseyEvent, Participant, Position, RoomJoinedData } from "./types";
3
4
  export declare class OdysseySpatialComms extends EventManager {
4
5
  private socket;
@@ -9,7 +10,7 @@ export declare class OdysseySpatialComms extends EventManager {
9
10
  private localParticipant;
10
11
  private mediasoupManager;
11
12
  private spatialAudioManager;
12
- constructor(serverUrl: string);
13
+ constructor(serverUrl: string, spatialOptions?: SpatialAudioOptions);
13
14
  on(event: OdysseyEvent, listener: (...args: any[]) => void): this;
14
15
  emit(event: OdysseyEvent, ...args: any[]): boolean;
15
16
  joinRoom(data: {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ const EventManager_1 = require("./EventManager");
6
6
  const MediasoupManager_1 = require("./MediasoupManager");
7
7
  const SpatialAudioManager_1 = require("./SpatialAudioManager");
8
8
  class OdysseySpatialComms extends EventManager_1.EventManager {
9
- constructor(serverUrl) {
9
+ constructor(serverUrl, spatialOptions) {
10
10
  super(); // Initialize the EventEmitter base class
11
11
  this.room = null;
12
12
  this.localParticipant = null;
@@ -14,7 +14,7 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
14
14
  transports: ["websocket"],
15
15
  });
16
16
  this.mediasoupManager = new MediasoupManager_1.MediasoupManager(this.socket);
17
- this.spatialAudioManager = new SpatialAudioManager_1.SpatialAudioManager();
17
+ this.spatialAudioManager = new SpatialAudioManager_1.SpatialAudioManager(spatialOptions);
18
18
  // Set max listeners to prevent warning
19
19
  this.setMaxListeners(50);
20
20
  this.listenForEvents();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
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",