@tensamin/audio 0.1.1 → 0.1.3

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 (57) hide show
  1. package/README.md +50 -3
  2. package/dist/chunk-6P2RDBW5.mjs +47 -0
  3. package/dist/chunk-EXH2PNUE.mjs +212 -0
  4. package/{src/vad/vad-state.ts → dist/chunk-JJASCVEW.mjs} +21 -33
  5. package/dist/chunk-OZ7KMC4S.mjs +46 -0
  6. package/dist/chunk-R5JVHKWA.mjs +98 -0
  7. package/dist/chunk-WBQAMGXK.mjs +0 -0
  8. package/dist/chunk-XMTQPMQ6.mjs +91 -0
  9. package/dist/chunk-XO6B3D4A.mjs +67 -0
  10. package/dist/context/audio-context.d.mts +32 -0
  11. package/dist/context/audio-context.d.ts +32 -0
  12. package/dist/context/audio-context.js +75 -0
  13. package/dist/context/audio-context.mjs +16 -0
  14. package/dist/extensibility/plugins.d.mts +9 -0
  15. package/dist/extensibility/plugins.d.ts +9 -0
  16. package/dist/extensibility/plugins.js +238 -0
  17. package/dist/extensibility/plugins.mjs +14 -0
  18. package/dist/index.d.mts +10 -216
  19. package/dist/index.d.ts +10 -216
  20. package/dist/index.js +298 -80
  21. package/dist/index.mjs +29 -352
  22. package/dist/livekit/integration.d.mts +11 -0
  23. package/dist/livekit/integration.d.ts +11 -0
  24. package/dist/livekit/integration.js +585 -0
  25. package/dist/livekit/integration.mjs +12 -0
  26. package/dist/noise-suppression/rnnoise-node.d.mts +10 -0
  27. package/dist/noise-suppression/rnnoise-node.d.ts +10 -0
  28. package/dist/noise-suppression/rnnoise-node.js +101 -0
  29. package/dist/noise-suppression/rnnoise-node.mjs +6 -0
  30. package/dist/pipeline/audio-pipeline.d.mts +6 -0
  31. package/dist/pipeline/audio-pipeline.d.ts +6 -0
  32. package/dist/pipeline/audio-pipeline.js +499 -0
  33. package/dist/pipeline/audio-pipeline.mjs +11 -0
  34. package/dist/types.d.mts +155 -0
  35. package/dist/types.d.ts +155 -0
  36. package/dist/types.js +18 -0
  37. package/dist/types.mjs +1 -0
  38. package/dist/vad/vad-node.d.mts +9 -0
  39. package/dist/vad/vad-node.d.ts +9 -0
  40. package/dist/vad/vad-node.js +122 -0
  41. package/dist/vad/vad-node.mjs +6 -0
  42. package/dist/vad/vad-state.d.mts +15 -0
  43. package/dist/vad/vad-state.d.ts +15 -0
  44. package/dist/vad/vad-state.js +83 -0
  45. package/dist/vad/vad-state.mjs +6 -0
  46. package/package.json +8 -5
  47. package/.github/workflows/publish.yml +0 -29
  48. package/bun.lock +0 -258
  49. package/src/context/audio-context.ts +0 -69
  50. package/src/extensibility/plugins.ts +0 -45
  51. package/src/index.ts +0 -8
  52. package/src/livekit/integration.ts +0 -61
  53. package/src/noise-suppression/rnnoise-node.ts +0 -62
  54. package/src/pipeline/audio-pipeline.ts +0 -154
  55. package/src/types.ts +0 -167
  56. package/src/vad/vad-node.ts +0 -78
  57. package/tsconfig.json +0 -46
