@tensamin/audio 0.1.2 → 0.1.4

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.
@@ -0,0 +1,93 @@
1
+ // src/vad/vad-state.ts
2
+ var VADStateMachine = class {
3
+ config;
4
+ currentState = "silent";
5
+ lastSpeechTime = 0;
6
+ speechStartTime = 0;
7
+ lastSilenceTime = 0;
8
+ frameDurationMs = 20;
9
+ // Assumed frame duration, updated by calls
10
+ constructor(config) {
11
+ this.config = {
12
+ enabled: config?.enabled ?? true,
13
+ pluginName: config?.pluginName ?? "energy-vad",
14
+ // Voice-optimized defaults
15
+ startThreshold: config?.startThreshold ?? 0.6,
16
+ // Higher threshold to avoid noise
17
+ stopThreshold: config?.stopThreshold ?? 0.45,
18
+ // Balanced for voice
19
+ hangoverMs: config?.hangoverMs ?? 400,
20
+ // Smooth for natural speech
21
+ preRollMs: config?.preRollMs ?? 250,
22
+ // Generous pre-roll
23
+ minSpeechDurationMs: config?.minSpeechDurationMs ?? 100,
24
+ minSilenceDurationMs: config?.minSilenceDurationMs ?? 150,
25
+ energyVad: {
26
+ smoothing: config?.energyVad?.smoothing ?? 0.95,
27
+ initialNoiseFloor: config?.energyVad?.initialNoiseFloor ?? 1e-3,
28
+ noiseFloorAdaptRateQuiet: config?.energyVad?.noiseFloorAdaptRateQuiet ?? 0.01,
29
+ noiseFloorAdaptRateLoud: config?.energyVad?.noiseFloorAdaptRateLoud ?? 1e-3,
30
+ minSNR: config?.energyVad?.minSNR ?? 2,
31
+ snrRange: config?.energyVad?.snrRange ?? 8
32
+ }
33
+ };
34
+ this.lastSilenceTime = Date.now();
35
+ }
36
+ updateConfig(config) {
37
+ this.config = { ...this.config, ...config };
38
+ }
39
+ processFrame(probability, timestamp) {
40
+ const {
41
+ startThreshold,
42
+ stopThreshold,
43
+ hangoverMs,
44
+ minSpeechDurationMs,
45
+ minSilenceDurationMs
46
+ } = this.config;
47
+ let newState = this.currentState;
48
+ if (this.currentState === "silent" || this.currentState === "speech_ending") {
49
+ if (probability >= startThreshold) {
50
+ const silenceDuration = timestamp - this.lastSilenceTime;
51
+ if (silenceDuration >= minSilenceDurationMs) {
52
+ newState = "speech_starting";
53
+ this.speechStartTime = timestamp;
54
+ this.lastSpeechTime = timestamp;
55
+ } else {
56
+ newState = "silent";
57
+ }
58
+ } else {
59
+ newState = "silent";
60
+ this.lastSilenceTime = timestamp;
61
+ }
62
+ } else if (this.currentState === "speech_starting" || this.currentState === "speaking") {
63
+ if (probability >= stopThreshold) {
64
+ newState = "speaking";
65
+ this.lastSpeechTime = timestamp;
66
+ } else {
67
+ const timeSinceSpeech = timestamp - this.lastSpeechTime;
68
+ const speechDuration = timestamp - this.speechStartTime;
69
+ if (timeSinceSpeech < hangoverMs) {
70
+ newState = "speaking";
71
+ } else if (speechDuration < minSpeechDurationMs) {
72
+ newState = "silent";
73
+ this.lastSilenceTime = timestamp;
74
+ } else {
75
+ newState = "speech_ending";
76
+ this.lastSilenceTime = timestamp;
77
+ }
78
+ }
79
+ }
80
+ if (newState === "speech_starting") newState = "speaking";
81
+ if (newState === "speech_ending") newState = "silent";
82
+ this.currentState = newState;
83
+ return {
84
+ isSpeaking: newState === "speaking",
85
+ probability,
86
+ state: newState
87
+ };
88
+ }
89
+ };
90
+
91
+ export {
92
+ VADStateMachine
93
+ };
@@ -0,0 +1,118 @@
1
+ // src/vad/vad-node.ts
2
+ var createEnergyVadWorkletCode = (vadConfig) => {
3
+ const energyParams = vadConfig?.energyVad || {};
4
+ const smoothing = energyParams.smoothing ?? 0.95;
5
+ const initialNoiseFloor = energyParams.initialNoiseFloor ?? 1e-3;
6
+ const noiseFloorAdaptRateQuiet = energyParams.noiseFloorAdaptRateQuiet ?? 0.01;
7
+ const noiseFloorAdaptRateLoud = energyParams.noiseFloorAdaptRateLoud ?? 1e-3;
8
+ const minSNR = energyParams.minSNR ?? 2;
9
+ const snrRange = energyParams.snrRange ?? 8;
10
+ return `
11
+ class EnergyVadProcessor extends AudioWorkletProcessor {
12
+ constructor() {
13
+ super();
14
+ this.smoothing = ${smoothing};
15
+ this.energy = 0;
16
+ this.noiseFloor = ${initialNoiseFloor};
17
+ this.noiseFloorAdaptRateQuiet = ${noiseFloorAdaptRateQuiet};
18
+ this.noiseFloorAdaptRateLoud = ${noiseFloorAdaptRateLoud};
19
+ this.minSNR = ${minSNR};
20
+ this.snrRange = ${snrRange};
21
+ }
22
+
23
+ process(inputs, outputs, parameters) {
24
+ const input = inputs[0];
25
+ if (!input || !input.length) return true;
26
+ const channel = input[0];
27
+
28
+ // Calculate RMS (Root Mean Square) energy
29
+ let sum = 0;
30
+ for (let i = 0; i < channel.length; i++) {
31
+ sum += channel[i] * channel[i];
32
+ }
33
+ const rms = Math.sqrt(sum / channel.length);
34
+
35
+ // Adaptive noise floor estimation
36
+ // When signal is quiet, adapt quickly to find new noise floor
37
+ // When signal is loud (speech), adapt slowly to avoid raising noise floor
38
+ if (rms < this.noiseFloor) {
39
+ this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateQuiet) + rms * this.noiseFloorAdaptRateQuiet;
40
+ } else {
41
+ this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateLoud) + rms * this.noiseFloorAdaptRateLoud;
42
+ }
43
+
44
+ // Calculate Signal-to-Noise Ratio (SNR)
45
+ const snr = rms / (this.noiseFloor + 1e-6);
46
+
47
+ // Map SNR to probability (0-1)
48
+ // Probability is 0 when SNR <= minSNR
49
+ // Probability scales linearly from 0 to 1 between minSNR and (minSNR + snrRange)
50
+ // Probability is 1 when SNR >= (minSNR + snrRange)
51
+ const probability = Math.min(1, Math.max(0, (snr - this.minSNR) / this.snrRange));
52
+
53
+ this.port.postMessage({ probability, snr, noiseFloor: this.noiseFloor, rms });
54
+
55
+ return true;
56
+ }
57
+ }
58
+ registerProcessor('energy-vad-processor', EnergyVadProcessor);
59
+ `;
60
+ };
61
+ var EnergyVADPlugin = class {
62
+ name = "energy-vad";
63
+ async createNode(context, config, onDecision) {
64
+ if (!config?.enabled) {
65
+ console.log("VAD disabled, using passthrough node");
66
+ const pass = context.createGain();
67
+ return pass;
68
+ }
69
+ const workletCode = createEnergyVadWorkletCode(config);
70
+ const blob = new Blob([workletCode], {
71
+ type: "application/javascript"
72
+ });
73
+ const url = URL.createObjectURL(blob);
74
+ try {
75
+ await context.audioWorklet.addModule(url);
76
+ console.log("Energy VAD worklet loaded successfully");
77
+ } catch (e) {
78
+ const error = new Error(
79
+ `Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
80
+ );
81
+ console.error(error.message);
82
+ URL.revokeObjectURL(url);
83
+ throw error;
84
+ }
85
+ URL.revokeObjectURL(url);
86
+ let node;
87
+ try {
88
+ node = new AudioWorkletNode(context, "energy-vad-processor");
89
+ console.log("Energy VAD node created successfully");
90
+ } catch (e) {
91
+ const error = new Error(
92
+ `Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
93
+ );
94
+ console.error(error.message);
95
+ throw error;
96
+ }
97
+ node.port.onmessage = (event) => {
98
+ try {
99
+ const { probability } = event.data;
100
+ if (typeof probability === "number" && !isNaN(probability)) {
101
+ onDecision(probability);
102
+ } else {
103
+ console.warn("Invalid VAD probability received:", event.data);
104
+ }
105
+ } catch (error) {
106
+ console.error("Error in VAD message handler:", error);
107
+ }
108
+ };
109
+ node.port.onmessageerror = (event) => {
110
+ console.error("VAD port message error:", event);
111
+ };
112
+ return node;
113
+ }
114
+ };
115
+
116
+ export {
117
+ EnergyVADPlugin
118
+ };
@@ -0,0 +1,67 @@
1
+ // src/noise-suppression/rnnoise-node.ts
2
+ var RNNoisePlugin = class {
3
+ name = "rnnoise-ns";
4
+ wasmBuffer = null;
5
+ async createNode(context, config) {
6
+ const { loadRnnoise, RnnoiseWorkletNode } = await import("@sapphi-red/web-noise-suppressor");
7
+ if (!config?.enabled) {
8
+ console.log("Noise suppression disabled, using passthrough node");
9
+ const pass = context.createGain();
10
+ return pass;
11
+ }
12
+ if (!config?.wasmUrl || !config?.simdUrl || !config?.workletUrl) {
13
+ const error = new Error(
14
+ `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}
15
+ To disable noise suppression, set noiseSuppression.enabled to false.`
16
+ );
17
+ console.error(error.message);
18
+ throw error;
19
+ }
20
+ try {
21
+ if (!this.wasmBuffer) {
22
+ console.log("Loading RNNoise WASM binary...");
23
+ this.wasmBuffer = await loadRnnoise({
24
+ url: config.wasmUrl,
25
+ simdUrl: config.simdUrl
26
+ });
27
+ console.log("RNNoise WASM loaded successfully");
28
+ }
29
+ } catch (error) {
30
+ const err = new Error(
31
+ `Failed to load RNNoise WASM binary: ${error instanceof Error ? error.message : String(error)}`
32
+ );
33
+ console.error(err);
34
+ throw err;
35
+ }
36
+ const workletUrl = config.workletUrl;
37
+ try {
38
+ await context.audioWorklet.addModule(workletUrl);
39
+ console.log("RNNoise worklet loaded successfully");
40
+ } catch (e) {
41
+ const error = new Error(
42
+ `Failed to load RNNoise worklet from ${workletUrl}: ${e instanceof Error ? e.message : String(e)}. Ensure the workletUrl points to a valid RNNoise worklet script.`
43
+ );
44
+ console.error(error.message);
45
+ throw error;
46
+ }
47
+ try {
48
+ const node = new RnnoiseWorkletNode(context, {
49
+ wasmBinary: this.wasmBuffer,
50
+ maxChannels: 1
51
+ // Mono for now
52
+ });
53
+ console.log("RNNoise worklet node created successfully");
54
+ return node;
55
+ } catch (error) {
56
+ const err = new Error(
57
+ `Failed to create RNNoise worklet node: ${error instanceof Error ? error.message : String(error)}`
58
+ );
59
+ console.error(err);
60
+ throw err;
61
+ }
62
+ }
63
+ };
64
+
65
+ export {
66
+ RNNoisePlugin
67
+ };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  RNNoisePlugin
3
- } from "./chunk-SDTOKWM2.mjs";
3
+ } from "./chunk-XO6B3D4A.mjs";
4
4
  import {
5
5
  EnergyVADPlugin
6
- } from "./chunk-UMU2KIB6.mjs";
6
+ } from "./chunk-NMHKX64G.mjs";
7
7
 
