@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.60 → 1.0.62

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.
@@ -74,6 +74,12 @@ export declare class SpatialAudioManager extends EventManager {
74
74
  */
75
75
  setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): Promise<void>;
76
76
  private startMonitoring;
77
+ /**
78
+ * Toggle spatialization for a participant (for huddle/spatial switching)
79
+ * @param participantId The participant to update
80
+ * @param enableSpatialization True for spatial audio, false for non-spatial (huddle)
81
+ */
82
+ setParticipantSpatialization(participantId: string, enableSpatialization: boolean): void;
77
83
  /**
78
84
  * Update spatial audio position and orientation for a participant
79
85
  *
@@ -131,12 +137,49 @@ export declare class SpatialAudioManager extends EventManager {
131
137
  private normalizePositionUnits;
132
138
  private getVectorFromListener;
133
139
  private applyDirectionalSuppression;
140
+ /**
141
+ * Dynamically adjust highpass filter based on voice characteristics
142
+ * Analyzes audio spectrum and sets filter between 85-300Hz
143
+ */
144
+ private adjustVoiceAdaptiveFilter;
134
145
  private calculateClarityScore;
135
146
  private calculateProximityWeight;
136
147
  private calculateDirectionFocus;
137
148
  private normalizeVector;
138
149
  private clamp;
139
150
  private isDenoiserEnabled;
151
+ /**
152
+ * Calculate angle between listener and sound source in degrees (0-360)
153
+ * 0° = front, 90° = right, 180° = back, 270° = left
154
+ */
155
+ private calculateAngle;
156
+ /**
157
+ * Calculate stereo panning based on angle (0-360°)
158
+ * Returns { left: 0-100, right: 0-100 }
159
+ *
160
+ * Reference angles:
161
+ * 0° (front): L100, R100
162
+ * 45° (front-right): L50, R100
163
+ * 90° (right): L0, R100
164
+ * 135° (back-right): L0, R50
165
+ * 180° (back): L50, R50
166
+ * 225° (back-left): L50, R0
167
+ * 270° (left): L100, R0
168
+ * 315° (front-left): L100, R50
169
+ */
170
+ private calculatePanning;
171
+ /**
172
+ * Calculate gain based on distance using logarithmic scale
173
+ * Distance range: 0.5m to 5m
174
+ * Gain range: 100% to 0%
175
+ * Uses quadratic equation for human ear perception
176
+ */
177
+ private calculateLogarithmicGain;
178
+ /**
179
+ * Apply stereo panning to participant audio
180
+ * Converts panning percentages to StereoPanner values
181
+ */
182
+ private applyStereoPanning;
140
183
  private ensureDenoiseWorklet;
141
184
  private resolveOptions;
142
185
  }
@@ -142,17 +142,31 @@ class SpatialAudioManager extends EventManager_1.EventManager {
142
142
  }
143
143
  // Create BiquadFilter nodes for static/noise reduction
144
144
  // Based on: https://tagdiwalaviral.medium.com/struggles-of-noise-reduction-in-rtc-part-2-2526f8179442
145
+ // HIGHPASS FILTER: Remove low-frequency rumble (< 80Hz)
146
+ // Human voice fundamental: 80-300Hz, harmonics: 300Hz-8kHz
147
+ // This cuts BELOW voice range while preserving full voice spectrum
145
148
  const highpassFilter = this.audioContext.createBiquadFilter();
146
149
  highpassFilter.type = "highpass";
147
- highpassFilter.frequency.value = 85; // Conservative value to preserve male voice depth
150
+ highpassFilter.frequency.value = 80; // Cut frequencies below 80Hz (removes rumble/pops)
148
151
  highpassFilter.Q.value = 1.0; // Quality factor
152
+ // LOWPASS FILTER: Remove high-frequency hiss (> 8000Hz)
153
+ // Voice harmonics extend to ~8kHz - this preserves full voice richness
154
+ // while removing digital artifacts and hiss ABOVE useful voice range
149
155
  const lowpassFilter = this.audioContext.createBiquadFilter();
150
156
  lowpassFilter.type = "lowpass";