package/dist/index.js CHANGED
@@ -90,37 +90,66 @@ async function closeAudioContext() {
90
90
  var import_mitt = __toESM(require("mitt"));
91
91
 
92
92
  // src/noise-suppression/rnnoise-node.ts
93
- var import_web_noise_suppressor = require("@sapphi-red/web-noise-suppressor");
94
- var DEFAULT_WASM_URL = "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/rnnoise.wasm";
95
- var DEFAULT_SIMD_WASM_URL = "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/rnnoise_simd.wasm";
96
- var DEFAULT_WORKLET_URL = "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/noise-suppressor-worklet.min.js";
97
93
  var RNNoisePlugin = class {
98
94
  name = "rnnoise-ns";
99
95
  wasmBuffer = null;
100
96
  async createNode(context, config) {
97
+ const { loadRnnoise, RnnoiseWorkletNode } = await import("@sapphi-red/web-noise-suppressor");
101
98
  if (!config?.enabled) {
99
+ console.log("Noise suppression disabled, using passthrough node");
102
100
  const pass = context.createGain();
103
101
  return pass;
104
102
  }
105
- if (!this.wasmBuffer) {
106
- this.wasmBuffer = await (0, import_web_noise_suppressor.loadRnnoise)({
107
- url: config.wasmUrl || DEFAULT_WASM_URL,
108
- simdUrl: DEFAULT_SIMD_WASM_URL
109
- // We should probably allow config for this too, but for now default is fine.
110
- });
103
+ if (!config?.wasmUrl || !config?.simdUrl || !config?.workletUrl) {
104
+ const error = new Error(
105
+ `RNNoisePlugin requires 'wasmUrl', 'simdUrl', and 'workletUrl' to be configured. Please download the assets from @sapphi-red/web-noise-suppressor and provide the URLs in the config. Current config: wasmUrl=${config?.wasmUrl}, simdUrl=${config?.simdUrl}, workletUrl=${config?.workletUrl}
106
+ To disable noise suppression, set noiseSuppression.enabled to false.`
107
+ );
108
+ console.error(error.message);
109
+ throw error;
110
+ }
111
+ try {
112
+ if (!this.wasmBuffer) {
113
+ console.log("Loading RNNoise WASM binary...");
114
+ this.wasmBuffer = await loadRnnoise({
115
+ url: config.wasmUrl,
116
+ simdUrl: config.simdUrl
117
+ });
118
+ console.log("RNNoise WASM loaded successfully");
119
+ }
120
+ } catch (error) {
121
+ const err = new Error(
122
+ `Failed to load RNNoise WASM binary: ${error instanceof Error ? error.message : String(error)}`
123
+ );
124
+ console.error(err);
125
+ throw err;
111
126
  }
112
- const workletUrl = config.workletUrl || DEFAULT_WORKLET_URL;
127
+ const workletUrl = config.workletUrl;
113
128
  try {
114
129
  await context.audioWorklet.addModule(workletUrl);
130
+ console.log("RNNoise worklet loaded successfully");
115
131
  } catch (e) {
116
- console.warn("Failed to add RNNoise worklet module:", e);
132
+ const error = new Error(
133
+ `Failed to load RNNoise worklet from ${workletUrl}: ${e instanceof Error ? e.message : String(e)}. Ensure the workletUrl points to a valid RNNoise worklet script.`
134
+ );
135
+ console.error(error.message);
136
+ throw error;
137
+ }
138
+ try {
139
+ const node = new RnnoiseWorkletNode(context, {
140
+ wasmBinary: this.wasmBuffer,
141
+ maxChannels: 1
142
+ // Mono for now
143
+ });
144
+ console.log("RNNoise worklet node created successfully");
145
+ return node;
146
+ } catch (error) {
147
+ const err = new Error(
148
+ `Failed to create RNNoise worklet node: ${error instanceof Error ? error.message : String(error)}`
149
+ );
150
+ console.error(err);
151
+ throw err;
117
152
  }
118
- const node = new import_web_noise_suppressor.RnnoiseWorkletNode(context, {
119
- wasmBinary: this.wasmBuffer,
120
- maxChannels: 1
121
- // Mono for now
122
- });
123
- return node;
124
153
  }
125
154
  };
126
155
 
@@ -168,22 +197,52 @@ registerProcessor('energy-vad-processor', EnergyVadProcessor);
168
197
  var EnergyVADPlugin = class {
169
198
  name = "energy-vad";
170
199
  async createNode(context, config, onDecision) {
200
+ if (!config?.enabled) {
201
+ console.log("VAD disabled, using passthrough node");
202
+ const pass = context.createGain();
203
+ return pass;
204
+ }
171
205
  const blob = new Blob([energyVadWorkletCode], {
172
206
  type: "application/javascript"
173
207
  });
174
208
  const url = URL.createObjectURL(blob);
175
209
  try {
176
210
  await context.audioWorklet.addModule(url);
211
+ console.log("Energy VAD worklet loaded successfully");
177
212
  } catch (e) {
178
- console.warn("Failed to add Energy VAD worklet:", e);
179
- throw e;
180
- } finally {
213
+ const error = new Error(
214
+ `Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
215
+ );
216
+ console.error(error.message);
181
217
  URL.revokeObjectURL(url);
218
+ throw error;
219
+ }
220
+ URL.revokeObjectURL(url);
221
+ let node;
222
+ try {
223
+ node = new AudioWorkletNode(context, "energy-vad-processor");
224
+ console.log("Energy VAD node created successfully");
225
+ } catch (e) {
226
+ const error = new Error(
227
+ `Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
228
+ );
229
+ console.error(error.message);
230
+ throw error;
182
231
  }
183
- const node = new AudioWorkletNode(context, "energy-vad-processor");
184
232
  node.port.onmessage = (event) => {
185
- const { probability } = event.data;
186
- onDecision(probability);
233
+ try {
234
+ const { probability } = event.data;
235
+ if (typeof probability === "number" && !isNaN(probability)) {
236
+ onDecision(probability);
237
+ } else {
238
+ console.warn("Invalid VAD probability received:", event.data);
239
+ }
240
+ } catch (error) {
241
+ console.error("Error in VAD message handler:", error);
242
+ }
243
+ };
244
+ node.port.onmessageerror = (event) => {
245
+ console.error("VAD port message error:", event);
187
246
  };
188
247
  return node;
189
248
  }
@@ -283,42 +342,84 @@ var VADStateMachine = class {
283
342
  async function createAudioPipeline(sourceTrack, config = {}) {
284
343
  const context = getAudioContext();
285
344
  registerPipeline();
345
+ const nsEnabled = config.noiseSuppression?.enabled !== false && Boolean(config.noiseSuppression?.wasmUrl && config.noiseSuppression?.simdUrl && config.noiseSuppression?.workletUrl);
346
+ const vadEnabled = config.vad?.enabled !== false;
286
347
  const fullConfig = {
287
- noiseSuppression: { enabled: true, ...config.noiseSuppression },
288
- vad: { enabled: true, ...config.vad },
348
+ noiseSuppression: {
349
+ enabled: nsEnabled,
350
+ ...config.noiseSuppression
351
+ },
352
+ vad: {
353
+ enabled: vadEnabled,
354
+ ...config.vad
355
+ },
289
356
  output: {
290
357
  speechGain: 1,
291
- silenceGain: 0,
358
+ silenceGain: vadEnabled ? 0 : 1,
359
+ // If no VAD, always output audio
292
360
  gainRampTime: 0.02,
293
361
  ...config.output
294
362
  },
295
363
  livekit: { manageTrackMute: false, ...config.livekit }
296
364
  };
365
+ console.log("Audio pipeline config:", {
366
+ noiseSuppression: fullConfig.noiseSuppression?.enabled,
367
+ vad: fullConfig.vad?.enabled,
368
+ output: fullConfig.output
369
+ });
370
+ if (!sourceTrack || sourceTrack.kind !== "audio") {
371
+ throw new Error("createAudioPipeline requires a valid audio MediaStreamTrack");
372
+ }
373
+ if (sourceTrack.readyState === "ended") {
374
+ throw new Error("Cannot create pipeline from an ended MediaStreamTrack");
375
+ }
297
376
  const sourceStream = new MediaStream([sourceTrack]);
298
377
  const sourceNode = context.createMediaStreamSource(sourceStream);
299
- const nsPlugin = getNoiseSuppressionPlugin(
300
- fullConfig.noiseSuppression?.pluginName
301
- );
302
- const nsNode = await nsPlugin.createNode(
303
- context,
304
- fullConfig.noiseSuppression
305
- );
306
- const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
307
- const vadStateMachine = new VADStateMachine(fullConfig.vad);
378
+ let nsNode;
379
+ let vadNode;
308
380
  const emitter = (0, import_mitt.default)();
309
- const vadNode = await vadPlugin.createNode(
310
- context,
311
- fullConfig.vad,
312
- (prob) => {
313
- const timestamp = context.currentTime * 1e3;
314
- const newState = vadStateMachine.processFrame(prob, timestamp);
315
- if (newState.state !== lastVadState.state || Math.abs(newState.probability - lastVadState.probability) > 0.1) {
316
- emitter.emit("vadChange", newState);
317
- lastVadState = newState;
318
- updateGain(newState);
381
+ try {
382
+ const nsPlugin = getNoiseSuppressionPlugin(
383
+ fullConfig.noiseSuppression?.pluginName
384
+ );
385
+ nsNode = await nsPlugin.createNode(
386
+ context,
387
+ fullConfig.noiseSuppression
388
+ );
389
+ } catch (error) {
390
+ const err = error instanceof Error ? error : new Error(String(error));
391
+ console.error("Failed to create noise suppression node:", err);
392
+ emitter.emit("error", err);
393
+ throw err;
394
+ }
395
+ const vadStateMachine = new VADStateMachine(fullConfig.vad);
396
+ try {
397
+ const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
398
+ vadNode = await vadPlugin.createNode(
399
+ context,
400
+ fullConfig.vad,
401
+ (prob) => {
402
+ try {
403
+ const timestamp = context.currentTime * 1e3;
404
+ const newState = vadStateMachine.processFrame(prob, timestamp);
405
+ if (newState.state !== lastVadState.state || Math.abs(newState.probability - lastVadState.probability) > 0.1) {
406
+ emitter.emit("vadChange", newState);
407
+ lastVadState = newState;
408
+ updateGain(newState);
409
+ }
410
+ } catch (vadError) {
411
+ const err = vadError instanceof Error ? vadError : new Error(String(vadError));
412
+ console.error("Error in VAD callback:", err);
413
+ emitter.emit("error", err);
414
+ }
319
415
  }
320
- }
321
- );
416
+ );
417
+ } catch (error) {
418
+ const err = error instanceof Error ? error : new Error(String(error));
419
+ console.error("Failed to create VAD node:", err);
420
+ emitter.emit("error", err);
421
+ throw err;
422
+ }
322
423
  let lastVadState = {
323
424
  isSpeaking: false,
324
425
  probability: 0,
@@ -334,34 +435,98 @@ async function createAudioPipeline(sourceTrack, config = {}) {
334
435
  const gainNode = context.createGain();
335
436
  gainNode.gain.value = fullConfig.output?.silenceGain ?? 0;
336
437
  const destination = context.createMediaStreamDestination();
337
- splitter.connect(delayNode);
338
- delayNode.connect(gainNode);
339
- gainNode.connect(destination);
438
+ try {
439
+ splitter.connect(delayNode);
440
+ delayNode.connect(gainNode);
441
+ gainNode.connect(destination);
442
+ } catch (error) {
443
+ const err = error instanceof Error ? error : new Error(String(error));
444
+ console.error("Failed to wire audio pipeline:", err);
445
+ emitter.emit("error", err);
446
+ throw err;
447
+ }
340
448
  function updateGain(state) {
341
- const { speechGain, silenceGain, gainRampTime } = fullConfig.output;
342
- const targetGain = state.isSpeaking ? speechGain ?? 1 : silenceGain ?? 0;
343
- const now = context.currentTime;
344
- gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
449
+ try {
450
+ const { speechGain, silenceGain, gainRampTime } = fullConfig.output;
451
+ const targetGain = state.isSpeaking ? speechGain ?? 1 : silenceGain ?? 0;
452
+ const now = context.currentTime;
453
+ gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
454
+ } catch (error) {
455
+ const err = error instanceof Error ? error : new Error(String(error));
456
+ console.error("Failed to update gain:", err);
457
+ emitter.emit("error", err);
458
+ }
459
+ }
460
+ const audioTracks = destination.stream.getAudioTracks();
461
+ console.log("Destination stream tracks:", {
462
+ count: audioTracks.length,
463
+ tracks: audioTracks.map((t) => ({
464
+ id: t.id,
465
+ label: t.label,
466
+ enabled: t.enabled,
467
+ readyState: t.readyState
468
+ }))
469
+ });
470
+ if (audioTracks.length === 0) {
471
+ const err = new Error(
472
+ "Failed to create processed audio track: destination stream has no audio tracks. This may indicate an issue with the audio graph connection."
473
+ );
474
+ console.error(err);
475
+ emitter.emit("error", err);
476
+ throw err;
345
477
  }
478
+ const processedTrack = audioTracks[0];
479
+ if (!processedTrack || processedTrack.readyState === "ended") {
480
+ const err = new Error("Processed audio track is invalid or ended");
481
+ console.error(err);
482
+ emitter.emit("error", err);
483
+ throw err;
484
+ }
485
+ console.log("Audio pipeline created successfully:", {
486
+ sourceTrack: {
487
+ id: sourceTrack.id,
488
+ label: sourceTrack.label,
489
+ readyState: sourceTrack.readyState
490
+ },
491
+ processedTrack: {
492
+ id: processedTrack.id,
493
+ label: processedTrack.label,
494
+ readyState: processedTrack.readyState
495
+ },
496
+ config: {
497
+ noiseSuppression: fullConfig.noiseSuppression?.enabled,
498
+ vad: fullConfig.vad?.enabled
499
+ }
500
+ });
346
501
  function dispose() {
347
- sourceNode.disconnect();
348
- nsNode.disconnect();
349
- splitter.disconnect();
350
- vadNode.disconnect();
351
- delayNode.disconnect();
352
- gainNode.disconnect();
353
- destination.stream.getTracks().forEach((t) => t.stop());
354
- unregisterPipeline();
502
+ try {
503
+ sourceNode.disconnect();
504
+ nsNode.disconnect();
505
+ splitter.disconnect();
506
+ vadNode.disconnect();
507
+ delayNode.disconnect();
508
+ gainNode.disconnect();
509
+ destination.stream.getTracks().forEach((t) => t.stop());
510
+ unregisterPipeline();
511
+ } catch (error) {
512
+ console.error("Error during pipeline disposal:", error);
513
+ }
355
514
  }
356
515
  return {
357
- processedTrack: destination.stream.getAudioTracks()[0],
516
+ processedTrack,
358
517
  events: emitter,
359
518
  get state() {
360
519
  return lastVadState;
361
520
  },
362
521
  setConfig: (newConfig) => {
363
- if (newConfig.vad) {
364
- vadStateMachine.updateConfig(newConfig.vad);
522
+ try {
523
+ if (newConfig.vad) {
524
+ vadStateMachine.updateConfig(newConfig.vad);
525
+ }
526
+ } catch (error) {
527
+ const err = error instanceof Error ? error : new Error(String(error));
528
+ console.error("Failed to update config:", err);
529
+ emitter.emit("error", err);
365
530
  }
366
531
  },
367
532
  dispose
@@ -370,31 +535,84 @@ async function createAudioPipeline(sourceTrack, config = {}) {
370
535
 
371
536
  // src/livekit/integration.ts
372
537
  async function attachProcessingToTrack(track, config = {}) {
538
+ if (!track) {
539
+ throw new Error("attachProcessingToTrack requires a valid LocalAudioTrack");
540
+ }
373
541
  const originalTrack = track.mediaStreamTrack;
374
- const pipeline = await createAudioPipeline(originalTrack, config);
375
- await track.replaceTrack(pipeline.processedTrack);
542
+ if (!originalTrack) {
543
+ throw new Error("LocalAudioTrack has no underlying MediaStreamTrack");
544
+ }
545
+ if (originalTrack.readyState === "ended") {
546
+ throw new Error("Cannot attach processing to an ended MediaStreamTrack");
547
+ }
548
+ let pipeline;
549
+ try {
550
+ console.log("Creating audio processing pipeline...");
551
+ pipeline = await createAudioPipeline(originalTrack, config);
552
+ console.log("Audio processing pipeline created successfully");
553
+ } catch (error) {
554
+ const err = new Error(
555
+ `Failed to create audio pipeline: ${error instanceof Error ? error.message : String(error)}`
556
+ );
557
+ console.error(err);
558
+ throw err;
559
+ }
560
+ if (!pipeline.processedTrack) {
561
+ throw new Error("Pipeline did not return a processed track");
562
+ }
563
+ try {
564
+ console.log("Replacing LiveKit track with processed track...");
565
+ await track.replaceTrack(pipeline.processedTrack);
566
+ console.log("LiveKit track replaced successfully");
567
+ } catch (error) {
568
+ pipeline.dispose();
569
+ const err = new Error(
570
+ `Failed to replace LiveKit track: ${error instanceof Error ? error.message : String(error)}`
571
+ );
572
+ console.error(err);
573
+ throw err;
574
+ }
376
575
  if (config.livekit?.manageTrackMute) {
377
576
  let isVadMuted = false;
378
577
  pipeline.events.on("vadChange", async (state) => {
379
- if (state.isSpeaking) {
380
- if (isVadMuted) {
381
- await track.unmute();
382
- isVadMuted = false;
383
- }
384
- } else {
385
- if (!track.isMuted) {
386
- await track.mute();
387
- isVadMuted = true;
578
+ try {
579
+ if (state.isSpeaking) {
580
+ if (isVadMuted) {
581
+ await track.unmute();
582
+ isVadMuted = false;
583
+ }
584
+ } else {
585
+ if (!track.isMuted) {
586
+ await track.mute();
587
+ isVadMuted = true;
588
+ }
388
589
  }
590
+ } catch (error) {
591
+ console.error("Error handling VAD-based track muting:", error);
389
592
  }
390
593
  });
391
594
  }
595
+ pipeline.events.on("error", (error) => {
596
+ console.error("Audio pipeline error:", error);
597
+ });
392
598
  const originalDispose = pipeline.dispose;
393
599
  pipeline.dispose = () => {
394
- if (originalTrack.readyState === "live") {
395
- track.replaceTrack(originalTrack).catch(console.error);
600
+ try {
601
+ if (originalTrack.readyState === "live") {
602
+ console.log("Restoring original track...");
603
+ track.replaceTrack(originalTrack).catch((error) => {
604
+ console.error("Failed to restore original track:", error);
605
+ });
606
+ }
607
+ originalDispose();
608
+ } catch (error) {
609
+ console.error("Error during pipeline disposal:", error);
610
+ try {
611
+ originalDispose();
612
+ } catch (disposeError) {
613
+ console.error("Error calling original dispose:", disposeError);
614
+ }
396
615
  }
397
- originalDispose();
398
616
  };
399
617
  return pipeline;
400
618
  }