@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.
- package/README.md +218 -30
- package/dist/chunk-AHBRT4RD.mjs +307 -0
- package/dist/chunk-ERJVV5JR.mjs +91 -0
- package/dist/chunk-N553RHTI.mjs +93 -0
- package/dist/chunk-NMHKX64G.mjs +118 -0
- package/dist/chunk-XO6B3D4A.mjs +67 -0
- package/dist/{chunk-FS635GMR.mjs → chunk-YOSTLLCS.mjs} +2 -2
- package/dist/extensibility/plugins.js +110 -32
- package/dist/extensibility/plugins.mjs +3 -3
- package/dist/index.js +463 -97
- package/dist/index.mjs +6 -6
- package/dist/livekit/integration.js +463 -97
- package/dist/livekit/integration.mjs +6 -6
- package/dist/noise-suppression/rnnoise-node.js +42 -14
- package/dist/noise-suppression/rnnoise-node.mjs +1 -1
- package/dist/pipeline/audio-pipeline.js +396 -83
- package/dist/pipeline/audio-pipeline.mjs +5 -5
- package/dist/types.d.mts +118 -10
- package/dist/types.d.ts +118 -10
- package/dist/vad/vad-node.js +68 -18
- package/dist/vad/vad-node.mjs +1 -1
- package/dist/vad/vad-state.d.mts +1 -0
- package/dist/vad/vad-state.d.ts +1 -0
- package/dist/vad/vad-state.js +42 -8
- package/dist/vad/vad-state.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-HFSKQ33X.mjs +0 -38
- package/dist/chunk-JJASCVEW.mjs +0 -59
- package/dist/chunk-QU7E5HBA.mjs +0 -106
- package/dist/chunk-SDTOKWM2.mjs +0 -39
- package/dist/chunk-UMU2KIB6.mjs +0 -68
|
@@ -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-
|
|
3
|
+
} from "./chunk-XO6B3D4A.mjs";
|
|
4
4
|
import {
|
|
5
5
|
EnergyVADPlugin
|
|
6
|
-
} from "./chunk-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
this.wasmBuffer
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
117
|
+
this.smoothing = ${smoothing};
|
|
82
118
|
this.energy = 0;
|
|
83
|
-
this.noiseFloor =
|
|
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
|
-
//
|
|
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 *
|
|
142
|
+
this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateQuiet) + rms * this.noiseFloorAdaptRateQuiet;
|
|
101
143
|
} else {
|
|
102
|
-
this.noiseFloor = this.noiseFloor *
|
|
144
|
+
this.noiseFloor = this.noiseFloor * (1 - this.noiseFloorAdaptRateLoud) + rms * this.noiseFloorAdaptRateLoud;
|
|
103
145
|
}
|
|
104
146
|
|
|
105
|
-
// Calculate
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-YOSTLLCS.mjs";
|
|
7
|
+
import "../chunk-XO6B3D4A.mjs";
|
|
8
|
+
import "../chunk-NMHKX64G.mjs";
|
|
9
9
|
export {
|
|
10
10
|
getNoiseSuppressionPlugin,
|
|
11
11
|
getVADPlugin,
|