151
- lowpassFilter.frequency.value = 7500; // Below 8kHz to avoid flat/muffled sound
157
+ lowpassFilter.frequency.value = 8000; // Cut frequencies above 8kHz (preserves voice harmonics)
152
158
  lowpassFilter.Q.value = 1.0; // Quality factor
159
+ // VOICE BAND EMPHASIS: Boost 80-300Hz fundamental range for clarity
160
+ // This emphasizes the base pitch without affecting harmonics
161
+ // Helps reduce the "pop" when someone starts speaking
162
+ const voiceBandFilter = this.audioContext.createBiquadFilter();
163
+ voiceBandFilter.type = "peaking";
164
+ voiceBandFilter.frequency.value = 180; // Center of voice fundamental (80-300Hz)
165
+ voiceBandFilter.Q.value = 1.5; // Moderate width (~100-260Hz affected)
166
+ voiceBandFilter.gain.value = 2; // +2dB boost for clarity
153
167
  const dynamicLowpass = this.audioContext.createBiquadFilter();
154
168
  dynamicLowpass.type = "lowpass";
155
- dynamicLowpass.frequency.value = 7600;
169
+ dynamicLowpass.frequency.value = 7500; // Fixed for all angles
156
170
  dynamicLowpass.Q.value = 0.8;
157
171
  proximityGain.gain.value = 1.0;
158
172
  // Configure Panner for realistic 3D spatial audio
@@ -172,8 +186,11 @@ class SpatialAudioManager extends EventManager_1.EventManager {
172
186
  currentNode.connect(denoiseNode);
173
187
  currentNode = denoiseNode;
174
188
  }
189
+ // Audio chain with voice optimization filters
190
+ // Chain: source -> [denoise] -> highpass -> voiceBand -> lowpass -> dynamicLowpass -> proximityGain -> panner -> analyser -> gain -> masterGain
175
191
  currentNode.connect(highpassFilter);
176
- highpassFilter.connect(lowpassFilter);
192
+ highpassFilter.connect(voiceBandFilter);
193
+ voiceBandFilter.connect(lowpassFilter);
177
194
  lowpassFilter.connect(dynamicLowpass);
178
195
  dynamicLowpass.connect(proximityGain);
179
196
  if (bypassSpatialization) {
@@ -196,6 +213,7 @@ class SpatialAudioManager extends EventManager_1.EventManager {
196
213
  proximityGain,
197
214
  highpassFilter,
198
215
  lowpassFilter,
216
+ voiceBandFilter,
199
217
  dynamicLowpass,
200
218
  denoiseNode,
201
219
  stream,
@@ -225,6 +243,40 @@ class SpatialAudioManager extends EventManager_1.EventManager {
225
243
  }, 2000); // Check every 2 seconds
226
244
  this.monitoringIntervals.set(participantId, interval);
227
245
  }