8
8
  // src/extensibility/plugins.ts
9
9
  var nsPlugins = /* @__PURE__ */ new Map();
@@ -44,43 +44,83 @@ var RNNoisePlugin = class {
44
44
  async createNode(context, config) {
45
45
  const { loadRnnoise, RnnoiseWorkletNode } = await import("@sapphi-red/web-noise-suppressor");
46
46
  if (!config?.enabled) {
47
+ console.log("Noise suppression disabled, using passthrough node");
47
48
  const pass = context.createGain();
48
49
  return pass;
49
50
  }
50
51
  if (!config?.wasmUrl || !config?.simdUrl || !config?.workletUrl) {
51
- throw new Error(
52
- "RNNoisePlugin requires 'wasmUrl', 'simdUrl', and 'workletUrl' to be configured. Please download the assets and provide the URLs."
52
+ const error = new Error(
53
+ `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}
54
+ To disable noise suppression, set noiseSuppression.enabled to false.`
53
55
  );
56
+ console.error(error.message);
57
+ throw error;
54
58
  }
55
- if (!this.wasmBuffer) {
56
- this.wasmBuffer = await loadRnnoise({
57
- url: config.wasmUrl,
58
- simdUrl: config.simdUrl
59
- });
59
+ try {
60
+ if (!this.wasmBuffer) {
61
+ console.log("Loading RNNoise WASM binary...");
62
+ this.wasmBuffer = await loadRnnoise({
63
+ url: config.wasmUrl,
64
+ simdUrl: config.simdUrl
65
+ });
66
+ console.log("RNNoise WASM loaded successfully");
67
+ }
68
+ } catch (error) {
69
+ const err = new Error(
70
+ `Failed to load RNNoise WASM binary: ${error instanceof Error ? error.message : String(error)}`
71
+ );
72
+ console.error(err);
73
+ throw err;
60
74
  }
61
75
  const workletUrl = config.workletUrl;
62
76
  try {
63
77
  await context.audioWorklet.addModule(workletUrl);
78
+ console.log("RNNoise worklet loaded successfully");
64
79
  } catch (e) {
65
- console.warn("Failed to add RNNoise worklet module:", e);
80
+ const error = new Error(
81
+ `Failed to load RNNoise worklet from ${workletUrl}: ${e instanceof Error ? e.message : String(e)}. Ensure the workletUrl points to a valid RNNoise worklet script.`
82
+ );
83
+ console.error(error.message);
84
+ throw error;
85
+ }
86
+ try {
87
+ const node = new RnnoiseWorkletNode(context, {
88
+ wasmBinary: this.wasmBuffer,
89
+ maxChannels: 1
90
+ // Mono for now
91
+ });
92
+ console.log("RNNoise worklet node created successfully");
93
+ return node;
94
+ } catch (error) {
95
+ const err = new Error(
96
+ `Failed to create RNNoise worklet node: ${error instanceof Error ? error.message : String(error)}`
97
+ );
98
+ console.error(err);
99
+ throw err;
66
100
  }
67
- const node = new RnnoiseWorkletNode(context, {
68
- wasmBinary: this.wasmBuffer,
69
- maxChannels: 1
70
- // Mono for now
71
- });
72
- return node;
73
101
  }
74
102
  };
75
103
 
76
104
  // src/vad/vad-node.ts
77
- var energyVadWorkletCode = `
105
+ var createEnergyVadWorkletCode = (vadConfig) => {
106
+ const energyParams = vadConfig?.energyVad || {};
107
+ const smoothing = energyParams.smoothing ?? 0.95;
108
+ const initialNoiseFloor = energyParams.initialNoiseFloor ?? 1e-3;
109
+ const noiseFloorAdaptRateQuiet = energyParams.noiseFloorAdaptRateQuiet ?? 0.01;
110
+ const noiseFloorAdaptRateLoud = energyParams.noiseFloorAdaptRateLoud ?? 1e-3;
111
+ const minSNR = energyParams.minSNR ?? 2;
112
+ const snrRange = energyParams.snrRange ?? 8;
113
+ return `
78
114
  class EnergyVadProcessor extends AudioWorkletProcessor {
79
115
  constructor() {
80
116
  super();
81
- this.smoothing = 0.95;
117
+ this.smoothing = ${smoothing};
82
118
  this.energy = 0;
83
- this.noiseFloor = 0.001;
119
+ this.noiseFloor = ${initialNoiseFloor};
120
+ this.noiseFloorAdaptRateQuiet = ${noiseFloorAdaptRateQuiet};
121
+ this.noiseFloorAdaptRateLoud = ${noiseFloorAdaptRateLoud};
122
+ this.minSNR = ${minSNR};
123
+ this.snrRange = ${snrRange};
84
124
  }
85
125
 
86
126
  process(inputs, outputs, parameters) {
@@ -88,51 +128,89 @@ class EnergyVadProcessor extends AudioWorkletProcessor {
88
128
  if (!input || !input.length) return true;
89
129
  const channel = input[0];
90
130
 
91
- // Calculate RMS
131
+ // Calculate RMS (Root Mean Square) energy
92
132
  let sum = 0;
93
133
  for (let i = 0; i < channel.length; i++) {
94
134
  sum += channel[i] * channel[i];
95
135
  }
96
136
  const rms = Math.sqrt(sum / channel.length);
97
137
 
98
- // Simple adaptive noise floor (very basic)
138
+ // Adaptive noise floor estimation
139
+ // When signal is quiet, adapt quickly to find new noise floor
140
+ // When signal is loud (speech), adapt slowly to avoid raising noise floor
99
141
  if (rms < this.noiseFloor) {
100
- this.noiseFloor = this.noiseFloor * 0.99 + rms * 0.01;
142
+ this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateQuiet) + rms * this.noiseFloorAdaptRateQuiet;
101
143
  } else {
102
- this.noiseFloor = this.noiseFloor * 0.999 + rms * 0.001;
144
+ this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateLoud) + rms * this.noiseFloorAdaptRateLoud;
103
145
  }
104
146
 
105
- // Calculate "probability" based on SNR
106
- // This is a heuristic mapping from energy to 0-1
147
+ // Calculate Signal-to-Noise Ratio (SNR)
107
148
  const snr = rms / (this.noiseFloor + 1e-6);
108
- const probability = Math.min(1, Math.max(0, (snr - 1.5) / 10)); // Arbitrary scaling
149
+
150
+ // Map SNR to probability (0-1)
151
+ // Probability is 0 when SNR <= minSNR
152
+ // Probability scales linearly from 0 to 1 between minSNR and (minSNR + snrRange)
153
+ // Probability is 1 when SNR >= (minSNR + snrRange)
154
+ const probability = Math.min(1, Math.max(0, (snr - this.minSNR) / this.snrRange));
109
155
 
110
- this.port.postMessage({ probability });
156
+ this.port.postMessage({ probability, snr, noiseFloor: this.noiseFloor, rms });
111
157
 
112
158
  return true;
113
159
  }
114
160
  }
115
161
  registerProcessor('energy-vad-processor', EnergyVadProcessor);
116
162
  `;
163
+ };
117
164
  var EnergyVADPlugin = class {
118
165
  name = "energy-vad";
119
166
  async createNode(context, config, onDecision) {
120
- const blob = new Blob([energyVadWorkletCode], {
167
+ if (!config?.enabled) {
168
+ console.log("VAD disabled, using passthrough node");
169
+ const pass = context.createGain();
170
+ return pass;
171
+ }
172
+ const workletCode = createEnergyVadWorkletCode(config);
173
+ const blob = new Blob([workletCode], {
121
174
  type: "application/javascript"
122
175
  });
123
176
  const url = URL.createObjectURL(blob);
124
177
  try {
125
178
  await context.audioWorklet.addModule(url);
179
+ console.log("Energy VAD worklet loaded successfully");
126
180
  } catch (e) {
127
- console.warn("Failed to add Energy VAD worklet:", e);
128
- throw e;
129
- } finally {
181
+ const error = new Error(
182
+ `Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
183
+ );
184
+ console.error(error.message);
130
185
  URL.revokeObjectURL(url);
