decibri 4.4.1 → 5.0.0
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/CHANGELOG.md +35 -3
- package/MIGRATION.md +70 -0
- package/README.md +45 -8
- package/examples/README.md +4 -3
- package/examples/decibri.browser.js +18 -6
- package/index.d.ts +41 -0
- package/index.js +52 -52
- package/models/README.md +149 -0
- package/models/fastenhancer_t.onnx +0 -0
- package/package.json +5 -5
- package/src/browser/decibri-browser.js +33 -13
- package/src/browser/index.d.ts +30 -18
- package/src/decibri.d.ts +104 -17
- package/src/decibri.js +197 -55
- package/src/errors.js +14 -0
package/src/decibri.d.ts
CHANGED
|
@@ -33,6 +33,34 @@ export interface VersionInfo {
|
|
|
33
33
|
binding: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Voice-activity-detection config object, passed on the `vad` option to tune
|
|
38
|
+
* the detector's threshold and holdoff. The bare `vad: 'silero'` / `vad:
|
|
39
|
+
* 'energy'` shorthand selects a mode with its default policy; pass this object
|
|
40
|
+
* to override the threshold or holdoff.
|
|
41
|
+
*/
|
|
42
|
+
export interface VadOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Which detector to run.
|
|
45
|
+
* - `'silero'`: Silero VAD v5 ML model (more accurate, ~1ms inference)
|
|
46
|
+
* - `'energy'`: RMS energy threshold (lightweight, no model)
|
|
47
|
+
*/
|
|
48
|
+
model: 'silero' | 'energy';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Speech-detection threshold for the active mode.
|
|
52
|
+
* @default 0.5 for `'silero'`, 0.01 for `'energy'`
|
|
53
|
+
* @range 0–1
|
|
54
|
+
*/
|
|
55
|
+
threshold?: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Milliseconds of sub-threshold audio before emitting `'silence'`.
|
|
59
|
+
* @default 300
|
|
60
|
+
*/
|
|
61
|
+
holdoffMs?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
/** Constructor options for `Microphone`. */
|
|
37
65
|
export interface MicrophoneOptions extends ReadableOptions {
|
|
38
66
|
/**
|
|
@@ -43,9 +71,12 @@ export interface MicrophoneOptions extends ReadableOptions {
|
|
|
43
71
|
sampleRate?: number;
|
|
44
72
|
|
|
45
73
|
/**
|
|
46
|
-
* Number of input channels.
|
|
74
|
+
* Number of input channels. Mono only: the only accepted value is `1`, and a
|
|
75
|
+
* value greater than `1` throws a `RangeError` (multichannel capture is not
|
|
76
|
+
* supported) rather than being silently downmixed. The option is kept for
|
|
77
|
+
* forward compatibility: a future release may accept a value greater than `1`
|
|
78
|
+
* by delivering true interleaved multichannel.
|
|
47
79
|
* @default 1
|
|
48
|
-
* @range 1–32
|
|
49
80
|
*/
|
|
50
81
|
channels?: number;
|
|
51
82
|
|
|
@@ -76,36 +107,85 @@ export interface MicrophoneOptions extends ReadableOptions {
|
|
|
76
107
|
dtype?: 'int16' | 'float32';
|
|
77
108
|
|
|
78
109
|
/**
|
|
79
|
-
* Voice activity detection
|
|
110
|
+
* Voice activity detection. One of:
|
|
80
111
|
* - `false`: disabled (default)
|
|
81
112
|
* - `'silero'`: Silero VAD v5 ML model (more accurate, ~1ms inference)
|
|
82
113
|
* - `'energy'`: RMS energy threshold (lightweight)
|
|
114
|
+
* - a `VadOptions` config object `{ model, threshold?, holdoffMs? }` to tune
|
|
115
|
+
* the threshold and holdoff for the chosen model
|
|
83
116
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
117
|
+
* The string shorthand uses the mode's default threshold (0.5 for `'silero'`,
|
|
118
|
+
* 0.01 for `'energy'`) and a 300 ms holdoff; pass a `VadOptions` object to
|
|
119
|
+
* override them. When enabled, emits `'speech'` and `'silence'` events and
|
|
120
|
+
* updates `vadScore`. The legacy `vad: true` form is rejected; specify the
|
|
121
|
+
* mode explicitly.
|
|
86
122
|
* @default false
|
|
87
123
|
*/
|
|
88
|
-
vad?: false | 'silero' | 'energy';
|
|
124
|
+
vad?: false | 'silero' | 'energy' | VadOptions;
|
|
89
125
|
|
|
90
126
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
127
|
+
* Path to the Silero VAD ONNX model file.
|
|
128
|
+
* Only used when `vad` is `'silero'`.
|
|
129
|
+
* Defaults to `models/silero_vad.onnx` relative to the package.
|
|
94
130
|
*/
|
|
95
|
-
|
|
131
|
+
modelPath?: string;
|
|
96
132
|
|
|
97
133
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
134
|
+
* Remove a constant (DC) offset from the captured audio with a one-pole
|
|
135
|
+
* DC-blocking high-pass. Set `true` to enable it; omit or set `false` to
|
|
136
|
+
* leave it off (the default), which keeps the capture path byte-identical.
|
|
137
|
+
* Runs first in the chain, before denoise, and is same-length with no added
|
|
138
|
+
* latency, so `vadScore` and the `speech` / `silence` events are unaffected.
|
|
139
|
+
* Pure DSP: no bundled file or download is needed.
|
|
140
|
+
* @default undefined
|
|
100
141
|
*/
|
|
101
|
-
|
|
142
|
+
dcRemoval?: boolean;
|
|
102
143
|
|
|
103
144
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
145
|
+
* Single-channel speech enhancement (denoise) model applied to the captured
|
|
146
|
+
* audio. The only accepted value is `'fastenhancer-t'`; omit to leave denoise
|
|
147
|
+
* off (the default), which keeps the capture path unchanged. The bundled
|
|
148
|
+
* model ships with the package; no path is required.
|
|
149
|
+
*
|
|
150
|
+
* When set, the captured audio is denoised before delivery and the `'data'`
|
|
151
|
+
* chunks carry the enhanced signal. VAD reads the pre-enhancement signal, so
|
|
152
|
+
* `vadScore` and the `speech` / `silence` events are unaffected.
|
|
153
|
+
* @default undefined
|
|
107
154
|
*/
|
|
108
|
-
|
|
155
|
+
denoise?: 'fastenhancer-t';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* High-pass filter cutoff in Hz applied to the captured audio, removing
|
|
159
|
+
* low-frequency rumble below the voice band. The accepted values are `80` (an
|
|
160
|
+
* 80 Hz second-order Butterworth high-pass) and `100` (a 100 Hz one); omit to
|
|
161
|
+
* leave the high-pass off (the default), which keeps the capture path
|
|
162
|
+
* full-range. Runs after denoise in the chain. The closed value set is
|
|
163
|
+
* designed to grow (further cutoffs are additive) the way `denoise` grows.
|
|
164
|
+
* Out-of-set values raise a `RangeError`.
|
|
165
|
+
* @default undefined
|
|
166
|
+
*/
|
|
167
|
+
highpass?: 80 | 100;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Automatic gain control target level in dBFS applied to the captured audio.
|
|
171
|
+
* Drives the running level toward this target with a smoothed, rate-limited
|
|
172
|
+
* gain. An integer in the range -40 to -3 (typical -18); omit to leave AGC
|
|
173
|
+
* off (the default), which keeps the level untouched. Runs after the
|
|
174
|
+
* high-pass step. Out-of-range values raise a `RangeError`.
|
|
175
|
+
* @default undefined
|
|
176
|
+
*/
|
|
177
|
+
agc?: number;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Peak limiter ceiling in dBFS (sample-peak) applied to the captured audio.
|
|
181
|
+
* Holds the signal at or below this ceiling, the safety net that catches a
|
|
182
|
+
* transient the AGC's gain would let exceed full scale. A number in the range
|
|
183
|
+
* -3.0 to 0.0 (typical -1.0); omit to leave the limiter off (the default),
|
|
184
|
+
* which keeps the level untouched. Runs last in the chain, after the AGC step.
|
|
185
|
+
* Out-of-range values raise a `RangeError`.
|
|
186
|
+
* @default undefined
|
|
187
|
+
*/
|
|
188
|
+
limiter?: number;
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
/**
|
|
@@ -158,6 +238,13 @@ export declare class Microphone extends Readable {
|
|
|
158
238
|
*/
|
|
159
239
|
readonly vadScore: number;
|
|
160
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Number of capture buffers dropped because the consumer could not keep pace.
|
|
243
|
+
* 0 while the consumer keeps up, or before capture starts. A rising value
|
|
244
|
+
* means audio is being dropped to bound memory.
|
|
245
|
+
*/
|
|
246
|
+
readonly overrunCount: number;
|
|
247
|
+
|
|
161
248
|
/** List all available audio input devices. */
|
|
162
249
|
static devices(): MicrophoneInfo[];
|
|
163
250
|
|
package/src/decibri.js
CHANGED
|
@@ -72,25 +72,6 @@ function resolveBundledOrtPath() {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// ─── RMS helper ──────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
function computeRMS(chunk, dtype) {
|
|
78
|
-
let sum = 0, n;
|
|
79
|
-
if (dtype === 'float32') {
|
|
80
|
-
const samples = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
|
|
81
|
-
n = samples.length;
|
|
82
|
-
for (let i = 0; i < n; i++) sum += samples[i] * samples[i];
|
|
83
|
-
} else {
|
|
84
|
-
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
|
|
85
|
-
n = samples.length;
|
|
86
|
-
for (let i = 0; i < n; i++) {
|
|
87
|
-
const s = samples[i] / 32768;
|
|
88
|
-
sum += s * s;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return n > 0 ? Math.sqrt(sum / n) : 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
75
|
// ─── Microphone (Readable) ──────────────────────────────────────────────────
|
|
95
76
|
|
|
96
77
|
class Microphone extends Readable {
|
|
@@ -111,15 +92,19 @@ class Microphone extends Readable {
|
|
|
111
92
|
|
|
112
93
|
// ── Store config ───────────────────────────────────────────────────────
|
|
113
94
|
|
|
114
|
-
this._dtype = prepared.dtype;
|
|
115
95
|
this._vad = prepared.vadEnabled;
|
|
116
|
-
this._vadMode = prepared.vadMode;
|
|
117
96
|
this._vadThreshold = prepared.vadThreshold;
|
|
118
97
|
this._vadHoldoff = prepared.vadHoldoff;
|
|
119
98
|
this._vadScore = 0;
|
|
120
99
|
this._isSpeaking = false;
|
|
121
100
|
this._silenceTimer = null;
|
|
122
101
|
this._started = false;
|
|
102
|
+
// Terminal once stop() runs: blocks _read() from restarting native capture
|
|
103
|
+
// while the end-of-stream push(null) is deferred past the flushed tail.
|
|
104
|
+
this._stopped = false;
|
|
105
|
+
// Set just before the deferred push(null); the data callback drops any
|
|
106
|
+
// straggler that arrives after end-of-stream rather than pushing past EOF.
|
|
107
|
+
this._ended = false;
|
|
123
108
|
|
|
124
109
|
// ── Create or adopt native bridge ───────────────────────────────────────
|
|
125
110
|
|
|
@@ -153,10 +138,17 @@ class Microphone extends Readable {
|
|
|
153
138
|
throw new RangeError('sample rate must be between 1000 and 384000');
|
|
154
139
|
}
|
|
155
140
|
|
|
141
|
+
// Mono only: the capture path delivers a single channel. A value below 1
|
|
142
|
+
// is a plain range error; a value above 1 is rejected as multichannel
|
|
143
|
+
// (not silently downmixed) so a later move to true multichannel stays
|
|
144
|
+
// additive. The `channels` option is kept for that forward compatibility.
|
|
156
145
|
const channels = options.channels ?? 1;
|
|
157
|
-
if (channels < 1
|
|
146
|
+
if (channels < 1) {
|
|
158
147
|
throw new RangeError('channels must be between 1 and 32');
|
|
159
148
|
}
|
|
149
|
+
if (channels > 1) {
|
|
150
|
+
throw new RangeError('multichannel capture is not supported; channels must be 1 (mono)');
|
|
151
|
+
}
|
|
160
152
|
|
|
161
153
|
const framesPerBuffer = options.framesPerBuffer ?? 1600;
|
|
162
154
|
if (framesPerBuffer < 64 || framesPerBuffer > 65536) {
|
|
@@ -198,13 +190,25 @@ class Microphone extends Readable {
|
|
|
198
190
|
|
|
199
191
|
// ── Validate VAD options ─────────────────────────────────────────────────
|
|
200
192
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// the
|
|
193
|
+
// vad selects the detector and (optionally) its threshold/holdoff policy.
|
|
194
|
+
// It accepts false (disabled, default), the 'silero'/'energy' shorthand
|
|
195
|
+
// (which uses the mode's default threshold and holdoff), or a config object
|
|
196
|
+
// { model, threshold, holdoffMs } to tune the policy. The legacy two-flag
|
|
197
|
+
// form (vad: true plus vadMode) and the flat vadThreshold/vadHoldoff
|
|
198
|
+
// options are rejected with a migration error. The threshold and holdoff
|
|
199
|
+
// live JS-side (the state machine runs in this wrapper); only the mode is
|
|
200
|
+
// passed to native.
|
|
201
|
+
if (options.vadThreshold !== undefined || options.vadHoldoff !== undefined) {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
'vadThreshold and vadHoldoff are no longer supported. ' +
|
|
204
|
+
"Pass them on the vad config object: vad: { model: 'silero', threshold: 0.5, holdoffMs: 300 }."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
205
207
|
const vad = options.vad ?? false;
|
|
206
208
|
let vadEnabled;
|
|
207
209
|
let vadMode;
|
|
210
|
+
let vadThreshold;
|
|
211
|
+
let vadHoldoff;
|
|
208
212
|
if (vad === false) {
|
|
209
213
|
vadEnabled = false;
|
|
210
214
|
vadMode = 'energy'; // inert placeholder; ignored while disabled
|
|
@@ -215,23 +219,123 @@ class Microphone extends Readable {
|
|
|
215
219
|
} else if (vad === 'silero' || vad === 'energy') {
|
|
216
220
|
vadEnabled = true;
|
|
217
221
|
vadMode = vad;
|
|
222
|
+
} else if (vad !== null && typeof vad === 'object' && !Array.isArray(vad)) {
|
|
223
|
+
// Config object form: { model, threshold?, holdoffMs? }. model is required
|
|
224
|
+
// and selects the detector; threshold and holdoffMs override the mode
|
|
225
|
+
// defaults when supplied.
|
|
226
|
+
const { model, threshold, holdoffMs } = vad;
|
|
227
|
+
if (model !== 'silero' && model !== 'energy') {
|
|
228
|
+
throw new TypeError(
|
|
229
|
+
`Invalid vad model: ${JSON.stringify(model)}. Expected 'silero' or 'energy'.`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
vadEnabled = true;
|
|
233
|
+
vadMode = model;
|
|
234
|
+
if (threshold !== undefined) {
|
|
235
|
+
if (typeof threshold !== 'number' || Number.isNaN(threshold)) {
|
|
236
|
+
throw new TypeError('vad threshold must be a number');
|
|
237
|
+
}
|
|
238
|
+
if (threshold < 0 || threshold > 1) {
|
|
239
|
+
throw new RangeError('vad threshold must be between 0 and 1');
|
|
240
|
+
}
|
|
241
|
+
vadThreshold = threshold;
|
|
242
|
+
}
|
|
243
|
+
if (holdoffMs !== undefined) {
|
|
244
|
+
if (typeof holdoffMs !== 'number' || Number.isNaN(holdoffMs)) {
|
|
245
|
+
throw new TypeError('vad holdoffMs must be a number');
|
|
246
|
+
}
|
|
247
|
+
if (holdoffMs < 0) {
|
|
248
|
+
throw new RangeError('vad holdoffMs must be non-negative');
|
|
249
|
+
}
|
|
250
|
+
vadHoldoff = holdoffMs;
|
|
251
|
+
}
|
|
218
252
|
} else {
|
|
219
253
|
throw new TypeError(
|
|
220
|
-
`Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero',
|
|
254
|
+
`Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero', 'energy', or a config object { model, threshold, holdoffMs }.`
|
|
221
255
|
);
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
let modelPath = undefined;
|
|
225
|
-
let ortLibraryPath = undefined;
|
|
226
259
|
if (vadEnabled && vadMode === 'silero') {
|
|
227
260
|
modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
|
|
228
261
|
if (!fs.existsSync(modelPath)) {
|
|
229
262
|
throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
|
|
230
263
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── DC removal ───────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
// A plain bool toggle for the one-pole DC-blocking high-pass, the first
|
|
269
|
+
// transform stage (before denoise). Absent or false leaves it off (the
|
|
270
|
+
// default), a byte-identical no-op. No range or closed-set check (it is just
|
|
271
|
+
// a bool); the value is threaded straight to the native config.
|
|
272
|
+
const dcRemoval = options.dcRemoval;
|
|
273
|
+
|
|
274
|
+
// ── Validate and resolve denoise ─────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
// Closed-set selector mirroring the Silero VAD shape: a model name resolves
|
|
277
|
+
// to a bundled ONNX file, absence leaves denoise off. The only accepted
|
|
278
|
+
// value is 'fastenhancer-t'; anything else is an explicit error rather than
|
|
279
|
+
// a silent miss. The bundled model file is resolved relative to the package
|
|
280
|
+
// exactly as the Silero model is.
|
|
281
|
+
const denoise = options.denoise;
|
|
282
|
+
let denoiseModelPath = undefined;
|
|
283
|
+
if (denoise !== undefined) {
|
|
284
|
+
if (denoise !== 'fastenhancer-t') {
|
|
285
|
+
throw new TypeError(
|
|
286
|
+
`Invalid denoise value: ${JSON.stringify(denoise)}. Expected 'fastenhancer-t'.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
denoiseModelPath = path.join(__dirname, '..', 'models', 'fastenhancer_t.onnx');
|
|
290
|
+
if (!fs.existsSync(denoiseModelPath)) {
|
|
291
|
+
throw new Error(`Denoise model not found at ${denoiseModelPath}. Ensure the models/ directory is included in your installation.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Validate high-pass ───────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
// Closed growable numeric cutoff selector mirroring the denoise shape: a
|
|
298
|
+
// cutoff in Hz selects a filter, absence leaves the high-pass off. The
|
|
299
|
+
// accepted values are 80 and 100; anything else (an out-of-set cutoff or a
|
|
300
|
+
// non-number) is an explicit RangeError rather than a silent miss, matching
|
|
301
|
+
// the numeric range checks on agc and limiter. The filter is pure DSP with
|
|
302
|
+
// no bundled file, so there is nothing to resolve here, only the closed-set
|
|
303
|
+
// check.
|
|
304
|
+
const highpass = options.highpass;
|
|
305
|
+
if (highpass !== undefined && highpass !== 80 && highpass !== 100) {
|
|
306
|
+
throw new RangeError('highpass must be one of: 80, 100');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Validate AGC ─────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
// AGC target level in dBFS: a number in [-40, -3] (typical -18) drives the
|
|
312
|
+
// captured level toward the target; absence leaves it off. Mirrors the
|
|
313
|
+
// sample-rate range check, a RangeError on an out-of-range numeric value;
|
|
314
|
+
// the native backstop and the Rust core guard the same range.
|
|
315
|
+
const agc = options.agc;
|
|
316
|
+
if (agc !== undefined && (agc < -40 || agc > -3)) {
|
|
317
|
+
throw new RangeError('agc target level must be between -40 and -3');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Validate limiter ─────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
// Sample-peak ceiling in dBFS: a number in [-3.0, 0.0] (typical -1.0) holds
|
|
323
|
+
// the captured signal at or below the ceiling, catching a peak the AGC would
|
|
324
|
+
// let through; absence leaves it off. Mirrors the agc range check, a
|
|
325
|
+
// RangeError on an out-of-range numeric value; the native backstop and the
|
|
326
|
+
// Rust core guard the same range.
|
|
327
|
+
const limiter = options.limiter;
|
|
328
|
+
if (limiter !== undefined && (limiter < -3.0 || limiter > 0.0)) {
|
|
329
|
+
throw new RangeError('limiter ceiling must be between -3.0 and 0.0');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Internal plumbing: inject the bundled ORT dylib path into the napi
|
|
333
|
+
// constructor whenever an ONNX stage loads (Silero VAD or denoise). If
|
|
334
|
+
// resolution fails (unknown platform, platform package not installed), this
|
|
335
|
+
// is left undefined and Rust falls through to ORT_DYLIB_PATH or surfaces a
|
|
336
|
+
// decibri-specific init error.
|
|
337
|
+
let ortLibraryPath = undefined;
|
|
338
|
+
if ((vadEnabled && vadMode === 'silero') || denoise !== undefined) {
|
|
235
339
|
ortLibraryPath = resolveBundledOrtPath();
|
|
236
340
|
}
|
|
237
341
|
|
|
@@ -239,17 +343,27 @@ class Microphone extends Readable {
|
|
|
239
343
|
dtype,
|
|
240
344
|
vadEnabled,
|
|
241
345
|
vadMode,
|
|
242
|
-
vadThreshold:
|
|
243
|
-
vadHoldoff:
|
|
346
|
+
vadThreshold: vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
|
|
347
|
+
vadHoldoff: vadHoldoff ?? 300,
|
|
244
348
|
nativeOptions: {
|
|
245
349
|
sampleRate,
|
|
246
350
|
channels,
|
|
247
351
|
framesPerBuffer,
|
|
248
352
|
format: dtype,
|
|
249
353
|
device: resolvedDevice,
|
|
250
|
-
|
|
354
|
+
// Pass the mode to native only when VAD is enabled. When disabled the
|
|
355
|
+
// local vadMode is an inert 'energy' placeholder; sending it would make
|
|
356
|
+
// native compute the energy score for a microphone that did not ask for
|
|
357
|
+
// VAD. Absent means VAD off in native.
|
|
358
|
+
vadMode: vadEnabled ? vadMode : undefined,
|
|
251
359
|
modelPath,
|
|
360
|
+
dcRemoval,
|
|
361
|
+
denoise,
|
|
362
|
+
denoiseModelPath,
|
|
252
363
|
ortLibraryPath,
|
|
364
|
+
highpass,
|
|
365
|
+
agc,
|
|
366
|
+
limiter,
|
|
253
367
|
},
|
|
254
368
|
};
|
|
255
369
|
}
|
|
@@ -287,16 +401,31 @@ class Microphone extends Readable {
|
|
|
287
401
|
|
|
288
402
|
/** @internal */
|
|
289
403
|
_read() {
|
|
290
|
-
|
|
404
|
+
// Never (re)start native capture once stopped: stop() defers the
|
|
405
|
+
// end-of-stream push(null), and a _read() in that window would otherwise
|
|
406
|
+
// reopen the device.
|
|
407
|
+
if (this._started || this._stopped) return;
|
|
291
408
|
this._started = true;
|
|
292
409
|
|
|
293
410
|
this._native.start((err, chunk) => {
|
|
294
411
|
if (err) {
|
|
295
412
|
this._started = false;
|
|
413
|
+
// A start()-time failure is delivered here as the raw native error (not
|
|
414
|
+
// via wrapNativeError), so no decibri `code` is attached on the 'error'
|
|
415
|
+
// event, as with device-open failures; the dedicated MODEL_LOAD_FAILED
|
|
416
|
+
// code is reachable through wrapNativeError, not on this streaming path.
|
|
296
417
|
this.destroy(err);
|
|
297
418
|
return;
|
|
298
419
|
}
|
|
299
420
|
|
|
421
|
+
// On close the native pump flushes the buffered tail; that final data
|
|
422
|
+
// callback can run after stop() has begun ending the stream. Once the
|
|
423
|
+
// end-of-stream push(null) has been issued (or the stream was destroyed),
|
|
424
|
+
// drop any straggler rather than pushing past EOF.
|
|
425
|
+
if (this._ended || this.destroyed) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
300
429
|
// push returns false when the consumer is slow. We can't pause a mic,
|
|
301
430
|
// but we surface the backpressure warning so callers can react.
|
|
302
431
|
if (!this.push(chunk)) {
|
|
@@ -304,27 +433,15 @@ class Microphone extends Readable {
|
|
|
304
433
|
}
|
|
305
434
|
|
|
306
435
|
if (this._vad) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
436
|
+
// Both modes read the score from native: the Silero speech probability
|
|
437
|
+
// or, in energy mode, the RMS of the pre-enhancement signal. The native
|
|
438
|
+
// pump computes both on the signal before the opt-in enhancement step,
|
|
439
|
+
// so enabling enhancement does not change detection in either mode.
|
|
440
|
+
this._processVadValue(this._native.vadProbability);
|
|
313
441
|
}
|
|
314
442
|
});
|
|
315
443
|
}
|
|
316
444
|
|
|
317
|
-
/** @internal Energy-based VAD (RMS threshold) */
|
|
318
|
-
_processVadEnergy(chunk) {
|
|
319
|
-
const rms = computeRMS(chunk, this._dtype);
|
|
320
|
-
this._processVadValue(rms);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** @internal Silero ML-based VAD (reads probability from native) */
|
|
324
|
-
_processVadSilero(probability) {
|
|
325
|
-
this._processVadValue(probability);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
445
|
/** @internal Common speech/silence state machine */
|
|
329
446
|
_processVadValue(value) {
|
|
330
447
|
this._vadScore = value;
|
|
@@ -350,10 +467,23 @@ class Microphone extends Readable {
|
|
|
350
467
|
stop() {
|
|
351
468
|
if (!this._started) return;
|
|
352
469
|
this._started = false;
|
|
470
|
+
this._stopped = true;
|
|
353
471
|
this._native.stop();
|
|
354
472
|
clearTimeout(this._silenceTimer);
|
|
355
473
|
this._silenceTimer = null;
|
|
356
|
-
|
|
474
|
+
// The native pump flushes any buffered tail on close; those final data
|
|
475
|
+
// callbacks are queued on the event loop and run before a setImmediate
|
|
476
|
+
// scheduled now. Defer the end-of-stream push(null) past them so the final
|
|
477
|
+
// short chunk reaches consumers without landing after EOF
|
|
478
|
+
// (ERR_STREAM_PUSH_AFTER_EOF). `_ended` is set first so the data callback
|
|
479
|
+
// drops any straggler that somehow arrives after this point.
|
|
480
|
+
setImmediate(() => {
|
|
481
|
+
// A device error in the same tick may have destroyed the stream before
|
|
482
|
+
// this runs; don't end an already-destroyed stream.
|
|
483
|
+
if (this.destroyed) return;
|
|
484
|
+
this._ended = true;
|
|
485
|
+
this.push(null); // signals stream end
|
|
486
|
+
});
|
|
357
487
|
}
|
|
358
488
|
|
|
359
489
|
/**
|
|
@@ -366,7 +496,9 @@ class Microphone extends Readable {
|
|
|
366
496
|
|
|
367
497
|
/**
|
|
368
498
|
* Most recent VAD score for the active mode: the Silero speech probability
|
|
369
|
-
* in 'silero' mode, the normalized RMS of the
|
|
499
|
+
* in 'silero' mode, the normalized RMS of the pre-enhancement signal in
|
|
500
|
+
* 'energy' mode. Both are computed natively on the signal before any opt-in
|
|
501
|
+
* enhancement step, so enabling enhancement does not change the score.
|
|
370
502
|
* 0 when VAD is disabled or before the first chunk is processed.
|
|
371
503
|
* @returns {number}
|
|
372
504
|
*/
|
|
@@ -374,6 +506,16 @@ class Microphone extends Readable {
|
|
|
374
506
|
return this._vadScore;
|
|
375
507
|
}
|
|
376
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Number of capture buffers dropped because the consumer could not keep
|
|
511
|
+
* pace. 0 while the consumer keeps up, or before capture starts. A rising
|
|
512
|
+
* value means audio is being dropped to bound memory.
|
|
513
|
+
* @returns {number}
|
|
514
|
+
*/
|
|
515
|
+
get overrunCount() {
|
|
516
|
+
return this._native.overrunCount;
|
|
517
|
+
}
|
|
518
|
+
|
|
377
519
|
/**
|
|
378
520
|
* List all available input devices on the system.
|
|
379
521
|
* @returns {Array<{index: number, name: string, id: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
package/src/errors.js
CHANGED
|
@@ -72,6 +72,7 @@ const DEVICE_CODES = [
|
|
|
72
72
|
const ORT_CODES = [
|
|
73
73
|
['decibri: failed to initialize ONNX Runtime', 'ORT_INIT_FAILED'],
|
|
74
74
|
['Failed to load Silero VAD model from', 'VAD_MODEL_LOAD_FAILED'],
|
|
75
|
+
['Failed to load model from', 'MODEL_LOAD_FAILED'],
|
|
75
76
|
['Failed to create ort session builder', 'ORT_SESSION_BUILD_FAILED'],
|
|
76
77
|
['Failed to set ort threads', 'ORT_THREADS_CONFIG_FAILED'],
|
|
77
78
|
['Silero VAD inference failed', 'ORT_INFERENCE_FAILED'],
|
|
@@ -130,6 +131,19 @@ function wrapNativeError(err) {
|
|
|
130
131
|
return new OrtError(msg, 'ORT_TENSOR_EXTRACT_FAILED');
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
// Runtime device/driver failure during streaming. Distinct from the
|
|
135
|
+
// enumeration/selection DeviceError family above (which mirrors the core's
|
|
136
|
+
// "Device errors"): this is the core's "Stream errors" DeviceFailed, surfaced
|
|
137
|
+
// as a base DecibriError with a dedicated code.
|
|
138
|
+
if (msg.startsWith('decibri: audio device error:')) {
|
|
139
|
+
return new DecibriError(msg, 'DEVICE_FAILED');
|
|
140
|
+
}
|
|
141
|
+
// Non-ORT ONNX backend failure: the reserved backend catch-all, surfaced as a
|
|
142
|
+
// base DecibriError with a dedicated code (not an OrtError).
|
|
143
|
+
if (msg.startsWith('ONNX backend error from')) {
|
|
144
|
+
return new DecibriError(msg, 'ONNX_BACKEND_FAILED');
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
// Any other error from the native constructor is still a decibri error.
|
|
134
148
|
return new DecibriError(msg, 'DECIBRI_ERROR');
|
|
135
149
|
}
|