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/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 mode. One of:
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
- * When enabled, emits `'speech'` and `'silence'` events and updates `vadScore`.
85
- * The legacy `vad: true` form is rejected; specify the mode explicitly.
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
- * Speech-detection threshold for the active VAD mode.
92
- * @default 0.5 for `'silero'`, 0.01 for `'energy'`
93
- * @range 0–1
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
- vadThreshold?: number;
131
+ modelPath?: string;
96
132
 
97
133
  /**
98
- * Milliseconds of sub-threshold audio before emitting `'silence'`.
99
- * @default 300
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
- vadHoldoff?: number;
142
+ dcRemoval?: boolean;
102
143
 
103
144
  /**
104
- * Path to the Silero VAD ONNX model file.
105
- * Only used when `vad` is `'silero'`.
106
- * Defaults to `models/silero_vad.onnx` relative to the package.
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
- modelPath?: string;
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 || channels > 32) {
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
- // Single vad union: false (disabled, default), 'silero', or 'energy'. The
202
- // legacy two-flag form (vad: true plus vadMode) is rejected with a
203
- // migration error. Energy and Silero are both computed in this wrapper;
204
- // the union only selects which.
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', or 'energy'.`
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
- // Internal plumbing: inject the bundled ORT dylib path into the napi
232
- // constructor. If resolution fails (unknown platform, platform package
233
- // not installed), leaves ortLibraryPath as undefined and lets Rust fall
234
- // through to ORT_DYLIB_PATH or surface a decibri-specific init error.
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: options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
243
- vadHoldoff: options.vadHoldoff ?? 300,
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
- vadMode,
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
- if (this._started) return;
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
- if (this._vadMode === 'silero') {
308
- const prob = this._native.vadProbability;
309
- this._processVadSilero(prob);
310
- } else {
311
- this._processVadEnergy(chunk);
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
- this.push(null); // signals stream end
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 last chunk in 'energy' mode.
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
  }