@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 +13 -0
- package/dist/SpatialAudioManager.d.ts +3 -1
- package/dist/SpatialAudioManager.js +85 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
713
|
-
noiseFloor: 0.
|
|
714
|
-
release: 0.
|
|
715
|
-
attack: 0.
|
|
716
|
-
holdMs:
|
|
717
|
-
maxReduction: 0.
|
|
718
|
-
hissCut: 0.
|
|
719
|
-
expansionRatio:
|
|
720
|
-
learnRate: 0.
|
|
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