246
+ /**
247
+ * Toggle spatialization for a participant (for huddle/spatial switching)
248
+ * @param participantId The participant to update
249
+ * @param enableSpatialization True for spatial audio, false for non-spatial (huddle)
250
+ */
251
+ setParticipantSpatialization(participantId, enableSpatialization) {
252
+ const nodes = this.participantNodes.get(participantId);
253
+ if (!nodes) {
254
+ console.warn(`[SpatialAudio] No nodes found for participant ${participantId}`);
255
+ return;
256
+ }
257
+ // Disconnect and reconnect audio chain
258
+ try {
259
+ // Disconnect from current destination
260
+ nodes.proximityGain.disconnect();
261
+ if (enableSpatialization) {
262
+ // Connect through panner for 3D spatial audio
263
+ nodes.proximityGain.connect(nodes.panner);
264
+ nodes.panner.connect(nodes.analyser);
265
+ console.log(`🎯 [SpatialAudio] Enabled spatialization for ${participantId.substring(0, 8)}`);
266
+ }
267
+ else {
268
+ // Bypass panner for non-spatial (huddle) audio
269
+ nodes.proximityGain.connect(nodes.analyser);
270
+ console.log(`🔊 [SpatialAudio] Disabled spatialization (huddle mode) for ${participantId.substring(0, 8)}`);
271
+ }
272
+ // Rest of the chain remains the same
273
+ nodes.analyser.connect(nodes.gain);
274
+ nodes.gain.connect(this.masterGainNode);
275
+ }
276
+ catch (error) {
277
+ console.error(`[SpatialAudio] Error toggling spatialization for ${participantId}:`, error);
278
+ }
279
+ }
228
280
  /**
229
281
  * Update spatial audio position and orientation for a participant
230
282
  *
@@ -248,35 +300,35 @@ class SpatialAudioManager extends EventManager_1.EventManager {
248
300
  updateSpatialAudio(participantId, position, direction) {
249
301
  const nodes = this.participantNodes.get(participantId);
250
302
  if (nodes?.panner) {
251
- const distanceConfig = this.getDistanceConfig();
252
303
  const normalizedPosition = this.normalizePositionUnits(position);
253
- const targetPosition = this.applySpatialBoostIfNeeded(normalizedPosition);
254
- // Update position (where the sound is coming from)
255
- nodes.panner.positionX.setValueAtTime(targetPosition.x, this.audioContext.currentTime);
256
- nodes.panner.positionY.setValueAtTime(targetPosition.y, this.audioContext.currentTime);
257
- nodes.panner.positionZ.setValueAtTime(targetPosition.z, this.audioContext.currentTime);
258
- // Update orientation (where the participant is facing)
259
- // This makes the audio source directional based on participant's direction
260
- if (direction) {
261
- // Normalize direction vector
262
- const length = Math.sqrt(direction.x * direction.x +
263
- direction.y * direction.y +
264
- direction.z * direction.z);
265
- if (length > 0.001) {
266
- const normX = direction.x / length;
267
- const normY = direction.y / length;
268
- const normZ = direction.z / length;
269
- nodes.panner.orientationX.setValueAtTime(normX, this.audioContext.currentTime);
270
- nodes.panner.orientationY.setValueAtTime(normY, this.audioContext.currentTime);
271
- nodes.panner.orientationZ.setValueAtTime(normZ, this.audioContext.currentTime);
272
- }
273
- }
274
304
  const listenerPos = this.listenerPosition;
275
- const vectorToSource = this.getVectorFromListener(targetPosition);
276
- const distance = this.getDistanceBetween(listenerPos, targetPosition);
277
- this.applyDirectionalSuppression(participantId, distance, vectorToSource);
278
- const distanceGain = this.calculateDistanceGain(distanceConfig, distance);
279
- nodes.gain.gain.setTargetAtTime(distanceGain, this.audioContext.currentTime, 0.05);
305
+ // Calculate distance (in meters)
306
+ const distance = this.getDistanceBetween(listenerPos, normalizedPosition);
307
+ // Calculate angle between listener and source
308
+ const angle = this.calculateAngle(listenerPos, normalizedPosition, this.listenerDirection.forward);
309
+ // Calculate stereo panning based on angle
310
+ const panning = this.calculatePanning(angle);
311
+ // Calculate logarithmic gain based on distance
312
+ const gain = this.calculateLogarithmicGain(distance);
313
+ // Apply panning
314
+ this.applyStereoPanning(participantId, panning);
315
+ // Apply gain with smooth transition to reduce clicking/popping
316
+ const gainValue = gain / 100; // Convert to 0-1 range
317
+ nodes.gain.gain.setTargetAtTime(gainValue, this.audioContext.currentTime, 0.05 // Smooth transition over 50ms to reduce clicking
318
+ );
319
+ // Apply proximity gain for additional distance-based attenuation
320
+ nodes.proximityGain.gain.setTargetAtTime(gainValue, this.audioContext.currentTime, 0.05);
321
+ // Update 3D position for PannerNode (still used for vertical positioning)
322
+ nodes.panner.positionY.setValueAtTime(normalizedPosition.y, this.audioContext.currentTime);
323
+ nodes.panner.positionZ.setValueAtTime(normalizedPosition.z, this.audioContext.currentTime);
324
+ // Log for debugging (remove in production)
325
+ if (Math.random() < 0.01) { // Log 1% of updates to avoid spam
326
+ console.log(`[Spatial Audio] Participant: ${participantId}`);
327
+ console.log(` Distance: ${distance.toFixed(2)}m`);
328
+ console.log(` Angle: ${angle.toFixed(1)}°`);
329
+ console.log(` Panning: L${panning.left.toFixed(0)}% R${panning.right.toFixed(0)}%`);
330
+ console.log(` Gain: ${gain.toFixed(0)}%`);
331
+ }
280
332
  }
281
333
  }
282
334
  /**
@@ -322,7 +374,9 @@ class SpatialAudioManager extends EventManager_1.EventManager {
322
374
  const fwdY = forwardY / forwardLen;
323
375
  const fwdZ = forwardZ / forwardLen;
324
376
  // Calculate right vector (cross product of world up and forward)
325
- const worldUp = { x: 0, y: 0, z: 1 }; // Unreal Z-up
377
+ // Web Audio API uses Y-up coordinate system, Unreal uses Z-up
378
+ // We need to transform: Unreal (X,Y,Z) -> WebAudio (X,Z,-Y)
379
+ const worldUp = { x: 0, y: 1, z: 0 }; // Web Audio Y-up
326
380
  const rightX = worldUp.y * fwdZ - worldUp.z * fwdY;
327
381
  const rightY = worldUp.z * fwdX - worldUp.x * fwdZ;
328
382
  const rightZ = worldUp.x * fwdY - worldUp.y * fwdX;
@@ -334,8 +388,8 @@ class SpatialAudioManager extends EventManager_1.EventManager {
334
388
  forwardY: fwdY,
335
389
  forwardZ: fwdZ,
336
390
  upX: 0,
337
- upY: 0,
338
- upZ: 1,
391
+ upY: 1,
392
+ upZ: 0,
339
393
  });
340
394
  return;
341
395
  }
@@ -499,9 +553,45 @@ class SpatialAudioManager extends EventManager_1.EventManager {
499
553
  }
500
554
  const clarityScore = this.calculateClarityScore(distance, vectorToSource);
501
555
  const targetGain = 0.48 + clarityScore * 0.72; // 0.48 → 1.20
502
- const targetLowpass = 3600 + clarityScore * 4600; // 3.6kHz → ~8.2kHz
556
+ // Only adjust gain based on angle, not frequency
503
557
  nodes.proximityGain.gain.setTargetAtTime(targetGain, this.audioContext.currentTime, 0.08);
504
- nodes.dynamicLowpass.frequency.setTargetAtTime(targetLowpass, this.audioContext.currentTime, 0.12);
558
+ // Analyze voice and adjust highpass filter dynamically (85-300Hz)
559
+ this.adjustVoiceAdaptiveFilter(participantId);
560
+ }
561
+ /**
562
+ * Dynamically adjust highpass filter based on voice characteristics
563
+ * Analyzes audio spectrum and sets filter between 85-300Hz
564
+ */
565
+ adjustVoiceAdaptiveFilter(participantId) {
566
+ const nodes = this.participantNodes.get(participantId);
567
+ if (!nodes?.analyser) {
568
+ return;
569
+ }
570
+ const bufferLength = nodes.analyser.frequencyBinCount;
571
+ const dataArray = new Uint8Array(bufferLength);
572
+ nodes.analyser.getByteFrequencyData(dataArray);
573
+ // Calculate spectral centroid in low frequency range (0-500Hz)
574
+ const sampleRate = this.audioContext.sampleRate;
575
+ const nyquist = sampleRate / 2;
576
+ const binWidth = nyquist / bufferLength;
577
+ let weightedSum = 0;
578
+ let totalEnergy = 0;
579
+ const maxBin = Math.floor(500 / binWidth); // Only analyze up to 500Hz
580
+ for (let i = 0; i < Math.min(maxBin, bufferLength); i++) {
581
+ const frequency = i * binWidth;
582
+ const magnitude = dataArray[i] / 255.0;
583
+ weightedSum += frequency * magnitude;
584
+ totalEnergy += magnitude;
585
+ }
586
+ if (totalEnergy > 0.01) {
587
+ const centroid = weightedSum / totalEnergy;
588
+ // Map centroid to highpass frequency (85-300Hz)
589
+ // Lower centroid = deeper voice = use lower highpass (preserve bass)
590
+ // Higher centroid = higher voice = use higher highpass (remove mud)
591
+ const targetFreq = Math.max(85, Math.min(300, 85 + (centroid - 100) * 0.5));
592
+ nodes.highpassFilter.frequency.setTargetAtTime(targetFreq, this.audioContext.currentTime, 0.15 // Smooth transition
593
+ );
594
+ }
505
595
  }
