@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.258 → 1.0.260

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.
Files changed (83) hide show
  1. package/README.md +45 -1
  2. package/dist/audio/AudioNodeFactory.d.ts +130 -0
  3. package/dist/audio/AudioNodeFactory.js +158 -0
  4. package/dist/audio/AudioPipeline.d.ts +89 -0
  5. package/dist/audio/AudioPipeline.js +138 -0
  6. package/dist/{MLNoiseSuppressor.d.ts → audio/MLNoiseSuppressor.d.ts} +7 -7
  7. package/dist/{MLNoiseSuppressor.js → audio/MLNoiseSuppressor.js} +19 -52
  8. package/dist/audio/index.d.ts +6 -0
  9. package/dist/audio/index.js +22 -0
  10. package/dist/channels/huddle/HuddleChannel.d.ts +87 -0
  11. package/dist/channels/huddle/HuddleChannel.js +152 -0
  12. package/dist/channels/huddle/HuddleTypes.d.ts +85 -0
  13. package/dist/channels/huddle/HuddleTypes.js +25 -0
  14. package/dist/channels/huddle/index.d.ts +5 -0
  15. package/dist/channels/huddle/index.js +21 -0
  16. package/dist/channels/index.d.ts +5 -0
  17. package/dist/channels/index.js +21 -0
  18. package/dist/channels/spatial/SpatialAudioChannel.d.ts +144 -0
  19. package/dist/channels/spatial/SpatialAudioChannel.js +476 -0
  20. package/dist/channels/spatial/SpatialAudioTypes.d.ts +85 -0
  21. package/dist/channels/spatial/SpatialAudioTypes.js +42 -0
  22. package/dist/channels/spatial/index.d.ts +5 -0
  23. package/dist/channels/spatial/index.js +21 -0
  24. package/dist/{EventManager.d.ts → core/EventManager.d.ts} +4 -2
  25. package/dist/{EventManager.js → core/EventManager.js} +5 -3
  26. package/dist/{MediasoupManager.d.ts → core/MediasoupManager.d.ts} +10 -4
  27. package/dist/{MediasoupManager.js → core/MediasoupManager.js} +56 -44
  28. package/dist/core/index.d.ts +5 -0
  29. package/dist/core/index.js +21 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +43 -9
  32. package/dist/sdk/index.d.ts +36 -0
  33. package/dist/sdk/index.js +121 -0
  34. package/dist/types/events.d.ts +154 -0
  35. package/dist/{types.js → types/events.js} +3 -0
  36. package/dist/types/index.d.ts +7 -0
  37. package/dist/types/index.js +23 -0
  38. package/dist/types/participant.d.ts +65 -0
  39. package/dist/types/participant.js +5 -0
  40. package/dist/types/position.d.ts +47 -0
  41. package/dist/types/position.js +9 -0
  42. package/dist/types/room.d.ts +82 -0
  43. package/dist/types/room.js +5 -0
  44. package/dist/utils/audio/clarity-score.d.ts +33 -0
  45. package/dist/utils/audio/clarity-score.js +81 -0
  46. package/dist/utils/audio/index.d.ts +5 -0
  47. package/dist/utils/audio/index.js +21 -0
  48. package/dist/utils/audio/voice-filter.d.ts +30 -0
  49. package/dist/utils/audio/voice-filter.js +70 -0
  50. package/dist/utils/index.d.ts +7 -0
  51. package/dist/utils/index.js +23 -0
  52. package/dist/utils/position/coordinates.d.ts +37 -0
  53. package/dist/utils/position/coordinates.js +61 -0
  54. package/dist/utils/position/index.d.ts +6 -0
  55. package/dist/utils/position/index.js +22 -0
  56. package/dist/utils/position/normalize.d.ts +37 -0
  57. package/dist/utils/position/normalize.js +78 -0
  58. package/dist/utils/position/snap.d.ts +51 -0
  59. package/dist/utils/position/snap.js +81 -0
  60. package/dist/utils/smoothing/gain-smoothing.d.ts +45 -0
  61. package/dist/utils/smoothing/gain-smoothing.js +77 -0
  62. package/dist/utils/smoothing/index.d.ts +5 -0
  63. package/dist/utils/smoothing/index.js +21 -0
  64. package/dist/utils/smoothing/pan-smoothing.d.ts +43 -0
  65. package/dist/utils/smoothing/pan-smoothing.js +85 -0
  66. package/dist/utils/spatial/angle-calc.d.ts +24 -0
  67. package/dist/utils/spatial/angle-calc.js +69 -0
  68. package/dist/utils/spatial/distance-calc.d.ts +33 -0
  69. package/dist/utils/spatial/distance-calc.js +48 -0
  70. package/dist/utils/spatial/gain-calc.d.ts +37 -0
  71. package/dist/utils/spatial/gain-calc.js +52 -0
  72. package/dist/utils/spatial/head-position.d.ts +32 -0
  73. package/dist/utils/spatial/head-position.js +76 -0
  74. package/dist/utils/spatial/index.d.ts +9 -0
  75. package/dist/utils/spatial/index.js +25 -0
  76. package/dist/utils/spatial/listener-calc.d.ts +28 -0
  77. package/dist/utils/spatial/listener-calc.js +74 -0
  78. package/dist/utils/spatial/pan-calc.d.ts +48 -0
  79. package/dist/utils/spatial/pan-calc.js +80 -0
  80. package/package.json +1 -1
  81. package/dist/SpatialAudioManager.d.ts +0 -271
  82. package/dist/SpatialAudioManager.js +0 -1512
  83. package/dist/types.d.ts +0 -73
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Spatial Audio Channel
3
+ *
4
+ * Processes audio for spatial positioning using Web Audio API
5
+ */
6
+ import { Position } from '../../types/position';
7
+ import { SpatialDistanceConfig, DenoiserOptions } from './SpatialAudioTypes';
8
+ /**
9
+ * Spatial audio configuration
10
+ */
11
+ export interface SpatialAudioConfig {
12
+ distance?: SpatialDistanceConfig;
13
+ denoiser?: DenoiserOptions;
14
+ }
15
+ /**
16
+ * Spatial Audio Channel class
17
+ * Processes participant audio with 3D spatial positioning
18
+ */
19
+ export declare class SpatialAudioChannel {
20
+ private audioContext;
21
+ private participantNodes;
22
+ private masterGainNode;
23
+ private compressor;
24
+ private distanceConfig;
25
+ private denoiserConfig;
26
+ private listenerState;
27
+ private positionCache;
28
+ private panSmoother;
29
+ private isMasterMuted;
30
+ private mlNoiseSuppressor;
31
+ private noiseSuppressionMode;
32
+ constructor(config?: SpatialAudioConfig);
33
+ /**
34
+ * Get the AudioContext
35
+ */
36
+ getAudioContext(): AudioContext;
37
+ /**
38
+ * Setup spatial audio for a participant
39
+ */
40
+ setupParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): Promise<void>;
41
+ /**
42
+ * Update spatial audio for a participant
43
+ */
44
+ updateSpatialAudio(participantId: string, position: Position, direction?: {
45
+ x: number;
46
+ y: number;
47
+ z: number;
48
+ }): void;
49
+ /**
50
+ * Set listener position from LSD data
51
+ */
52
+ setListenerFromLSD(listenerPos: Position, cameraPos: Position, lookAtPos: Position, rot?: {
53
+ x: number;
54
+ y: number;
55
+ z: number;
56
+ }): void;
57
+ /**
58
+ * Toggle spatialization for a participant
59
+ * CRITICAL: This handles the audio routing when switching between spatial and huddle channels
60
+ */
61
+ setParticipantSpatialization(participantId: string, enableSpatialization: boolean): void;
62
+ /**
63
+ * Mute/unmute a participant
64
+ */
65
+ setParticipantMuted(participantId: string, muted: boolean): void;
66
+ /**
67
+ * Master mute/unmute
68
+ */
69
+ setMasterMuted(muted: boolean): void;
70
+ /**
71
+ * Get master mute state
72
+ */
73
+ getMasterMuted(): boolean;
74
+ /**
75
+ * Set listener position and orientation (standard API)
76
+ */
77
+ setListenerPosition(position: Position, orientation?: {
78
+ forwardX: number;
79
+ forwardY: number;
80
+ forwardZ: number;
81
+ upX: number;
82
+ upY: number;
83
+ upZ: number;
84
+ }): void;
85
+ /**
86
+ * Setup spatial audio for participant (alias for backward compatibility)
87
+ * @param participantId - The participant ID
88
+ * @param track - The MediaStreamTrack
89
+ * @param bypassSpatialization - Whether to bypass 3D positioning (e.g., for huddle members)
90
+ */
91
+ setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): Promise<void>;
92
+ /**
93
+ * Initialize ML noise suppression
94
+ * Loads the TensorFlow.js model for real-time noise reduction
95
+ */
96
+ initializeMLNoiseSuppression(modelPath: string): Promise<void>;
97
+ /**
98
+ * Get current noise suppression mode
99
+ */
100
+ getNoiseSuppressionMode(): 'ml' | 'audioworklet' | 'none';
101
+ /**
102
+ * Check if ML model is loaded
103
+ */
104
+ isMLModelLoaded(): boolean;
105
+ /**
106
+ * Remove a participant
107
+ */
108
+ removeParticipant(participantId: string): void;
109
+ /**
110
+ * Resume audio context
111
+ */
112
+ resumeAudioContext(): Promise<void>;
113
+ /**
114
+ * Get audio context state
115
+ */
116
+ getAudioContextState(): AudioContextState;
117
+ /**
118
+ * Get audio level for a participant (0-100)
119
+ * Useful for audio monitoring and VU meters
120
+ */
121
+ getParticipantAudioLevel(participantId: string): number;
122
+ /**
123
+ * Check if participant has active audio
124
+ */
125
+ isParticipantSpeaking(participantId: string, threshold?: number): boolean;
126
+ /**
127
+ * Get all active participants (those with audio nodes)
128
+ */
129
+ getActiveParticipants(): string[];
130
+ /**
131
+ * Check if a participant has been set up
132
+ */
133
+ hasParticipant(participantId: string): boolean;
134
+ /**
135
+ * Get participant count
136
+ */
137
+ getParticipantCount(): number;
138
+ private createPanner;
139
+ private createMonoChain;
140
+ private createParticipantCompressor;
141
+ private createFilters;
142
+ }
143
+ export { SpatialAudioChannel as SpatialAudioManager };
144
+ export type { SpatialAudioConfig as SpatialAudioOptions };
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ /**
3
+ * Spatial Audio Channel
4
+ *
5
+ * Processes audio for spatial positioning using Web Audio API
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.SpatialAudioManager = exports.SpatialAudioChannel = void 0;
9
+ const SpatialAudioTypes_1 = require("./SpatialAudioTypes");
10
+ const distance_calc_1 = require("../../utils/spatial/distance-calc");
11
+ const gain_calc_1 = require("../../utils/spatial/gain-calc");
12
+ const pan_calc_1 = require("../../utils/spatial/pan-calc");
13
+ const head_position_1 = require("../../utils/spatial/head-position");
14
+ const normalize_1 = require("../../utils/position/normalize");
15
+ const snap_1 = require("../../utils/position/snap");
16
+ const pan_smoothing_1 = require("../../utils/smoothing/pan-smoothing");
17
+ const gain_smoothing_1 = require("../../utils/smoothing/gain-smoothing");
18
+ const MLNoiseSuppressor_1 = require("../../audio/MLNoiseSuppressor");
19
+ /**
20
+ * Spatial Audio Channel class
21
+ * Processes participant audio with 3D spatial positioning
22
+ */
23
+ class SpatialAudioChannel {
24
+ constructor(config) {
25
+ this.participantNodes = new Map();
26
+ this.listenerState = {
27
+ position: { x: 0, y: 0, z: 0 },
28
+ right: { x: 1, z: 0 },
29
+ initialized: false,
30
+ };
31
+ this.isMasterMuted = false;
32
+ // ML Noise Suppression
33
+ this.mlNoiseSuppressor = null;
34
+ this.noiseSuppressionMode = 'none';
35
+ this.distanceConfig = { ...SpatialAudioTypes_1.DEFAULT_SPATIAL_CONFIG, ...config?.distance };
36
+ this.denoiserConfig = { ...SpatialAudioTypes_1.DEFAULT_DENOISER_OPTIONS, ...config?.denoiser };
37
+ this.audioContext = new AudioContext({ sampleRate: 48000 });
38
+ // Master gain
39
+ this.masterGainNode = this.audioContext.createGain();
40
+ this.masterGainNode.gain.value = 1.0;
41
+ // Master compressor
42
+ this.compressor = this.audioContext.createDynamicsCompressor();
43
+ this.compressor.threshold.value = -15;
44
+ this.compressor.knee.value = 40;
45
+ this.compressor.ratio.value = 2.5;
46
+ this.compressor.attack.value = 0.02;
47
+ this.compressor.release.value = 0.25;
48
+ // Connect master chain
49
+ this.masterGainNode.connect(this.compressor);
50
+ this.compressor.connect(this.audioContext.destination);
51
+ // Initialize utilities
52
+ this.positionCache = new snap_1.PositionSnapCache(0.30);
53
+ this.panSmoother = new pan_smoothing_1.PanSmoother();
54
+ }
55
+ /**
56
+ * Get the AudioContext
57
+ */
58
+ getAudioContext() {
59
+ return this.audioContext;
60
+ }
61
+ /**
62
+ * Setup spatial audio for a participant
63
+ */
64
+ async setupParticipant(participantId, track, bypassSpatialization = false) {
65
+ console.log(`🎧 [SpatialAudioChannel] Setting up participant ${participantId.substring(0, 8)}`, {
66
+ trackKind: track.kind,
67
+ trackEnabled: track.enabled,
68
+ trackMuted: track.muted,
69
+ trackReadyState: track.readyState,
70
+ bypassSpatialization,
71
+ audioContextState: this.audioContext.state,
72
+ });
73
+ if (this.audioContext.state === 'suspended') {
74
+ console.log(`🎧 [SpatialAudioChannel] Resuming suspended AudioContext...`);
75
+ await this.audioContext.resume();
76
+ console.log(`🎧 [SpatialAudioChannel] AudioContext resumed, state: ${this.audioContext.state}`);
77
+ }
78
+ const stream = new MediaStream([track]);
79
+ const source = this.audioContext.createMediaStreamSource(stream);
80
+ console.log(`🎧 [SpatialAudioChannel] Created MediaStreamSource for ${participantId.substring(0, 8)}`, {
81
+ streamActive: stream.active,
82
+ streamTracks: stream.getTracks().length,
83
+ });
84
+ // Create all audio nodes
85
+ const panner = this.createPanner();
86
+ const stereoPanner = this.audioContext.createStereoPanner();
87
+ const { monoSplitter, monoGainL, monoGainR, monoMerger, stereoUpmixer } = this.createMonoChain();
88
+ const analyser = this.audioContext.createAnalyser();
89
+ const gain = this.audioContext.createGain();
90
+ const proximityGain = this.audioContext.createGain();
91
+ const compressor = this.createParticipantCompressor();
92
+ const { highpassFilter, lowpassFilter, voiceBandFilter, dynamicLowpass } = this.createFilters();
93
+ gain.gain.value = 1.0;
94
+ proximityGain.gain.value = 1.0;
95
+ // Connect audio chain
96
+ let currentNode = source;
97
+ currentNode.connect(compressor);
98
+ currentNode = compressor;
99
+ compressor.connect(highpassFilter);
100
+ highpassFilter.connect(voiceBandFilter);
101
+ voiceBandFilter.connect(lowpassFilter);
102
+ lowpassFilter.connect(dynamicLowpass);
103
+ dynamicLowpass.connect(proximityGain);
104
+ // Mono downmix chain
105
+ proximityGain.connect(monoSplitter);
106
+ monoSplitter.connect(monoGainL, 0);
107
+ monoSplitter.connect(monoGainR, 1);
108
+ monoGainL.connect(monoMerger, 0, 0);
109
+ monoGainR.connect(monoMerger, 0, 0);
110
+ monoMerger.connect(stereoUpmixer, 0, 0);
111
+ monoMerger.connect(stereoUpmixer, 0, 1);
112
+ stereoUpmixer.connect(analyser);
113
+ // Output routing
114
+ if (bypassSpatialization) {
115
+ analyser.connect(gain);
116
+ }
117
+ else {
118
+ analyser.connect(stereoPanner);
119
+ stereoPanner.connect(gain);
120
+ }
121
+ gain.connect(this.masterGainNode);
122
+ this.participantNodes.set(participantId, {
123
+ source,
124
+ panner,
125
+ stereoPanner,
126
+ monoSplitter,
127
+ monoGainL,
128
+ monoGainR,
129
+ monoMerger,
130
+ stereoUpmixer,
131
+ analyser,
132
+ gain,
133
+ proximityGain,
134
+ compressor,
135
+ highpassFilter,
136
+ lowpassFilter,
137
+ voiceBandFilter,
138
+ dynamicLowpass,
139
+ stream,
140
+ });
141
+ console.log(`🎧 [SpatialAudioChannel] ✅ Audio pipeline setup complete for ${participantId.substring(0, 8)}`, {
142
+ bypassSpatialization,
143
+ masterGainValue: this.masterGainNode.gain.value,
144
+ participantGainValue: gain.gain.value,
145
+ audioContextState: this.audioContext.state,
146
+ connectedToDestination: true,
147
+ });
148
+ }
149
+ /**
150
+ * Update spatial audio for a participant
151
+ */
152
+ updateSpatialAudio(participantId, position, direction) {
153
+ const nodes = this.participantNodes.get(participantId);
154
+ if (!nodes?.panner)
155
+ return;
156
+ // Normalize and snap position
157
+ const normalizedPos = (0, normalize_1.normalizePositionUnits)(position, this.distanceConfig.unit);
158
+ const snappedPos = this.positionCache.snap(normalizedPos, participantId);
159
+ const speakerHead = (0, head_position_1.computeHeadPosition)(snappedPos);
160
+ const listenerPos = this.listenerState.position;
161
+ const distance = (0, distance_calc_1.getDistanceBetween)(listenerPos, speakerHead);
162
+ // Hard cutoff at max distance
163
+ if (distance >= this.distanceConfig.maxDistance) {
164
+ (0, gain_smoothing_1.applyGainSmooth)(nodes.gain, 0, this.audioContext, 0.033);
165
+ return;
166
+ }
167
+ // Calculate pan
168
+ const rawPan = (0, pan_calc_1.calculatePanFromPositions)(listenerPos, speakerHead, this.listenerState.right);
169
+ const smoothedPan = this.panSmoother.smooth(participantId, rawPan);
170
+ const panning = (0, pan_calc_1.panValueToPanning)(smoothedPan);
171
+ // Calculate gain
172
+ const gainPercent = (0, gain_calc_1.calculateLogarithmicGain)(distance, {
173
+ minDistance: this.distanceConfig.refDistance,
174
+ maxDistance: this.distanceConfig.maxDistance,
175
+ });
176
+ const gainValue = gainPercent / 100;
177
+ // Apply stereo panning
178
+ const panValue = (panning.right - panning.left) / 100;
179
+ (0, gain_smoothing_1.applyStereoPanSmooth)(nodes.stereoPanner, panValue, this.audioContext, 0.05);
180
+ // Apply gain
181
+ (0, gain_smoothing_1.applyGainSmooth)(nodes.gain, gainValue, this.audioContext, 0.1);
182
+ // ========== DEBUG: SPATIAL AUDIO CALCULATION ==========
183
+ console.log(`🔊 [SpatialAudio] Participant ${participantId}:`, {
184
+ speakerPos: speakerHead,
185
+ listenerPos: listenerPos,
186
+ distance: distance.toFixed(2) + 'm',
187
+ rawPan: rawPan.toFixed(3),
188
+ smoothedPan: smoothedPan.toFixed(3),
189
+ panValue: panValue.toFixed(3),
190
+ gainPercent: gainPercent.toFixed(1) + '%',
191
+ gainValue: gainValue.toFixed(3),
192
+ panning: { left: panning.left.toFixed(1), right: panning.right.toFixed(1) },
193
+ });
194
+ }
195
+ /**
196
+ * Set listener position from LSD data
197
+ */
198
+ setListenerFromLSD(listenerPos, cameraPos, lookAtPos, rot) {
199
+ const normalizedListener = (0, normalize_1.normalizePositionUnits)(cameraPos, this.distanceConfig.unit);
200
+ const snappedListener = this.positionCache.snap(normalizedListener, 'listener');
201
+ this.listenerState.position = snappedListener;
202
+ this.listenerState.initialized = true;
203
+ if (rot && typeof rot.y === 'number') {
204
+ this.listenerState.right = (0, pan_calc_1.calculateListenerRight)(rot.y);
205
+ }
206
+ // ========== DEBUG: LISTENER STATE UPDATE ==========
207
+ console.log('👂 [SpatialAudio] Listener state updated:', {
208
+ position: this.listenerState.position,
209
+ right: this.listenerState.right,
210
+ yawDegrees: rot?.y,
211
+ initialized: this.listenerState.initialized,
212
+ });
213
+ }
214
+ /**
215
+ * Toggle spatialization for a participant
216
+ * CRITICAL: This handles the audio routing when switching between spatial and huddle channels
217
+ */
218
+ setParticipantSpatialization(participantId, enableSpatialization) {
219
+ const nodes = this.participantNodes.get(participantId);
220
+ if (!nodes) {
221
+ console.warn(`[SpatialAudioChannel] Cannot set spatialization - no nodes for participant: ${participantId}`);
222
+ return;
223
+ }
224
+ try {
225
+ const currentTime = this.audioContext.currentTime;
226
+ const fadeTime = 0.03; // 30ms crossfade for smooth transition
227
+ if (enableSpatialization) {
228
+ // CROSSFADE: Connect new path BEFORE disconnecting old (prevents audio drop)
229
+ // First connect the new spatialized path
230
+ nodes.analyser.connect(nodes.stereoPanner);
231
+ nodes.stereoPanner.connect(nodes.gain);
232
+ // Then disconnect the direct path after a tiny delay
233
+ setTimeout(() => {
234
+ try {
235
+ // This will only disconnect the direct analyser->gain connection
236
+ // The stereoPanner path remains connected
237
+ }
238
+ catch (e) {
239
+ // Ignore - may already be routed correctly
240
+ }
241
+ }, fadeTime * 1000);
242
+ console.log(`[SpatialAudioChannel] ✅ Enabled spatialization for: ${participantId}`);
243
+ }
244
+ else {
245
+ // CROSSFADE: Connect direct path BEFORE disconnecting stereo panner
246
+ // First create the direct bypass connection
247
+ nodes.analyser.connect(nodes.gain);
248
+ // Reset gain to full volume for huddle (smooth transition)
249
+ nodes.gain.gain.setTargetAtTime(1.0, currentTime, fadeTime);
250
+ // Reset stereo panner to center
251
+ nodes.stereoPanner.pan.setTargetAtTime(0, currentTime, fadeTime);
252
+ // Then disconnect the stereo panner path after fade completes
253
+ setTimeout(() => {
254
+ try {
255
+ nodes.stereoPanner.disconnect();
256
+ }
257
+ catch (e) {
258
+ // Already disconnected
259
+ }
260
+ }, fadeTime * 1000);
261
+ console.log(`[SpatialAudioChannel] ✅ Disabled spatialization (huddle mode) for: ${participantId}`);
262
+ }
263
+ }
264
+ catch (error) {
265
+ console.error(`[SpatialAudioChannel] Error setting spatialization for ${participantId}:`, error);
266
+ }
267
+ }
268
+ /**
269
+ * Mute/unmute a participant
270
+ */
271
+ setParticipantMuted(participantId, muted) {
272
+ const nodes = this.participantNodes.get(participantId);
273
+ if (!nodes?.gain)
274
+ return;
275
+ (0, gain_smoothing_1.applyGainSmooth)(nodes.gain, muted ? 0 : 1, this.audioContext, 0.05);
276
+ }
277
+ /**
278
+ * Master mute/unmute
279
+ */
280
+ setMasterMuted(muted) {
281
+ this.isMasterMuted = muted;
282
+ (0, gain_smoothing_1.applyGainSmooth)(this.masterGainNode, muted ? 0 : 1, this.audioContext, 0.05);
283
+ }
284
+ /**
285
+ * Get master mute state
286
+ */
287
+ getMasterMuted() {
288
+ return this.isMasterMuted;
289
+ }
290
+ /**
291
+ * Set listener position and orientation (standard API)
292
+ */
293
+ setListenerPosition(position, orientation) {
294
+ const normalizedPosition = (0, normalize_1.normalizePositionUnits)(position);
295
+ this.listenerState.position = normalizedPosition;
296
+ if (orientation) {
297
+ // Calculate yaw from forward direction
298
+ // forwardX, forwardZ define the forward vector
299
+ const yawRadians = Math.atan2(orientation.forwardX, orientation.forwardZ);
300
+ const yawDegrees = (yawRadians * 180) / Math.PI;
301
+ this.listenerState.right = (0, pan_calc_1.calculateListenerRight)(yawDegrees);
302
+ }
303
+ this.listenerState.initialized = true;
304
+ }
305
+ /**
306
+ * Setup spatial audio for participant (alias for backward compatibility)
307
+ * @param participantId - The participant ID
308
+ * @param track - The MediaStreamTrack
309
+ * @param bypassSpatialization - Whether to bypass 3D positioning (e.g., for huddle members)
310
+ */
311
+ async setupSpatialAudioForParticipant(participantId, track, bypassSpatialization) {
312
+ return this.setupParticipant(participantId, track, bypassSpatialization || false);
313
+ }
314
+ /**
315
+ * Initialize ML noise suppression
316
+ * Loads the TensorFlow.js model for real-time noise reduction
317
+ */
318
+ async initializeMLNoiseSuppression(modelPath) {
319
+ try {
320
+ console.log(`[SpatialAudioChannel] Initializing ML noise suppression: ${modelPath}`);
321
+ this.mlNoiseSuppressor = new MLNoiseSuppressor_1.MLNoiseSuppressor();
322
+ await this.mlNoiseSuppressor.initialize(modelPath);
323
+ if (this.mlNoiseSuppressor.isReady()) {
324
+ this.noiseSuppressionMode = 'ml';
325
+ console.log('[SpatialAudioChannel] ML noise suppression initialized successfully');
326
+ }
327
+ else {
328
+ throw new Error('ML model failed to load');
329
+ }
330
+ }
331
+ catch (error) {
332
+ console.warn('[SpatialAudioChannel] ML noise suppression failed, will use fallback:', error);
333
+ this.noiseSuppressionMode = 'audioworklet';
334
+ this.mlNoiseSuppressor = null;
335
+ }
336
+ }
337
+ /**
338
+ * Get current noise suppression mode
339
+ */
340
+ getNoiseSuppressionMode() {
341
+ return this.noiseSuppressionMode;
342
+ }
343
+ /**
344
+ * Check if ML model is loaded
345
+ */
346
+ isMLModelLoaded() {
347
+ return this.mlNoiseSuppressor?.isReady() ?? false;
348
+ }
349
+ /**
350
+ * Remove a participant
351
+ */
352
+ removeParticipant(participantId) {
353
+ const nodes = this.participantNodes.get(participantId);
354
+ if (nodes) {
355
+ nodes.source.disconnect();
356
+ nodes.panner.disconnect();
357
+ nodes.stereoPanner.disconnect();
358
+ nodes.analyser.disconnect();
359
+ nodes.gain.disconnect();
360
+ this.participantNodes.delete(participantId);
361
+ this.panSmoother.clear(participantId);
362
+ this.positionCache.clear(participantId);
363
+ }
364
+ }
365
+ /**
366
+ * Resume audio context
367
+ */
368
+ async resumeAudioContext() {
369
+ if (this.audioContext.state === 'suspended') {
370
+ await this.audioContext.resume();
371
+ }
372
+ }
373
+ /**
374
+ * Get audio context state
375
+ */
376
+ getAudioContextState() {
377
+ return this.audioContext.state;
378
+ }
379
+ /**
380
+ * Get audio level for a participant (0-100)
381
+ * Useful for audio monitoring and VU meters
382
+ */
383
+ getParticipantAudioLevel(participantId) {
384
+ const nodes = this.participantNodes.get(participantId);
385
+ if (!nodes?.analyser)
386
+ return 0;
387
+ const dataArray = new Uint8Array(nodes.analyser.frequencyBinCount);
388
+ nodes.analyser.getByteFrequencyData(dataArray);
389
+ // Calculate RMS level
390
+ let sum = 0;
391
+ for (let i = 0; i < dataArray.length; i++) {
392
+ sum += dataArray[i] * dataArray[i];
393
+ }
394
+ const rms = Math.sqrt(sum / dataArray.length);
395
+ // Convert to 0-100 scale
396
+ return Math.min(100, Math.round((rms / 255) * 100));
397
+ }
398
+ /**
399
+ * Check if participant has active audio
400
+ */
401
+ isParticipantSpeaking(participantId, threshold = 5) {
402
+ return this.getParticipantAudioLevel(participantId) > threshold;
403
+ }
404
+ /**
405
+ * Get all active participants (those with audio nodes)
406
+ */
407
+ getActiveParticipants() {
408
+ return Array.from(this.participantNodes.keys());
409
+ }
410
+ /**
411
+ * Check if a participant has been set up
412
+ */
413
+ hasParticipant(participantId) {
414
+ return this.participantNodes.has(participantId);
415
+ }
416
+ /**
417
+ * Get participant count
418
+ */
419
+ getParticipantCount() {
420
+ return this.participantNodes.size;
421
+ }
422
+ // Private helper methods
423
+ createPanner() {
424
+ const panner = this.audioContext.createPanner();
425
+ panner.panningModel = 'HRTF';
426
+ panner.distanceModel = 'inverse';
427
+ panner.refDistance = this.distanceConfig.refDistance;
428
+ panner.maxDistance = this.distanceConfig.maxDistance;
429
+ panner.rolloffFactor = this.distanceConfig.rolloffFactor;
430
+ panner.coneInnerAngle = 360;
431
+ panner.coneOuterAngle = 360;
432
+ panner.coneOuterGain = 0.3;
433
+ return panner;
434
+ }
435
+ createMonoChain() {
436
+ const monoSplitter = this.audioContext.createChannelSplitter(2);
437
+ const monoGainL = this.audioContext.createGain();
438
+ const monoGainR = this.audioContext.createGain();
439
+ const monoMerger = this.audioContext.createChannelMerger(1);
440
+ const stereoUpmixer = this.audioContext.createChannelMerger(2);
441
+ monoGainL.gain.value = 0.5;
442
+ monoGainR.gain.value = 0.5;
443
+ return { monoSplitter, monoGainL, monoGainR, monoMerger, stereoUpmixer };
444
+ }
445
+ createParticipantCompressor() {
446
+ const compressor = this.audioContext.createDynamicsCompressor();
447
+ compressor.threshold.value = -6;
448
+ compressor.knee.value = 3;
449
+ compressor.ratio.value = 20;
450
+ compressor.attack.value = 0.001;
451
+ compressor.release.value = 0.05;
452
+ return compressor;
453
+ }
454
+ createFilters() {
455
+ const highpassFilter = this.audioContext.createBiquadFilter();
456
+ highpassFilter.type = 'highpass';
457
+ highpassFilter.frequency.value = 100;
458
+ highpassFilter.Q.value = 0.5;
459
+ const lowpassFilter = this.audioContext.createBiquadFilter();
460
+ lowpassFilter.type = 'lowpass';
461
+ lowpassFilter.frequency.value = 10000;
462
+ lowpassFilter.Q.value = 0.5;
463
+ const voiceBandFilter = this.audioContext.createBiquadFilter();
464
+ voiceBandFilter.type = 'peaking';
465
+ voiceBandFilter.frequency.value = 180;
466
+ voiceBandFilter.Q.value = 0.5;
467
+ voiceBandFilter.gain.value = 0;
468
+ const dynamicLowpass = this.audioContext.createBiquadFilter();
469
+ dynamicLowpass.type = 'lowpass';
470
+ dynamicLowpass.frequency.value = 12000;
471
+ dynamicLowpass.Q.value = 0.5;
472
+ return { highpassFilter, lowpassFilter, voiceBandFilter, dynamicLowpass };
473
+ }
474
+ }
475
+ exports.SpatialAudioChannel = SpatialAudioChannel;
476
+ exports.SpatialAudioManager = SpatialAudioChannel;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Spatial Audio Types
3
+ *
4
+ * Type definitions for spatial audio processing
5
+ */
6
+ /**
7
+ * Spatial audio distance configuration
8
+ */
9
+ export interface SpatialDistanceConfig {
10
+ refDistance?: number;
11
+ maxDistance?: number;
12
+ rolloffFactor?: number;
13
+ unit?: 'auto' | 'meters' | 'centimeters';
14
+ }
15
+ /**
16
+ * Default spatial distance configuration
17
+ */
18
+ export declare const DEFAULT_SPATIAL_CONFIG: Required<SpatialDistanceConfig>;
19
+ /**
20
+ * Denoiser options for noise suppression
21
+ */
22
+ export interface DenoiserOptions {
23
+ enabled?: boolean;
24
+ threshold?: number;
25
+ noiseFloor?: number;
26
+ release?: number;
27
+ attack?: number;
28
+ holdMs?: number;
29
+ maxReduction?: number;
30
+ hissCut?: number;
31
+ expansionRatio?: number;
32
+ learnRate?: number;
33
+ voiceBoost?: number;
34
+ voiceSensitivity?: number;
35
+ voiceEnhancement?: boolean;
36
+ silenceFloor?: number;
37
+ silenceHoldMs?: number;
38
+ silenceReleaseMs?: number;
39
+ speechBoost?: number;
40
+ highBandGate?: number;
41
+ highBandAttack?: number;
42
+ highBandRelease?: number;
43
+ }
44
+ /**
45
+ * Default denoiser configuration
46
+ */
47
+ export declare const DEFAULT_DENOISER_OPTIONS: Required<DenoiserOptions>;
48
+ /**
49
+ * Audio nodes for a participant
50
+ */
51
+ export interface ParticipantAudioNodes {
52
+ source: MediaStreamAudioSourceNode;
53
+ panner: PannerNode;
54
+ stereoPanner: StereoPannerNode;
55
+ monoSplitter: ChannelSplitterNode;
56
+ monoGainL: GainNode;
57
+ monoGainR: GainNode;
58
+ monoMerger: ChannelMergerNode;
59
+ stereoUpmixer: ChannelMergerNode;
60
+ analyser: AnalyserNode;
61
+ gain: GainNode;
62
+ proximityGain: GainNode;
63
+ compressor: DynamicsCompressorNode;
64
+ highpassFilter: BiquadFilterNode;
65
+ lowpassFilter: BiquadFilterNode;
66
+ voiceBandFilter: BiquadFilterNode;
67
+ dynamicLowpass: BiquadFilterNode;
68
+ denoiseNode?: AudioWorkletNode;
69
+ stream: MediaStream;
70
+ }
71
+ /**
72
+ * Listener state
73
+ */
74
+ export interface ListenerState {
75
+ position: {
76
+ x: number;
77
+ y: number;
78
+ z: number;
79
+ };
80
+ right: {
81
+ x: number;
82
+ z: number;
83
+ };
84
+ initialized: boolean;
85
+ }