@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.
- package/README.md +50 -3
- package/dist/chunk-6P2RDBW5.mjs +47 -0
- package/dist/chunk-EXH2PNUE.mjs +212 -0
- package/{src/vad/vad-state.ts → dist/chunk-JJASCVEW.mjs} +21 -33
- package/dist/chunk-OZ7KMC4S.mjs +46 -0
- package/dist/chunk-R5JVHKWA.mjs +98 -0
- package/dist/chunk-WBQAMGXK.mjs +0 -0
- package/dist/chunk-XMTQPMQ6.mjs +91 -0
- package/dist/chunk-XO6B3D4A.mjs +67 -0
- package/dist/context/audio-context.d.mts +32 -0
- package/dist/context/audio-context.d.ts +32 -0
- package/dist/context/audio-context.js +75 -0
- package/dist/context/audio-context.mjs +16 -0
- package/dist/extensibility/plugins.d.mts +9 -0
- package/dist/extensibility/plugins.d.ts +9 -0
- package/dist/extensibility/plugins.js +238 -0
- package/dist/extensibility/plugins.mjs +14 -0
- package/dist/index.d.mts +10 -216
- package/dist/index.d.ts +10 -216
- package/dist/index.js +298 -80
- package/dist/index.mjs +29 -352
- package/dist/livekit/integration.d.mts +11 -0
- package/dist/livekit/integration.d.ts +11 -0
- package/dist/livekit/integration.js +585 -0
- package/dist/livekit/integration.mjs +12 -0
- package/dist/noise-suppression/rnnoise-node.d.mts +10 -0
- package/dist/noise-suppression/rnnoise-node.d.ts +10 -0
- package/dist/noise-suppression/rnnoise-node.js +101 -0
- package/dist/noise-suppression/rnnoise-node.mjs +6 -0
- package/dist/pipeline/audio-pipeline.d.mts +6 -0
- package/dist/pipeline/audio-pipeline.d.ts +6 -0
- package/dist/pipeline/audio-pipeline.js +499 -0
- package/dist/pipeline/audio-pipeline.mjs +11 -0
- package/dist/types.d.mts +155 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +18 -0
- package/dist/types.mjs +1 -0
- package/dist/vad/vad-node.d.mts +9 -0
- package/dist/vad/vad-node.d.ts +9 -0
- package/dist/vad/vad-node.js +122 -0
- package/dist/vad/vad-node.mjs +6 -0
- package/dist/vad/vad-state.d.mts +15 -0
- package/dist/vad/vad-state.d.ts +15 -0
- package/dist/vad/vad-state.js +83 -0
- package/dist/vad/vad-state.mjs +6 -0
- package/package.json +8 -5
- package/.github/workflows/publish.yml +0 -29
- package/bun.lock +0 -258
- package/src/context/audio-context.ts +0 -69
- package/src/extensibility/plugins.ts +0 -45
- package/src/index.ts +0 -8
- package/src/livekit/integration.ts +0 -61
- package/src/noise-suppression/rnnoise-node.ts +0 -62
- package/src/pipeline/audio-pipeline.ts +0 -154
- package/src/types.ts +0 -167
- package/src/vad/vad-node.ts +0 -78
- 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 (!
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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: {
|
|
288
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
516
|
+
processedTrack,
|
|
358
517
|
events: emitter,
|
|
359
518
|
get state() {
|
|
360
519
|
return lastVadState;
|
|
361
520
|
},
|
|
362
521
|
setConfig: (newConfig) => {
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
}
|