@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 +1 -1
- package/dist/SpatialAudioManager.d.ts +3 -3
- package/dist/SpatialAudioManager.js +24 -22
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
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 = -
|
|
189
|
+
participantCompressor.threshold.value = -30; // Higher threshold (less compression)
|
|
189
190
|
participantCompressor.knee.value = 10; // Smooth knee for natural sound
|
|
190
|
-
participantCompressor.ratio.value =
|
|
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
|
|
214
|
-
// Audio Chain: source -> filters -> panner -> analyser -> gain -> masterGain -> compressor -> destination
|
|
215
|
-
proximityGain.connect(
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
*
|
|
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?.
|
|
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
|
-
//
|
|
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.
|
|
792
|
+
nodes.stereoPanner.pan.setTargetAtTime(panValue, currentTime, 0.05);
|
|
791
793
|
}
|
|
792
794
|
async ensureDenoiseWorklet() {
|
|
793
795
|
if (!this.isDenoiserEnabled()) {
|
package/package.json
CHANGED