506
596
  calculateClarityScore(distance, vectorToSource) {
507
597
  const proximityWeight = this.calculateProximityWeight(distance);
@@ -545,6 +635,132 @@ class SpatialAudioManager extends EventManager_1.EventManager {
545
635
  isDenoiserEnabled() {
546
636
  return this.options.denoiser?.enabled !== false;
547
637
  }
638
+ /**
639
+ * Calculate angle between listener and sound source in degrees (0-360)
640
+ * 0° = front, 90° = right, 180° = back, 270° = left
641
+ */
642
+ calculateAngle(listenerPos, sourcePos, listenerForward) {
643
+ // Vector from listener to source
644
+ const dx = sourcePos.x - listenerPos.x;
645
+ const dy = sourcePos.y - listenerPos.y;
646
+ // Project onto horizontal plane (assuming Z is up)
647
+ // Use listener's forward direction to determine angle
648
+ const forwardX = listenerForward.x;
649
+ const forwardY = listenerForward.y;
650
+ // Calculate angle using atan2
651
+ const angleToSource = Math.atan2(dy, dx);
652
+ const forwardAngle = Math.atan2(forwardY, forwardX);
653
+ // Relative angle in radians
654
+ let relativeAngle = angleToSource - forwardAngle;
655
+ // Normalize to 0-2π
656
+ while (relativeAngle < 0)
657
+ relativeAngle += Math.PI * 2;
658
+ while (relativeAngle >= Math.PI * 2)
659
+ relativeAngle -= Math.PI * 2;
660
+ // Convert to degrees (0-360)
661
+ return (relativeAngle * 180 / Math.PI);
662
+ }
663
+ /**
664
+ * Calculate stereo panning based on angle (0-360°)
665
+ * Returns { left: 0-100, right: 0-100 }
666
+ *
667
+ * Reference angles:
668
+ * 0° (front): L100, R100
669
+ * 45° (front-right): L50, R100
670
+ * 90° (right): L0, R100
671
+ * 135° (back-right): L0, R50
672
+ * 180° (back): L50, R50
673
+ * 225° (back-left): L50, R0
674
+ * 270° (left): L100, R0
675
+ * 315° (front-left): L100, R50
676
+ */
677
+ calculatePanning(angle) {
678
+ // Normalize angle to 0-360
679
+ while (angle < 0)
680
+ angle += 360;
681
+ while (angle >= 360)
682
+ angle -= 360;
683
+ let left = 100;
684
+ let right = 100;
685
+ if (angle <= 90) {
686
+ // Front-right quadrant (0° to 90°)
687
+ // Left decreases from 100 to 0
688
+ // Right stays at 100
689
+ left = 100 * (1 - angle / 90);
690
+ right = 100;
691
+ }
692
+ else if (angle <= 180) {
693
+ // Back-right quadrant (90° to 180°)
694
+ // Left stays at 0
695
+ // Right decreases from 100 to 50
696
+ left = 0;
697
+ right = 100 - 50 * ((angle - 90) / 90);
698
+ }
699
+ else if (angle <= 270) {
700
+ // Back-left quadrant (180° to 270°)
701
+ // Left increases from 0 to 100
702
+ // Right decreases from 50 to 0
703
+ const progress = (angle - 180) / 90;
704
+ left = 50 + 50 * progress;
705
+ right = 50 * (1 - progress);
706
+ }
707
+ else {
708
+ // Front-left quadrant (270° to 360°)
709
+ // Left stays at 100
710
+ // Right increases from 0 to 100
711
+ left = 100;
712
+ right = 100 * ((angle - 270) / 90);
713
+ }
714
+ return {
715
+ left: Math.max(0, Math.min(100, left)),
716
+ right: Math.max(0, Math.min(100, right))
717
+ };
718
+ }
719
+ /**
720
+ * Calculate gain based on distance using logarithmic scale
721
+ * Distance range: 0.5m to 5m
722
+ * Gain range: 100% to 0%
723
+ * Uses quadratic equation for human ear perception
724
+ */
725
+ calculateLogarithmicGain(distance) {
726
+ const minDistance = 0.5; // meters
727
+ const maxDistance = 5.0; // meters
728
+ // Clamp distance
729
+ if (distance <= minDistance)
730
+ return 100;
731
+ if (distance >= maxDistance)
732
+ return 0;
733
+ // Normalize distance to 0-1 range
734
+ const normalizedDistance = (distance - minDistance) / (maxDistance - minDistance);
735
+ // Apply quadratic falloff for natural perception
736
+ // gain = 100 * (1 - x²)
737
+ // This creates a logarithmic-like curve that sounds linear to human ear
738
+ const gain = 100 * Math.pow(1 - normalizedDistance, 2);
739
+ return Math.max(0, Math.min(100, gain));
740
+ }
741
+ /**
742
+ * Apply stereo panning to participant audio
743
+ * Converts panning percentages to StereoPanner values
744
+ */
745
+ applyStereoPanning(participantId, panning) {
746
+ const nodes = this.participantNodes.get(participantId);
747
+ if (!nodes?.panner)
748
+ return;
749
+ // Convert left/right percentages to pan value (-1 to +1)
750
+ // If left=100, right=0 → pan = -1 (full left)
751
+ // If left=0, right=100 → pan = +1 (full right)
752
+ // If left=100, right=100 → pan = 0 (center)
753
+ const leftRatio = panning.left / 100;
754
+ const rightRatio = panning.right / 100;
755
+ // Calculate pan position
756
+ let panValue = 0;
757
+ if (leftRatio + rightRatio > 0) {
758
+ panValue = (rightRatio - leftRatio);
759
+ }
760
+ // Adjust X position for left-right panning (-1 = left, +1 = right)
761
+ const currentTime = this.audioContext.currentTime;
762
+ nodes.panner.positionX.setTargetAtTime(panValue * 5, currentTime, 0.05);
763
+ }
548
764
  async ensureDenoiseWorklet() {
549
765
  if (!this.isDenoiserEnabled()) {
550
766
  return;
package/dist/index.js CHANGED
@@ -383,11 +383,16 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
383
383
  return; // Exit early to prevent any audio processing
384
384
  }
385
385
  else {
386
- // Setup spatial audio with full 3D positioning
387
- await this.spatialAudioManager.setupSpatialAudioForParticipant(participant.participantId, track, false // Always enable spatial audio
386
+ // Check if participant is in a huddle (non-spatial channel)
387
+ const participantChannel = participant.currentChannel || "spatial";
388
+ const isInHuddle = participantChannel !== "spatial";
389
+ // Setup spatial audio - bypass 3D positioning for huddle members
390
+ await this.spatialAudioManager.setupSpatialAudioForParticipant(participant.participantId, track, isInHuddle // Bypass spatialization if in huddle
388
391
  );
389
- // Update spatial audio position
390
- this.spatialAudioManager.updateSpatialAudio(participant.participantId, data.position);
392
+ // Only update spatial position if in spatial channel
393
+ if (!isInHuddle) {
394
+ this.spatialAudioManager.updateSpatialAudio(participant.participantId, data.position);
395
+ }
391
396
  }
392
397
  // NOW resume the consumer after audio pipeline is ready
393
398
  this.mediasoupManager
@@ -496,6 +501,9 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
496
501
  const participant = this.room?.participants.get(data.participantId);
497
502
  if (participant) {
498
503
  participant.currentChannel = data.channelId;
504
+ // Update spatialization based on channel
505
+ const isInSpatialChannel = data.channelId === "spatial";
506
+ this.spatialAudioManager.setParticipantSpatialization(data.participantId, isInSpatialChannel);
499
507
  // If this participant is now in a different channel from us, clear their screenshare
500
508
  const myChannel = this.localParticipant?.currentChannel || "spatial";
501
509
  const theirChannel = data.channelId || "spatial";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.60",
3
+ "version": "1.0.62",
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",