@tensamin/audio 0.1.2 → 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.
- package/README.md +29 -4
- package/dist/{chunk-FS635GMR.mjs → chunk-6P2RDBW5.mjs} +2 -2
- package/dist/chunk-EXH2PNUE.mjs +212 -0
- package/dist/{chunk-UMU2KIB6.mjs → chunk-R5JVHKWA.mjs} +36 -6
- package/dist/chunk-XMTQPMQ6.mjs +91 -0
- package/dist/chunk-XO6B3D4A.mjs +67 -0
- package/dist/extensibility/plugins.js +78 -20
- package/dist/extensibility/plugins.mjs +3 -3
- package/dist/index.js +293 -76
- package/dist/index.mjs +5 -5
- package/dist/livekit/integration.js +293 -76
- package/dist/livekit/integration.mjs +5 -5
- package/dist/noise-suppression/rnnoise-node.js +42 -14
- package/dist/noise-suppression/rnnoise-node.mjs +1 -1
- package/dist/pipeline/audio-pipeline.js +226 -62
- package/dist/pipeline/audio-pipeline.mjs +4 -4
- package/dist/vad/vad-node.js +36 -6
- package/dist/vad/vad-node.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-HFSKQ33X.mjs +0 -38
- package/dist/chunk-QU7E5HBA.mjs +0 -106
- package/dist/chunk-SDTOKWM2.mjs +0 -39
|
@@ -63,32 +63,60 @@ var RNNoisePlugin = class {
|
|
|
63
63
|
async createNode(context, config) {
|
|
64
64
|
const { loadRnnoise, RnnoiseWorkletNode } = await import("@sapphi-red/web-noise-suppressor");
|
|
65
65
|
if (!config?.enabled) {
|
|
66
|
+
console.log("Noise suppression disabled, using passthrough node");
|
|
66
67
|
const pass = context.createGain();
|
|
67
68
|
return pass;
|
|
68
69
|
}
|
|
69
70
|
if (!config?.wasmUrl || !config?.simdUrl || !config?.workletUrl) {
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const error = new Error(
|
|
72
|
+
`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}
|
|
73
|
+
To disable noise suppression, set noiseSuppression.enabled to false.`
|
|
72
74
|
);
|
|
75
|
+
console.error(error.message);
|
|
76
|
+
throw error;
|
|
73
77
|
}
|
|
74
|
-
|
|
75
|
-
this.wasmBuffer
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
try {
|
|
79
|
+
if (!this.wasmBuffer) {
|
|
80
|
+
console.log("Loading RNNoise WASM binary...");
|
|
81
|
+
this.wasmBuffer = await loadRnnoise({
|
|
82
|
+
url: config.wasmUrl,
|
|
83
|
+
simdUrl: config.simdUrl
|
|
84
|
+
});
|
|
85
|
+
console.log("RNNoise WASM loaded successfully");
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const err = new Error(
|
|
89
|
+
`Failed to load RNNoise WASM binary: ${error instanceof Error ? error.message : String(error)}`
|
|
90
|
+
);
|
|
91
|
+
console.error(err);
|
|
92
|
+
throw err;
|
|
79
93
|
}
|
|
80
94
|
const workletUrl = config.workletUrl;
|
|
81
95
|
try {
|
|
82
96
|
await context.audioWorklet.addModule(workletUrl);
|
|
97
|
+
console.log("RNNoise worklet loaded successfully");
|
|
83
98
|
} catch (e) {
|
|
84
|
-
|
|
99
|
+
const error = new Error(
|
|
100
|
+
`Failed to load RNNoise worklet from ${workletUrl}: ${e instanceof Error ? e.message : String(e)}. Ensure the workletUrl points to a valid RNNoise worklet script.`
|
|
101
|
+
);
|
|
102
|
+
console.error(error.message);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const node = new RnnoiseWorkletNode(context, {
|
|
107
|
+
wasmBinary: this.wasmBuffer,
|
|
108
|
+
maxChannels: 1
|
|
109
|
+
// Mono for now
|
|
110
|
+
});
|
|
111
|
+
console.log("RNNoise worklet node created successfully");
|
|
112
|
+
return node;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const err = new Error(
|
|
115
|
+
`Failed to create RNNoise worklet node: ${error instanceof Error ? error.message : String(error)}`
|
|
116
|
+
);
|
|
117
|
+
console.error(err);
|
|
118
|
+
throw err;
|
|
85
119
|
}
|
|
86
|
-
const node = new RnnoiseWorkletNode(context, {
|
|
87
|
-
wasmBinary: this.wasmBuffer,
|
|
88
|
-
maxChannels: 1
|
|
89
|
-
// Mono for now
|
|
90
|
-
});
|
|
91
|
-
return node;
|
|
92
120
|
}
|
|
93
121
|
};
|
|
94
122
|
|
|
@@ -136,22 +164,52 @@ registerProcessor('energy-vad-processor', EnergyVadProcessor);
|
|
|
136
164
|
var EnergyVADPlugin = class {
|
|
137
165
|
name = "energy-vad";
|
|
138
166
|
async createNode(context, config, onDecision) {
|
|
167
|
+
if (!config?.enabled) {
|
|
168
|
+
console.log("VAD disabled, using passthrough node");
|
|
169
|
+
const pass = context.createGain();
|
|
170
|
+
return pass;
|
|
171
|
+
}
|
|
139
172
|
const blob = new Blob([energyVadWorkletCode], {
|
|
140
173
|
type: "application/javascript"
|
|
141
174
|
});
|
|
142
175
|
const url = URL.createObjectURL(blob);
|
|
143
176
|
try {
|
|
144
177
|
await context.audioWorklet.addModule(url);
|
|
178
|
+
console.log("Energy VAD worklet loaded successfully");
|
|
145
179
|
} catch (e) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
const error = new Error(
|
|
181
|
+
`Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
|
|
182
|
+
);
|
|
183
|
+
console.error(error.message);
|
|
149
184
|
URL.revokeObjectURL(url);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
URL.revokeObjectURL(url);
|
|
188
|
+
let node;
|
|
189
|
+
try {
|
|
190
|
+
node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
191
|
+
console.log("Energy VAD node created successfully");
|
|
192
|
+
} catch (e) {
|
|
193
|
+
const error = new Error(
|
|
194
|
+
`Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
|
|
195
|
+
);
|
|
196
|
+
console.error(error.message);
|
|
197
|
+
throw error;
|
|
150
198
|
}
|
|
151
|
-
const node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
152
199
|
node.port.onmessage = (event) => {
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
try {
|
|
201
|
+
const { probability } = event.data;
|
|
202
|
+
if (typeof probability === "number" && !isNaN(probability)) {
|
|
203
|
+
onDecision(probability);
|
|
204
|
+
} else {
|
|
205
|
+
console.warn("Invalid VAD probability received:", event.data);
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Error in VAD message handler:", error);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
node.port.onmessageerror = (event) => {
|
|
212
|
+
console.error("VAD port message error:", event);
|
|
155
213
|
};
|
|
156
214
|
return node;
|
|
157
215
|
}
|
|
@@ -245,42 +303,84 @@ var VADStateMachine = class {
|
|
|
245
303
|
async function createAudioPipeline(sourceTrack, config = {}) {
|
|
246
304
|
const context = getAudioContext();
|
|
247
305
|
registerPipeline();
|
|
306
|
+
const nsEnabled = config.noiseSuppression?.enabled !== false && Boolean(config.noiseSuppression?.wasmUrl && config.noiseSuppression?.simdUrl && config.noiseSuppression?.workletUrl);
|
|
307
|
+
const vadEnabled = config.vad?.enabled !== false;
|
|
248
308
|
const fullConfig = {
|
|
249
|
-
noiseSuppression: {
|
|
250
|
-
|
|
309
|
+
noiseSuppression: {
|
|
310
|
+
enabled: nsEnabled,
|
|
311
|
+
...config.noiseSuppression
|
|
312
|
+
},
|
|
313
|
+
vad: {
|
|
314
|
+
enabled: vadEnabled,
|
|
315
|
+
...config.vad
|
|
316
|
+
},
|
|
251
317
|
output: {
|
|
252
318
|
speechGain: 1,
|
|
253
|
-
silenceGain: 0,
|
|
319
|
+
silenceGain: vadEnabled ? 0 : 1,
|
|
320
|
+
// If no VAD, always output audio
|
|
254
321
|
gainRampTime: 0.02,
|
|
255
322
|
...config.output
|
|
256
323
|
},
|
|
257
324
|
livekit: { manageTrackMute: false, ...config.livekit }
|
|
258
325
|
};
|
|
326
|
+
console.log("Audio pipeline config:", {
|
|
327
|
+
noiseSuppression: fullConfig.noiseSuppression?.enabled,
|
|
328
|
+
vad: fullConfig.vad?.enabled,
|
|
329
|
+
output: fullConfig.output
|
|
330
|
+
});
|
|
331
|
+
if (!sourceTrack || sourceTrack.kind !== "audio") {
|
|
332
|
+
throw new Error("createAudioPipeline requires a valid audio MediaStreamTrack");
|
|
333
|
+
}
|
|
334
|
+
if (sourceTrack.readyState === "ended") {
|
|
335
|
+
throw new Error("Cannot create pipeline from an ended MediaStreamTrack");
|
|
336
|
+
}
|
|
259
337
|
const sourceStream = new MediaStream([sourceTrack]);
|
|
260
338
|
const sourceNode = context.createMediaStreamSource(sourceStream);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
);
|
|
264
|
-
const nsNode = await nsPlugin.createNode(
|
|
265
|
-
context,
|
|
266
|
-
fullConfig.noiseSuppression
|
|
267
|
-
);
|
|
268
|
-
const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
|
|
269
|
-
const vadStateMachine = new VADStateMachine(fullConfig.vad);
|
|
339
|
+
let nsNode;
|
|
340
|
+
let vadNode;
|
|
270
341
|
const emitter = (0, import_mitt.default)();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
342
|
+
try {
|
|
343
|
+
const nsPlugin = getNoiseSuppressionPlugin(
|
|
344
|
+
fullConfig.noiseSuppression?.pluginName
|
|
345
|
+
);
|
|
346
|
+
nsNode = await nsPlugin.createNode(
|
|
347
|
+
context,
|
|
348
|
+
fullConfig.noiseSuppression
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
352
|
+
console.error("Failed to create noise suppression node:", err);
|
|
353
|
+
emitter.emit("error", err);
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
const vadStateMachine = new VADStateMachine(fullConfig.vad);
|
|
357
|
+
try {
|
|
358
|
+
const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
|
|
359
|
+
vadNode = await vadPlugin.createNode(
|
|
360
|
+
context,
|
|
361
|
+
fullConfig.vad,
|
|
362
|
+
(prob) => {
|
|
363
|
+
try {
|
|
364
|
+
const timestamp = context.currentTime * 1e3;
|
|
365
|
+
const newState = vadStateMachine.processFrame(prob, timestamp);
|
|
366
|
+
if (newState.state !== lastVadState.state || Math.abs(newState.probability - lastVadState.probability) > 0.1) {
|
|
367
|
+
emitter.emit("vadChange", newState);
|
|
368
|
+
lastVadState = newState;
|
|
369
|
+
updateGain(newState);
|
|
370
|
+
}
|
|
371
|
+
} catch (vadError) {
|
|
372
|
+
const err = vadError instanceof Error ? vadError : new Error(String(vadError));
|
|
373
|
+
console.error("Error in VAD callback:", err);
|
|
374
|
+
emitter.emit("error", err);
|
|
375
|
+
}
|
|
281
376
|
}
|
|
282
|
-
|
|
283
|
-
)
|
|
377
|
+
);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
380
|
+
console.error("Failed to create VAD node:", err);
|
|
381
|
+
emitter.emit("error", err);
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
284
384
|
let lastVadState = {
|
|
285
385
|
isSpeaking: false,
|
|
286
386
|
probability: 0,
|
|
@@ -296,34 +396,98 @@ async function createAudioPipeline(sourceTrack, config = {}) {
|
|
|
296
396
|
const gainNode = context.createGain();
|
|
297
397
|
gainNode.gain.value = fullConfig.output?.silenceGain ?? 0;
|
|
298
398
|
const destination = context.createMediaStreamDestination();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
399
|
+
try {
|
|
400
|
+
splitter.connect(delayNode);
|
|
401
|
+
delayNode.connect(gainNode);
|
|
402
|
+
gainNode.connect(destination);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
405
|
+
console.error("Failed to wire audio pipeline:", err);
|
|
406
|
+
emitter.emit("error", err);
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
302
409
|
function updateGain(state) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
410
|
+
try {
|
|
411
|
+
const { speechGain, silenceGain, gainRampTime } = fullConfig.output;
|
|
412
|
+
const targetGain = state.isSpeaking ? speechGain ?? 1 : silenceGain ?? 0;
|
|
413
|
+
const now = context.currentTime;
|
|
414
|
+
gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
417
|
+
console.error("Failed to update gain:", err);
|
|
418
|
+
emitter.emit("error", err);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const audioTracks = destination.stream.getAudioTracks();
|
|
422
|
+
console.log("Destination stream tracks:", {
|
|
423
|
+
count: audioTracks.length,
|
|
424
|
+
tracks: audioTracks.map((t) => ({
|
|
425
|
+
id: t.id,
|
|
426
|
+
label: t.label,
|
|
427
|
+
enabled: t.enabled,
|
|
428
|
+
readyState: t.readyState
|
|
429
|
+
}))
|
|
430
|
+
});
|
|
431
|
+
if (audioTracks.length === 0) {
|
|
432
|
+
const err = new Error(
|
|
433
|
+
"Failed to create processed audio track: destination stream has no audio tracks. This may indicate an issue with the audio graph connection."
|
|
434
|
+
);
|
|
435
|
+
console.error(err);
|
|
436
|
+
emitter.emit("error", err);
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
const processedTrack = audioTracks[0];
|
|
440
|
+
if (!processedTrack || processedTrack.readyState === "ended") {
|
|
441
|
+
const err = new Error("Processed audio track is invalid or ended");
|
|
442
|
+
console.error(err);
|
|
443
|
+
emitter.emit("error", err);
|
|
444
|
+
throw err;
|
|
307
445
|
}
|
|
446
|
+
console.log("Audio pipeline created successfully:", {
|
|
447
|
+
sourceTrack: {
|
|
448
|
+
id: sourceTrack.id,
|
|
449
|
+
label: sourceTrack.label,
|
|
450
|
+
readyState: sourceTrack.readyState
|
|
451
|
+
},
|
|
452
|
+
processedTrack: {
|
|
453
|
+
id: processedTrack.id,
|
|
454
|
+
label: processedTrack.label,
|
|
455
|
+
readyState: processedTrack.readyState
|
|
456
|
+
},
|
|
457
|
+
config: {
|
|
458
|
+
noiseSuppression: fullConfig.noiseSuppression?.enabled,
|
|
459
|
+
vad: fullConfig.vad?.enabled
|
|
460
|
+
}
|
|
461
|
+
});
|
|
308
462
|
function dispose() {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
463
|
+
try {
|
|
464
|
+
sourceNode.disconnect();
|
|
465
|
+
nsNode.disconnect();
|
|
466
|
+
splitter.disconnect();
|
|
467
|
+
vadNode.disconnect();
|
|
468
|
+
delayNode.disconnect();
|
|
469
|
+
gainNode.disconnect();
|
|
470
|
+
destination.stream.getTracks().forEach((t) => t.stop());
|
|
471
|
+
unregisterPipeline();
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error("Error during pipeline disposal:", error);
|
|
474
|
+
}
|
|
317
475
|
}
|
|
318
476
|
return {
|
|
319
|
-
processedTrack
|
|
477
|
+
processedTrack,
|
|
320
478
|
events: emitter,
|
|
321
479
|
get state() {
|
|
322
480
|
return lastVadState;
|
|
323
481
|
},
|
|
324
482
|
setConfig: (newConfig) => {
|
|
325
|
-
|
|
326
|
-
|
|
483
|
+
try {
|
|
484
|
+
if (newConfig.vad) {
|
|
485
|
+
vadStateMachine.updateConfig(newConfig.vad);
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
489
|
+
console.error("Failed to update config:", err);
|
|
490
|
+
emitter.emit("error", err);
|
|
327
491
|
}
|
|
328
492
|
},
|
|
329
493
|
dispose
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createAudioPipeline
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-EXH2PNUE.mjs";
|
|
4
4
|
import "../chunk-JJASCVEW.mjs";
|
|
5
5
|
import "../chunk-OZ7KMC4S.mjs";
|
|
6
|
-
import "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-6P2RDBW5.mjs";
|
|
7
|
+
import "../chunk-XO6B3D4A.mjs";
|
|
8
|
+
import "../chunk-R5JVHKWA.mjs";
|
|
9
9
|
export {
|
|
10
10
|
createAudioPipeline
|
|
11
11
|
};
|
package/dist/vad/vad-node.js
CHANGED
|
@@ -66,22 +66,52 @@ registerProcessor('energy-vad-processor', EnergyVadProcessor);
|
|
|
66
66
|
var EnergyVADPlugin = class {
|
|
67
67
|
name = "energy-vad";
|
|
68
68
|
async createNode(context, config, onDecision) {
|
|
69
|
+
if (!config?.enabled) {
|
|
70
|
+
console.log("VAD disabled, using passthrough node");
|
|
71
|
+
const pass = context.createGain();
|
|
72
|
+
return pass;
|
|
73
|
+
}
|
|
69
74
|
const blob = new Blob([energyVadWorkletCode], {
|
|
70
75
|
type: "application/javascript"
|
|
71
76
|
});
|
|
72
77
|
const url = URL.createObjectURL(blob);
|
|
73
78
|
try {
|
|
74
79
|
await context.audioWorklet.addModule(url);
|
|
80
|
+
console.log("Energy VAD worklet loaded successfully");
|
|
75
81
|
} catch (e) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const error = new Error(
|
|
83
|
+
`Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
|
|
84
|
+
);
|
|
85
|
+
console.error(error.message);
|
|
79
86
|
URL.revokeObjectURL(url);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
URL.revokeObjectURL(url);
|
|
90
|
+
let node;
|
|
91
|
+
try {
|
|
92
|
+
node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
93
|
+
console.log("Energy VAD node created successfully");
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const error = new Error(
|
|
96
|
+
`Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
|
|
97
|
+
);
|
|
98
|
+
console.error(error.message);
|
|
99
|
+
throw error;
|
|
80
100
|
}
|
|
81
|
-
const node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
82
101
|
node.port.onmessage = (event) => {
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
try {
|
|
103
|
+
const { probability } = event.data;
|
|
104
|
+
if (typeof probability === "number" && !isNaN(probability)) {
|
|
105
|
+
onDecision(probability);
|
|
106
|
+
} else {
|
|
107
|
+
console.warn("Invalid VAD probability received:", event.data);
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("Error in VAD message handler:", error);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
node.port.onmessageerror = (event) => {
|
|
114
|
+
console.error("VAD port message error:", event);
|
|
85
115
|
};
|
|
86
116
|
return node;
|
|
87
117
|
}
|
package/dist/vad/vad-node.mjs
CHANGED
package/package.json
CHANGED
package/dist/chunk-HFSKQ33X.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAudioPipeline
|
|
3
|
-
} from "./chunk-QU7E5HBA.mjs";
|
|
4
|
-
|
|
5
|
-
// src/livekit/integration.ts
|
|
6
|
-
async function attachProcessingToTrack(track, config = {}) {
|
|
7
|
-
const originalTrack = track.mediaStreamTrack;
|
|
8
|
-
const pipeline = await createAudioPipeline(originalTrack, config);
|
|
9
|
-
await track.replaceTrack(pipeline.processedTrack);
|
|
10
|
-
if (config.livekit?.manageTrackMute) {
|
|
11
|
-
let isVadMuted = false;
|
|
12
|
-
pipeline.events.on("vadChange", async (state) => {
|
|
13
|
-
if (state.isSpeaking) {
|
|
14
|
-
if (isVadMuted) {
|
|
15
|
-
await track.unmute();
|
|
16
|
-
isVadMuted = false;
|
|
17
|
-
}
|
|
18
|
-
} else {
|
|
19
|
-
if (!track.isMuted) {
|
|
20
|
-
await track.mute();
|
|
21
|
-
isVadMuted = true;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
const originalDispose = pipeline.dispose;
|
|
27
|
-
pipeline.dispose = () => {
|
|
28
|
-
if (originalTrack.readyState === "live") {
|
|
29
|
-
track.replaceTrack(originalTrack).catch(console.error);
|
|
30
|
-
}
|
|
31
|
-
originalDispose();
|
|
32
|
-
};
|
|
33
|
-
return pipeline;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export {
|
|
37
|
-
attachProcessingToTrack
|
|
38
|
-
};
|
package/dist/chunk-QU7E5HBA.mjs
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
VADStateMachine
|
|
3
|
-
} from "./chunk-JJASCVEW.mjs";
|
|
4
|
-
import {
|
|
5
|
-
getAudioContext,
|
|
6
|
-
registerPipeline,
|
|
7
|
-
unregisterPipeline
|
|
8
|
-
} from "./chunk-OZ7KMC4S.mjs";
|
|
9
|
-
import {
|
|
10
|
-
getNoiseSuppressionPlugin,
|
|
11
|
-
getVADPlugin
|
|
12
|
-
} from "./chunk-FS635GMR.mjs";
|
|
13
|
-
|
|
14
|
-
// src/pipeline/audio-pipeline.ts
|
|
15
|
-
import mitt from "mitt";
|
|
16
|
-
async function createAudioPipeline(sourceTrack, config = {}) {
|
|
17
|
-
const context = getAudioContext();
|
|
18
|
-
registerPipeline();
|
|
19
|
-
const fullConfig = {
|
|
20
|
-
noiseSuppression: { enabled: true, ...config.noiseSuppression },
|
|
21
|
-
vad: { enabled: true, ...config.vad },
|
|
22
|
-
output: {
|
|
23
|
-
speechGain: 1,
|
|
24
|
-
silenceGain: 0,
|
|
25
|
-
gainRampTime: 0.02,
|
|
26
|
-
...config.output
|
|
27
|
-
},
|
|
28
|
-
livekit: { manageTrackMute: false, ...config.livekit }
|
|
29
|
-
};
|
|
30
|
-
const sourceStream = new MediaStream([sourceTrack]);
|
|
31
|
-
const sourceNode = context.createMediaStreamSource(sourceStream);
|
|
32
|
-
const nsPlugin = getNoiseSuppressionPlugin(
|
|
33
|
-
fullConfig.noiseSuppression?.pluginName
|
|
34
|
-
);
|
|
35
|
-
const nsNode = await nsPlugin.createNode(
|
|
36
|
-
context,
|
|
37
|
-
fullConfig.noiseSuppression
|
|
38
|
-
);
|
|
39
|
-
const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
|
|
40
|
-
const vadStateMachine = new VADStateMachine(fullConfig.vad);
|
|
41
|
-
const emitter = mitt();
|
|
42
|
-
const vadNode = await vadPlugin.createNode(
|
|
43
|
-
context,
|
|
44
|
-
fullConfig.vad,
|
|
45
|
-
(prob) => {
|
|
46
|
-
const timestamp = context.currentTime * 1e3;
|
|
47
|
-
const newState = vadStateMachine.processFrame(prob, timestamp);
|
|
48
|
-
if (newState.state !== lastVadState.state || Math.abs(newState.probability - lastVadState.probability) > 0.1) {
|
|
49
|
-
emitter.emit("vadChange", newState);
|
|
50
|
-
lastVadState = newState;
|
|
51
|
-
updateGain(newState);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
);
|
|
55
|
-
let lastVadState = {
|
|
56
|
-
isSpeaking: false,
|
|
57
|
-
probability: 0,
|
|
58
|
-
state: "silent"
|
|
59
|
-
};
|
|
60
|
-
const splitter = context.createGain();
|
|
61
|
-
sourceNode.connect(nsNode);
|
|
62
|
-
nsNode.connect(splitter);
|
|
63
|
-
splitter.connect(vadNode);
|
|
64
|
-
const delayNode = context.createDelay(1);
|
|
65
|
-
const preRollSeconds = (fullConfig.vad?.preRollMs ?? 200) / 1e3;
|
|
66
|
-
delayNode.delayTime.value = preRollSeconds;
|
|
67
|
-
const gainNode = context.createGain();
|
|
68
|
-
gainNode.gain.value = fullConfig.output?.silenceGain ?? 0;
|
|
69
|
-
const destination = context.createMediaStreamDestination();
|
|
70
|
-
splitter.connect(delayNode);
|
|
71
|
-
delayNode.connect(gainNode);
|
|
72
|
-
gainNode.connect(destination);
|
|
73
|
-
function updateGain(state) {
|
|
74
|
-
const { speechGain, silenceGain, gainRampTime } = fullConfig.output;
|
|
75
|
-
const targetGain = state.isSpeaking ? speechGain ?? 1 : silenceGain ?? 0;
|
|
76
|
-
const now = context.currentTime;
|
|
77
|
-
gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
|
|
78
|
-
}
|
|
79
|
-
function dispose() {
|
|
80
|
-
sourceNode.disconnect();
|
|
81
|
-
nsNode.disconnect();
|
|
82
|
-
splitter.disconnect();
|
|
83
|
-
vadNode.disconnect();
|
|
84
|
-
delayNode.disconnect();
|
|
85
|
-
gainNode.disconnect();
|
|
86
|
-
destination.stream.getTracks().forEach((t) => t.stop());
|
|
87
|
-
unregisterPipeline();
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
processedTrack: destination.stream.getAudioTracks()[0],
|
|
91
|
-
events: emitter,
|
|
92
|
-
get state() {
|
|
93
|
-
return lastVadState;
|
|
94
|
-
},
|
|
95
|
-
setConfig: (newConfig) => {
|
|
96
|
-
if (newConfig.vad) {
|
|
97
|
-
vadStateMachine.updateConfig(newConfig.vad);
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
dispose
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export {
|
|
105
|
-
createAudioPipeline
|
|
106
|
-
};
|
package/dist/chunk-SDTOKWM2.mjs
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
const pass = context.createGain();
|
|
9
|
-
return pass;
|
|
10
|
-
}
|
|
11
|
-
if (!config?.wasmUrl || !config?.simdUrl || !config?.workletUrl) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"RNNoisePlugin requires 'wasmUrl', 'simdUrl', and 'workletUrl' to be configured. Please download the assets and provide the URLs."
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
if (!this.wasmBuffer) {
|
|
17
|
-
this.wasmBuffer = await loadRnnoise({
|
|
18
|
-
url: config.wasmUrl,
|
|
19
|
-
simdUrl: config.simdUrl
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
const workletUrl = config.workletUrl;
|
|
23
|
-
try {
|
|
24
|
-
await context.audioWorklet.addModule(workletUrl);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
console.warn("Failed to add RNNoise worklet module:", e);
|
|
27
|
-
}
|
|
28
|
-
const node = new RnnoiseWorkletNode(context, {
|
|
29
|
-
wasmBinary: this.wasmBuffer,
|
|
30
|
-
maxChannels: 1
|
|
31
|
-
// Mono for now
|
|
32
|
-
});
|
|
33
|
-
return node;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export {
|
|
38
|
-
RNNoisePlugin
|
|
39
|
-
};
|