@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
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A audio processing library for the web, featuring RNNoise-based noise suppressio
|
|
|
7
7
|
- **Noise Suppression**: Uses `@sapphi-red/web-noise-suppressor` (RNNoise) for high-quality noise reduction.
|
|
8
8
|
- **Robust VAD**: Energy-based VAD with hysteresis, hangover, and pre-roll buffering to prevent cutting off speech onset.
|
|
9
9
|
- **Intelligent Muting**: Automatically gates audio or mutes LiveKit tracks when silent.
|
|
10
|
-
- **LiveKit Integration**:
|
|
10
|
+
- **LiveKit Integration**: Good support for `LocalAudioTrack`.
|
|
11
11
|
- **Extensible**: Plugin system for custom WASM/Worklet processors.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
@@ -20,18 +20,43 @@ pnpm install @tensamin/audio livekit-client
|
|
|
20
20
|
|
|
21
21
|
## Setup Assets
|
|
22
22
|
|
|
23
|
-
This library uses WASM and AudioWorklets for processing.
|
|
23
|
+
This library uses WASM and AudioWorklets for processing. **Asset setup is optional** - the pipeline can run in passthrough mode without them.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### For Noise Suppression (Optional)
|
|
26
|
+
|
|
27
|
+
If you want to enable noise suppression, download these files from `https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.5/dist/`:
|
|
26
28
|
|
|
27
29
|
- `rnnoise.wasm`
|
|
28
30
|
- `rnnoise_simd.wasm`
|
|
29
31
|
- `noise-suppressor-worklet.min.js`
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Place them in your project's public directory (e.g., `public/audio-processor/`).
|
|
34
|
+
|
|
35
|
+
**Note:** The pipeline will automatically disable noise suppression if these URLs are not provided, and will use passthrough mode instead.
|
|
32
36
|
|
|
33
37
|
## Usage
|
|
34
38
|
|
|
39
|
+
### Minimal Setup (Passthrough Mode)
|
|
40
|
+
|
|
41
|
+
If you want to use the pipeline without noise suppression or VAD (e.g., for testing or when features are not needed), you can disable them:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createAudioPipeline } from "@tensamin/audio";
|
|
45
|
+
|
|
46
|
+
// Get a stream
|
|
47
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
48
|
+
const track = stream.getAudioTracks()[0];
|
|
49
|
+
|
|
50
|
+
// Create pipeline
|
|
51
|
+
const pipeline = await createAudioPipeline(track, {
|
|
52
|
+
noiseSuppression: { enabled: false },
|
|
53
|
+
vad: { enabled: false },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Use the processed track
|
|
57
|
+
const processedStream = new MediaStream([pipeline.processedTrack]);
|
|
58
|
+
```
|
|
59
|
+
|
|
35
60
|
### Basic Usage (Raw MediaStream)
|
|
36
61
|
|
|
37
62
|
```ts
|
|
@@ -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-R5JVHKWA.mjs";
|
|
7
7
|
|
|
8
8
|
// src/extensibility/plugins.ts
|
|
9
9
|
var nsPlugins = /* @__PURE__ */ new Map();
|
|
@@ -0,0 +1,212 @@
|
|
|
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-6P2RDBW5.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 nsEnabled = config.noiseSuppression?.enabled !== false && Boolean(config.noiseSuppression?.wasmUrl && config.noiseSuppression?.simdUrl && config.noiseSuppression?.workletUrl);
|
|
20
|
+
const vadEnabled = config.vad?.enabled !== false;
|
|
21
|
+
const fullConfig = {
|
|
22
|
+
noiseSuppression: {
|
|
23
|
+
enabled: nsEnabled,
|
|
24
|
+
...config.noiseSuppression
|
|
25
|
+
},
|
|
26
|
+
vad: {
|
|
27
|
+
enabled: vadEnabled,
|
|
28
|
+
...config.vad
|
|
29
|
+
},
|
|
30
|
+
output: {
|
|
31
|
+
speechGain: 1,
|
|
32
|
+
silenceGain: vadEnabled ? 0 : 1,
|
|
33
|
+
// If no VAD, always output audio
|
|
34
|
+
gainRampTime: 0.02,
|
|
35
|
+
...config.output
|
|
36
|
+
},
|
|
37
|
+
livekit: { manageTrackMute: false, ...config.livekit }
|
|
38
|
+
};
|
|
39
|
+
console.log("Audio pipeline config:", {
|
|
40
|
+
noiseSuppression: fullConfig.noiseSuppression?.enabled,
|
|
41
|
+
vad: fullConfig.vad?.enabled,
|
|
42
|
+
output: fullConfig.output
|
|
43
|
+
});
|
|
44
|
+
if (!sourceTrack || sourceTrack.kind !== "audio") {
|
|
45
|
+
throw new Error("createAudioPipeline requires a valid audio MediaStreamTrack");
|
|
46
|
+
}
|
|
47
|
+
if (sourceTrack.readyState === "ended") {
|
|
48
|
+
throw new Error("Cannot create pipeline from an ended MediaStreamTrack");
|
|
49
|
+
}
|
|
50
|
+
const sourceStream = new MediaStream([sourceTrack]);
|
|
51
|
+
const sourceNode = context.createMediaStreamSource(sourceStream);
|
|
52
|
+
let nsNode;
|
|
53
|
+
let vadNode;
|
|
54
|
+
const emitter = mitt();
|
|
55
|
+
try {
|
|
56
|
+
const nsPlugin = getNoiseSuppressionPlugin(
|
|
57
|
+
fullConfig.noiseSuppression?.pluginName
|
|
58
|
+
);
|
|
59
|
+
nsNode = await nsPlugin.createNode(
|
|
60
|
+
context,
|
|
61
|
+
fullConfig.noiseSuppression
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
65
|
+
console.error("Failed to create noise suppression node:", err);
|
|
66
|
+
emitter.emit("error", err);
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
const vadStateMachine = new VADStateMachine(fullConfig.vad);
|
|
70
|
+
try {
|
|
71
|
+
const vadPlugin = getVADPlugin(fullConfig.vad?.pluginName);
|
|
72
|
+
vadNode = await vadPlugin.createNode(
|
|
73
|
+
context,
|
|
74
|
+
fullConfig.vad,
|
|
75
|
+
(prob) => {
|
|
76
|
+
try {
|
|
77
|
+
const timestamp = context.currentTime * 1e3;
|
|
78
|
+
const newState = vadStateMachine.processFrame(prob, timestamp);
|
|
79
|
+
if (newState.state !== lastVadState.state || Math.abs(newState.probability - lastVadState.probability) > 0.1) {
|
|
80
|
+
emitter.emit("vadChange", newState);
|
|
81
|
+
lastVadState = newState;
|
|
82
|
+
updateGain(newState);
|
|
83
|
+
}
|
|
84
|
+
} catch (vadError) {
|
|
85
|
+
const err = vadError instanceof Error ? vadError : new Error(String(vadError));
|
|
86
|
+
console.error("Error in VAD callback:", err);
|
|
87
|
+
emitter.emit("error", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
93
|
+
console.error("Failed to create VAD node:", err);
|
|
94
|
+
emitter.emit("error", err);
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
let lastVadState = {
|
|
98
|
+
isSpeaking: false,
|
|
99
|
+
probability: 0,
|
|
100
|
+
state: "silent"
|
|
101
|
+
};
|
|
102
|
+
const splitter = context.createGain();
|
|
103
|
+
sourceNode.connect(nsNode);
|
|
104
|
+
nsNode.connect(splitter);
|
|
105
|
+
splitter.connect(vadNode);
|
|
106
|
+
const delayNode = context.createDelay(1);
|
|
107
|
+
const preRollSeconds = (fullConfig.vad?.preRollMs ?? 200) / 1e3;
|
|
108
|
+
delayNode.delayTime.value = preRollSeconds;
|
|
109
|
+
const gainNode = context.createGain();
|
|
110
|
+
gainNode.gain.value = fullConfig.output?.silenceGain ?? 0;
|
|
111
|
+
const destination = context.createMediaStreamDestination();
|
|
112
|
+
try {
|
|
113
|
+
splitter.connect(delayNode);
|
|
114
|
+
delayNode.connect(gainNode);
|
|
115
|
+
gainNode.connect(destination);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
118
|
+
console.error("Failed to wire audio pipeline:", err);
|
|
119
|
+
emitter.emit("error", err);
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
function updateGain(state) {
|
|
123
|
+
try {
|
|
124
|
+
const { speechGain, silenceGain, gainRampTime } = fullConfig.output;
|
|
125
|
+
const targetGain = state.isSpeaking ? speechGain ?? 1 : silenceGain ?? 0;
|
|
126
|
+
const now = context.currentTime;
|
|
127
|
+
gainNode.gain.setTargetAtTime(targetGain, now, gainRampTime ?? 0.02);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
130
|
+
console.error("Failed to update gain:", err);
|
|
131
|
+
emitter.emit("error", err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const audioTracks = destination.stream.getAudioTracks();
|
|
135
|
+
console.log("Destination stream tracks:", {
|
|
136
|
+
count: audioTracks.length,
|
|
137
|
+
tracks: audioTracks.map((t) => ({
|
|
138
|
+
id: t.id,
|
|
139
|
+
label: t.label,
|
|
140
|
+
enabled: t.enabled,
|
|
141
|
+
readyState: t.readyState
|
|
142
|
+
}))
|
|
143
|
+
});
|
|
144
|
+
if (audioTracks.length === 0) {
|
|
145
|
+
const err = new Error(
|
|
146
|
+
"Failed to create processed audio track: destination stream has no audio tracks. This may indicate an issue with the audio graph connection."
|
|
147
|
+
);
|
|
148
|
+
console.error(err);
|
|
149
|
+
emitter.emit("error", err);
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
const processedTrack = audioTracks[0];
|
|
153
|
+
if (!processedTrack || processedTrack.readyState === "ended") {
|
|
154
|
+
const err = new Error("Processed audio track is invalid or ended");
|
|
155
|
+
console.error(err);
|
|
156
|
+
emitter.emit("error", err);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
console.log("Audio pipeline created successfully:", {
|
|
160
|
+
sourceTrack: {
|
|
161
|
+
id: sourceTrack.id,
|
|
162
|
+
label: sourceTrack.label,
|
|
163
|
+
readyState: sourceTrack.readyState
|
|
164
|
+
},
|
|
165
|
+
processedTrack: {
|
|
166
|
+
id: processedTrack.id,
|
|
167
|
+
label: processedTrack.label,
|
|
168
|
+
readyState: processedTrack.readyState
|
|
169
|
+
},
|
|
170
|
+
config: {
|
|
171
|
+
noiseSuppression: fullConfig.noiseSuppression?.enabled,
|
|
172
|
+
vad: fullConfig.vad?.enabled
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
function dispose() {
|
|
176
|
+
try {
|
|
177
|
+
sourceNode.disconnect();
|
|
178
|
+
nsNode.disconnect();
|
|
179
|
+
splitter.disconnect();
|
|
180
|
+
vadNode.disconnect();
|
|
181
|
+
delayNode.disconnect();
|
|
182
|
+
gainNode.disconnect();
|
|
183
|
+
destination.stream.getTracks().forEach((t) => t.stop());
|
|
184
|
+
unregisterPipeline();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("Error during pipeline disposal:", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
processedTrack,
|
|
191
|
+
events: emitter,
|
|
192
|
+
get state() {
|
|
193
|
+
return lastVadState;
|
|
194
|
+
},
|
|
195
|
+
setConfig: (newConfig) => {
|
|
196
|
+
try {
|
|
197
|
+
if (newConfig.vad) {
|
|
198
|
+
vadStateMachine.updateConfig(newConfig.vad);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
202
|
+
console.error("Failed to update config:", err);
|
|
203
|
+
emitter.emit("error", err);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
dispose
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
createAudioPipeline
|
|
212
|
+
};
|
|
@@ -42,22 +42,52 @@ registerProcessor('energy-vad-processor', EnergyVadProcessor);
|
|
|
42
42
|
var EnergyVADPlugin = class {
|
|
43
43
|
name = "energy-vad";
|
|
44
44
|
async createNode(context, config, onDecision) {
|
|
45
|
+
if (!config?.enabled) {
|
|
46
|
+
console.log("VAD disabled, using passthrough node");
|
|
47
|
+
const pass = context.createGain();
|
|
48
|
+
return pass;
|
|
49
|
+
}
|
|
45
50
|
const blob = new Blob([energyVadWorkletCode], {
|
|
46
51
|
type: "application/javascript"
|
|
47
52
|
});
|
|
48
53
|
const url = URL.createObjectURL(blob);
|
|
49
54
|
try {
|
|
50
55
|
await context.audioWorklet.addModule(url);
|
|
56
|
+
console.log("Energy VAD worklet loaded successfully");
|
|
51
57
|
} catch (e) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const error = new Error(
|
|
59
|
+
`Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
|
|
60
|
+
);
|
|
61
|
+
console.error(error.message);
|
|
55
62
|
URL.revokeObjectURL(url);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
URL.revokeObjectURL(url);
|
|
66
|
+
let node;
|
|
67
|
+
try {
|
|
68
|
+
node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
69
|
+
console.log("Energy VAD node created successfully");
|
|
70
|
+
} catch (e) {
|
|
71
|
+
const error = new Error(
|
|
72
|
+
`Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
|
|
73
|
+
);
|
|
74
|
+
console.error(error.message);
|
|
75
|
+
throw error;
|
|
56
76
|
}
|
|
57
|
-
const node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
58
77
|
node.port.onmessage = (event) => {
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
try {
|
|
79
|
+
const { probability } = event.data;
|
|
80
|
+
if (typeof probability === "number" && !isNaN(probability)) {
|
|
81
|
+
onDecision(probability);
|
|
82
|
+
} else {
|
|
83
|
+
console.warn("Invalid VAD probability received:", event.data);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("Error in VAD message handler:", error);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
node.port.onmessageerror = (event) => {
|
|
90
|
+
console.error("VAD port message error:", event);
|
|
61
91
|
};
|
|
62
92
|
return node;
|
|
63
93
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAudioPipeline
|
|
3
|
+
} from "./chunk-EXH2PNUE.mjs";
|
|
4
|
+
|
|
5
|
+
// src/livekit/integration.ts
|
|
6
|
+
async function attachProcessingToTrack(track, config = {}) {
|
|
7
|
+
if (!track) {
|
|
8
|
+
throw new Error("attachProcessingToTrack requires a valid LocalAudioTrack");
|
|
9
|
+
}
|
|
10
|
+
const originalTrack = track.mediaStreamTrack;
|
|
11
|
+
if (!originalTrack) {
|
|
12
|
+
throw new Error("LocalAudioTrack has no underlying MediaStreamTrack");
|
|
13
|
+
}
|
|
14
|
+
if (originalTrack.readyState === "ended") {
|
|
15
|
+
throw new Error("Cannot attach processing to an ended MediaStreamTrack");
|
|
16
|
+
}
|
|
17
|
+
let pipeline;
|
|
18
|
+
try {
|
|
19
|
+
console.log("Creating audio processing pipeline...");
|
|
20
|
+
pipeline = await createAudioPipeline(originalTrack, config);
|
|
21
|
+
console.log("Audio processing pipeline created successfully");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const err = new Error(
|
|
24
|
+
`Failed to create audio pipeline: ${error instanceof Error ? error.message : String(error)}`
|
|
25
|
+
);
|
|
26
|
+
console.error(err);
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
if (!pipeline.processedTrack) {
|
|
30
|
+
throw new Error("Pipeline did not return a processed track");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
console.log("Replacing LiveKit track with processed track...");
|
|
34
|
+
await track.replaceTrack(pipeline.processedTrack);
|
|
35
|
+
console.log("LiveKit track replaced successfully");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
pipeline.dispose();
|
|
38
|
+
const err = new Error(
|
|
39
|
+
`Failed to replace LiveKit track: ${error instanceof Error ? error.message : String(error)}`
|
|
40
|
+
);
|
|
41
|
+
console.error(err);
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
if (config.livekit?.manageTrackMute) {
|
|
45
|
+
let isVadMuted = false;
|
|
46
|
+
pipeline.events.on("vadChange", async (state) => {
|
|
47
|
+
try {
|
|
48
|
+
if (state.isSpeaking) {
|
|
49
|
+
if (isVadMuted) {
|
|
50
|
+
await track.unmute();
|
|
51
|
+
isVadMuted = false;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (!track.isMuted) {
|
|
55
|
+
await track.mute();
|
|
56
|
+
isVadMuted = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Error handling VAD-based track muting:", error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
pipeline.events.on("error", (error) => {
|
|
65
|
+
console.error("Audio pipeline error:", error);
|
|
66
|
+
});
|
|
67
|
+
const originalDispose = pipeline.dispose;
|
|
68
|
+
pipeline.dispose = () => {
|
|
69
|
+
try {
|
|
70
|
+
if (originalTrack.readyState === "live") {
|
|
71
|
+
console.log("Restoring original track...");
|
|
72
|
+
track.replaceTrack(originalTrack).catch((error) => {
|
|
73
|
+
console.error("Failed to restore original track:", error);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
originalDispose();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Error during pipeline disposal:", error);
|
|
79
|
+
try {
|
|
80
|
+
originalDispose();
|
|
81
|
+
} catch (disposeError) {
|
|
82
|
+
console.error("Error calling original dispose:", disposeError);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return pipeline;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
attachProcessingToTrack
|
|
91
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -44,32 +44,60 @@ 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
|
|
|
@@ -117,22 +145,52 @@ registerProcessor('energy-vad-processor', EnergyVadProcessor);
|
|
|
117
145
|
var EnergyVADPlugin = class {
|
|
118
146
|
name = "energy-vad";
|
|
119
147
|
async createNode(context, config, onDecision) {
|
|
148
|
+
if (!config?.enabled) {
|
|
149
|
+
console.log("VAD disabled, using passthrough node");
|
|
150
|
+
const pass = context.createGain();
|
|
151
|
+
return pass;
|
|
152
|
+
}
|
|
120
153
|
const blob = new Blob([energyVadWorkletCode], {
|
|
121
154
|
type: "application/javascript"
|
|
122
155
|
});
|
|
123
156
|
const url = URL.createObjectURL(blob);
|
|
124
157
|
try {
|
|
125
158
|
await context.audioWorklet.addModule(url);
|
|
159
|
+
console.log("Energy VAD worklet loaded successfully");
|
|
126
160
|
} catch (e) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
const error = new Error(
|
|
162
|
+
`Failed to load Energy VAD worklet: ${e instanceof Error ? e.message : String(e)}`
|
|
163
|
+
);
|
|
164
|
+
console.error(error.message);
|
|
130
165
|
URL.revokeObjectURL(url);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
URL.revokeObjectURL(url);
|
|
169
|
+
let node;
|
|
170
|
+
try {
|
|
171
|
+
node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
172
|
+
console.log("Energy VAD node created successfully");
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const error = new Error(
|
|
175
|
+
`Failed to create Energy VAD node: ${e instanceof Error ? e.message : String(e)}`
|
|
176
|
+
);
|
|
177
|
+
console.error(error.message);
|
|
178
|
+
throw error;
|
|
131
179
|
}
|
|
132
|
-
const node = new AudioWorkletNode(context, "energy-vad-processor");
|
|
133
180
|
node.port.onmessage = (event) => {
|
|
134
|
-
|
|
135
|
-
|
|
181
|
+
try {
|
|
182
|
+
const { probability } = event.data;
|
|
183
|
+
if (typeof probability === "number" && !isNaN(probability)) {
|
|
184
|
+
onDecision(probability);
|
|
185
|
+
} else {
|
|
186
|
+
console.warn("Invalid VAD probability received:", event.data);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("Error in VAD message handler:", error);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
node.port.onmessageerror = (event) => {
|
|
193
|
+
console.error("VAD port message error:", event);
|
|
136
194
|
};
|
|
137
195
|
return node;
|
|
138
196
|
}
|
|
@@ -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-6P2RDBW5.mjs";
|
|
7
|
+
import "../chunk-XO6B3D4A.mjs";
|
|
8
|
+
import "../chunk-R5JVHKWA.mjs";
|
|
9
9
|
export {
|
|
10
10
|
getNoiseSuppressionPlugin,
|
|
11
11
|
getVADPlugin,
|