186
+ throw error;
187
+ }
188
+ URL.revokeObjectURL(url);
189
+ let node;
190
+ try {
191
+ node = new AudioWorkletNode(context, "energy-vad-processor");
192
+ console.log("Energy VAD node created successfully");
193
+ } catch (e) {
194
+ const error = new Error(
195
+ `Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
196
+ );
197
+ console.error(error.message);
198
+ throw error;
131
199
  }
132
- const node = new AudioWorkletNode(context, "energy-vad-processor");
133
200
  node.port.onmessage = (event) => {
134
- const { probability } = event.data;
135
- onDecision(probability);
201
+ try {
202
+ const { probability } = event.data;
203
+ if (typeof probability === "number" && !isNaN(probability)) {
204
+ onDecision(probability);
205
+ } else {
206
+ console.warn("Invalid VAD probability received:", event.data);
207
+ }
208
+ } catch (error) {
209
+ console.error("Error in VAD message handler:", error);
210
+ }
211
+ };
212
+ node.port.onmessageerror = (event) => {
213
+ console.error("VAD port message error:", event);
136
214
  };
137
215
  return node;
138
216
  }
@@ -3,9 +3,9 @@ import {
3
3
  getVADPlugin,
4
4
  registerNoiseSuppressionPlugin,
5
5
  registerVADPlugin
6
- } from "../chunk-FS635GMR.mjs";
7
- import "../chunk-SDTOKWM2.mjs";
8
- import "../chunk-UMU2KIB6.mjs";
6
+ } from "../chunk-YOSTLLCS.mjs";
7
+ import "../chunk-XO6B3D4A.mjs";
8
+ import "../chunk-NMHKX64G.mjs";
9
9
  export {
10
10
  getNoiseSuppressionPlugin,
11
11
  getVADPlugin,