decibri 3.4.1 → 4.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 +386 -0
- package/MIGRATION.md +151 -0
- package/README.md +206 -112
- package/examples/wav-capture.js +2 -2
- package/examples/websocket-stream.js +2 -2
- package/index.d.ts +1 -1
- package/index.js +52 -52
- package/package.json +9 -7
- package/src/browser/decibri-browser.js +37 -11
- package/src/browser/index.d.ts +28 -15
- package/src/browser/index.js +2 -2
- package/src/decibri-output.js +24 -29
- package/src/decibri.d.ts +81 -47
- package/src/decibri.js +106 -41
- package/src/errors.js +126 -23
package/src/decibri-output.js
CHANGED
|
@@ -4,11 +4,15 @@ const { Writable } = require('stream');
|
|
|
4
4
|
const { DecibriOutputBridge } = require('../index.js');
|
|
5
5
|
const { wrapNativeError } = require('./errors');
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// The npm package version, reported as `binding` by version(). Read from
|
|
8
|
+
// package.json so it tracks the published package and cannot drift.
|
|
9
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
// ─── Speaker (Writable) ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
class Speaker extends Writable {
|
|
10
14
|
/**
|
|
11
|
-
* @param {import('./decibri').
|
|
15
|
+
* @param {import('./decibri').SpeakerOptions} [options]
|
|
12
16
|
*/
|
|
13
17
|
constructor(options = {}) {
|
|
14
18
|
super({ highWaterMark: options.highWaterMark || 16384 });
|
|
@@ -25,33 +29,23 @@ class DecibriOutput extends Writable {
|
|
|
25
29
|
throw new RangeError('channels must be between 1 and 32');
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
throw new TypeError("
|
|
32
|
+
const dtype = options.dtype ?? 'int16';
|
|
33
|
+
if (dtype !== 'int16' && dtype !== 'float32') {
|
|
34
|
+
throw new TypeError("dtype must be 'int16' or 'float32'");
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
// ── Resolve device ──────────────────────────────────────────────────────
|
|
34
38
|
|
|
39
|
+
// Name and multi-match resolution are delegated to the core, which owns
|
|
40
|
+
// the renamed-vocabulary errors (SpeakerNotFound / MultipleDevicesMatch).
|
|
41
|
+
// A string name and an { id } object are passed straight through to the
|
|
42
|
+
// native addon. Only the numeric index keeps a client-side bounds check,
|
|
43
|
+
// for a clean Node-side RangeError without a round-trip.
|
|
35
44
|
let resolvedDevice = options.device;
|
|
36
|
-
if (typeof options.device === '
|
|
37
|
-
const lower = options.device.toLowerCase();
|
|
38
|
-
const matches = DecibriOutputBridge.devices().filter(d =>
|
|
39
|
-
d.name.toLowerCase().includes(lower)
|
|
40
|
-
);
|
|
41
|
-
if (matches.length === 0) {
|
|
42
|
-
throw new TypeError(`No audio output device found matching "${options.device}"`);
|
|
43
|
-
}
|
|
44
|
-
if (matches.length > 1) {
|
|
45
|
-
const names = matches.map(d => ` [${d.index}] ${d.name}`).join('\n');
|
|
46
|
-
throw new TypeError(
|
|
47
|
-
`Multiple devices match "${options.device}":\n${names}\nUse a more specific name or pass the device index directly.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
resolvedDevice = matches[0].index;
|
|
51
|
-
} else if (typeof options.device === 'number') {
|
|
45
|
+
if (typeof options.device === 'number') {
|
|
52
46
|
const devices = DecibriOutputBridge.devices();
|
|
53
47
|
if (options.device < 0 || options.device >= devices.length) {
|
|
54
|
-
throw new RangeError('device index out of range. Call
|
|
48
|
+
throw new RangeError('device index out of range. Call Speaker.devices() to list available devices');
|
|
55
49
|
}
|
|
56
50
|
resolvedDevice = options.device;
|
|
57
51
|
} else if (
|
|
@@ -69,7 +63,7 @@ class DecibriOutput extends Writable {
|
|
|
69
63
|
|
|
70
64
|
// ── Store config ───────────────────────────────────────────────────────
|
|
71
65
|
|
|
72
|
-
this.
|
|
66
|
+
this._dtype = dtype;
|
|
73
67
|
this._started = false;
|
|
74
68
|
|
|
75
69
|
// ── Create native bridge ───────────────────────────────────────────────
|
|
@@ -78,7 +72,7 @@ class DecibriOutput extends Writable {
|
|
|
78
72
|
this._native = new DecibriOutputBridge({
|
|
79
73
|
sampleRate,
|
|
80
74
|
channels,
|
|
81
|
-
format,
|
|
75
|
+
format: dtype,
|
|
82
76
|
device: resolvedDevice,
|
|
83
77
|
});
|
|
84
78
|
} catch (err) {
|
|
@@ -134,12 +128,13 @@ class DecibriOutput extends Writable {
|
|
|
134
128
|
}
|
|
135
129
|
|
|
136
130
|
/**
|
|
137
|
-
* Version information for decibri
|
|
138
|
-
* @returns {{ decibri: string,
|
|
131
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
132
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
139
133
|
*/
|
|
140
134
|
static version() {
|
|
141
|
-
|
|
135
|
+
const v = DecibriOutputBridge.version();
|
|
136
|
+
return { decibri: v.decibri, audioBackend: v.audioBackend, binding: PACKAGE_VERSION };
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
module.exports =
|
|
140
|
+
module.exports = Speaker;
|
package/src/decibri.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Readable, ReadableOptions, Writable, WritableOptions } from 'stream';
|
|
2
2
|
|
|
3
3
|
/** Information about an available audio input device. */
|
|
4
|
-
export interface
|
|
4
|
+
export interface MicrophoneInfo {
|
|
5
5
|
/** Device index (pass to constructor as `device`). */
|
|
6
6
|
index: number;
|
|
7
7
|
/** Human-readable device name from the OS. */
|
|
@@ -23,16 +23,18 @@ export interface DeviceInfo {
|
|
|
23
23
|
isDefault: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** Version strings returned by `
|
|
26
|
+
/** Version strings returned by `Microphone.version()`. */
|
|
27
27
|
export interface VersionInfo {
|
|
28
|
-
/** decibri
|
|
28
|
+
/** decibri core version. */
|
|
29
29
|
decibri: string;
|
|
30
|
-
/** Audio
|
|
31
|
-
|
|
30
|
+
/** Audio backend version string (e.g. `"cpal 0.17"`). */
|
|
31
|
+
audioBackend: string;
|
|
32
|
+
/** This binding's npm package version. */
|
|
33
|
+
binding: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
/** Constructor options for `
|
|
35
|
-
export interface
|
|
36
|
+
/** Constructor options for `Microphone`. */
|
|
37
|
+
export interface MicrophoneOptions extends ReadableOptions {
|
|
36
38
|
/**
|
|
37
39
|
* Sample rate in Hz.
|
|
38
40
|
* @default 16000
|
|
@@ -57,53 +59,50 @@ export interface DecibriOptions extends ReadableOptions {
|
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Audio input device. One of:
|
|
60
|
-
* - numeric index (from `
|
|
62
|
+
* - numeric index (from `MicrophoneInfo.index`)
|
|
61
63
|
* - case-insensitive name substring
|
|
62
|
-
* - `{ id: string }` for stable per-host device ID from `
|
|
64
|
+
* - `{ id: string }` for stable per-host device ID from `MicrophoneInfo.id`
|
|
63
65
|
*
|
|
64
66
|
* Omit to use the system default input device.
|
|
65
67
|
*/
|
|
66
68
|
device?: number | string | { id: string };
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Sample encoding
|
|
71
|
+
* Sample encoding data type.
|
|
70
72
|
* - `'int16'`: 16-bit signed integer, little-endian (2 bytes per sample)
|
|
71
73
|
* - `'float32'`: 32-bit IEEE 754 float, little-endian (4 bytes per sample)
|
|
72
74
|
* @default 'int16'
|
|
73
75
|
*/
|
|
74
|
-
|
|
76
|
+
dtype?: 'int16' | 'float32';
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
+
* Voice activity detection mode. One of:
|
|
80
|
+
* - `false`: disabled (default)
|
|
81
|
+
* - `'silero'`: Silero VAD v5 ML model (more accurate, ~1ms inference)
|
|
82
|
+
* - `'energy'`: RMS energy threshold (lightweight)
|
|
83
|
+
*
|
|
84
|
+
* When enabled, emits `'speech'` and `'silence'` events and updates `vadScore`.
|
|
85
|
+
* The legacy `vad: true` form is rejected; specify the mode explicitly.
|
|
79
86
|
* @default false
|
|
80
87
|
*/
|
|
81
|
-
vad?:
|
|
88
|
+
vad?: false | 'silero' | 'energy';
|
|
82
89
|
|
|
83
90
|
/**
|
|
84
|
-
*
|
|
85
|
-
* @default 0.01
|
|
91
|
+
* Speech-detection threshold for the active VAD mode.
|
|
92
|
+
* @default 0.5 for `'silero'`, 0.01 for `'energy'`
|
|
86
93
|
* @range 0–1
|
|
87
94
|
*/
|
|
88
95
|
vadThreshold?: number;
|
|
89
96
|
|
|
90
97
|
/**
|
|
91
|
-
* Milliseconds of sub-threshold audio before emitting `'silence'
|
|
98
|
+
* Milliseconds of sub-threshold audio before emitting `'silence'`.
|
|
92
99
|
* @default 300
|
|
93
100
|
*/
|
|
94
101
|
vadHoldoff?: number;
|
|
95
102
|
|
|
96
|
-
/**
|
|
97
|
-
* VAD engine to use.
|
|
98
|
-
* - `'energy'`: RMS energy threshold (default, lightweight)
|
|
99
|
-
* - `'silero'`: Silero VAD v5 ML model (more accurate, ~1ms inference)
|
|
100
|
-
* @default 'energy'
|
|
101
|
-
*/
|
|
102
|
-
vadMode?: 'energy' | 'silero';
|
|
103
|
-
|
|
104
103
|
/**
|
|
105
104
|
* Path to the Silero VAD ONNX model file.
|
|
106
|
-
* Only used when `
|
|
105
|
+
* Only used when `vad` is `'silero'`.
|
|
107
106
|
* Defaults to `models/silero_vad.onnx` relative to the package.
|
|
108
107
|
*/
|
|
109
108
|
modelPath?: string;
|
|
@@ -114,16 +113,16 @@ export interface DecibriOptions extends ReadableOptions {
|
|
|
114
113
|
*
|
|
115
114
|
* @example
|
|
116
115
|
* ```js
|
|
117
|
-
* const
|
|
118
|
-
* const mic = new
|
|
116
|
+
* const { Microphone } = require('decibri');
|
|
117
|
+
* const mic = new Microphone({ sampleRate: 16000, channels: 1 });
|
|
119
118
|
* mic.on('data', (chunk) => {
|
|
120
119
|
* // chunk is a Buffer of Int16 LE PCM samples
|
|
121
120
|
* });
|
|
122
121
|
* setTimeout(() => mic.stop(), 5000);
|
|
123
122
|
* ```
|
|
124
123
|
*/
|
|
125
|
-
declare class
|
|
126
|
-
constructor(options?:
|
|
124
|
+
export declare class Microphone extends Readable {
|
|
125
|
+
constructor(options?: MicrophoneOptions);
|
|
127
126
|
|
|
128
127
|
/** Stop microphone capture and end the stream. Safe to call multiple times. */
|
|
129
128
|
stop(): void;
|
|
@@ -131,8 +130,15 @@ declare class Decibri extends Readable {
|
|
|
131
130
|
/** Whether the microphone is currently capturing audio. */
|
|
132
131
|
readonly isOpen: boolean;
|
|
133
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Most recent VAD score for the active mode: the Silero speech probability in
|
|
135
|
+
* `'silero'` mode, the normalized RMS of the last chunk in `'energy'` mode.
|
|
136
|
+
* 0 when VAD is disabled or before the first chunk is processed.
|
|
137
|
+
*/
|
|
138
|
+
readonly vadScore: number;
|
|
139
|
+
|
|
134
140
|
/** List all available audio input devices. */
|
|
135
|
-
static devices():
|
|
141
|
+
static devices(): MicrophoneInfo[];
|
|
136
142
|
|
|
137
143
|
/** Version information for decibri and the audio runtime. */
|
|
138
144
|
static version(): VersionInfo;
|
|
@@ -162,13 +168,13 @@ declare class Decibri extends Readable {
|
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
/** Information about an available audio output device. */
|
|
165
|
-
export interface
|
|
171
|
+
export interface SpeakerInfo {
|
|
166
172
|
/** Device index (pass to constructor as `device`). */
|
|
167
173
|
index: number;
|
|
168
174
|
/** Human-readable device name from the OS. */
|
|
169
175
|
name: string;
|
|
170
176
|
/**
|
|
171
|
-
* Stable per-host device ID. See `
|
|
177
|
+
* Stable per-host device ID. See `MicrophoneInfo.id` for format and fallback
|
|
172
178
|
* semantics; identical rules for output devices.
|
|
173
179
|
*/
|
|
174
180
|
id: string;
|
|
@@ -180,8 +186,8 @@ export interface OutputDeviceInfo {
|
|
|
180
186
|
isDefault: boolean;
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
/** Constructor options for `
|
|
184
|
-
export interface
|
|
189
|
+
/** Constructor options for `Speaker`. */
|
|
190
|
+
export interface SpeakerOptions extends WritableOptions {
|
|
185
191
|
/**
|
|
186
192
|
* Sample rate in Hz.
|
|
187
193
|
* @default 16000
|
|
@@ -197,18 +203,18 @@ export interface DecibriOutputOptions extends WritableOptions {
|
|
|
197
203
|
channels?: number;
|
|
198
204
|
|
|
199
205
|
/**
|
|
200
|
-
* Sample encoding
|
|
206
|
+
* Sample encoding data type of incoming data.
|
|
201
207
|
* - `'int16'`: 16-bit signed integer, little-endian (2 bytes per sample)
|
|
202
208
|
* - `'float32'`: 32-bit IEEE 754 float, little-endian (4 bytes per sample)
|
|
203
209
|
* @default 'int16'
|
|
204
210
|
*/
|
|
205
|
-
|
|
211
|
+
dtype?: 'int16' | 'float32';
|
|
206
212
|
|
|
207
213
|
/**
|
|
208
214
|
* Audio output device. One of:
|
|
209
|
-
* - numeric index (from `
|
|
215
|
+
* - numeric index (from `SpeakerInfo.index`)
|
|
210
216
|
* - case-insensitive name substring
|
|
211
|
-
* - `{ id: string }` for stable per-host device ID from `
|
|
217
|
+
* - `{ id: string }` for stable per-host device ID from `SpeakerInfo.id`
|
|
212
218
|
*
|
|
213
219
|
* Omit to use the system default output device.
|
|
214
220
|
*/
|
|
@@ -220,14 +226,14 @@ export interface DecibriOutputOptions extends WritableOptions {
|
|
|
220
226
|
*
|
|
221
227
|
* @example
|
|
222
228
|
* ```js
|
|
223
|
-
* const {
|
|
224
|
-
* const speaker = new
|
|
229
|
+
* const { Speaker } = require('decibri');
|
|
230
|
+
* const speaker = new Speaker({ sampleRate: 16000, channels: 1 });
|
|
225
231
|
* speaker.write(pcmBuffer);
|
|
226
232
|
* speaker.end();
|
|
227
233
|
* ```
|
|
228
234
|
*/
|
|
229
|
-
declare class
|
|
230
|
-
constructor(options?:
|
|
235
|
+
export declare class Speaker extends Writable {
|
|
236
|
+
constructor(options?: SpeakerOptions);
|
|
231
237
|
|
|
232
238
|
/** Immediate stop. Discards remaining buffered audio. */
|
|
233
239
|
stop(): void;
|
|
@@ -236,7 +242,7 @@ declare class DecibriOutput extends Writable {
|
|
|
236
242
|
readonly isPlaying: boolean;
|
|
237
243
|
|
|
238
244
|
/** List all available audio output devices. */
|
|
239
|
-
static devices():
|
|
245
|
+
static devices(): SpeakerInfo[];
|
|
240
246
|
|
|
241
247
|
/** Version information for decibri and the audio runtime. */
|
|
242
248
|
static version(): VersionInfo;
|
|
@@ -252,8 +258,36 @@ declare class DecibriOutput extends Writable {
|
|
|
252
258
|
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
|
|
261
|
+
/** List all available audio input devices. */
|
|
262
|
+
export declare function inputDevices(): MicrophoneInfo[];
|
|
263
|
+
|
|
264
|
+
/** List all available audio output devices. */
|
|
265
|
+
export declare function outputDevices(): SpeakerInfo[];
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
/** Version information for decibri and the audio runtime. */
|
|
268
|
+
export declare function version(): VersionInfo;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Base class for errors raised by the decibri native bindings.
|
|
272
|
+
*
|
|
273
|
+
* Catch this to handle any decibri device or ONNX Runtime failure generically;
|
|
274
|
+
* catch a subclass for finer control. Argument validation (bad sample rate,
|
|
275
|
+
* channels, frames, dtype, vad) throws built-in `RangeError` / `TypeError`, not
|
|
276
|
+
* a `DecibriError`.
|
|
277
|
+
*/
|
|
278
|
+
export declare class DecibriError extends Error {
|
|
279
|
+
/** Stable string code identifying the specific failure. */
|
|
280
|
+
readonly code: string;
|
|
259
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Device enumeration or selection failure: an unmatched device name, an
|
|
285
|
+
* ambiguous name match, or missing hardware.
|
|
286
|
+
*/
|
|
287
|
+
export declare class DeviceError extends DecibriError {}
|
|
288
|
+
|
|
289
|
+
/** ONNX Runtime setup or inference failure (Silero VAD). */
|
|
290
|
+
export declare class OrtError extends DecibriError {}
|
|
291
|
+
|
|
292
|
+
/** A specific ONNX Runtime library path could not be loaded. */
|
|
293
|
+
export declare class OrtPathError extends OrtError {}
|
package/src/decibri.js
CHANGED
|
@@ -4,7 +4,18 @@ const { Readable } = require('stream');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const { DecibriBridge } = require('../index.js');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
wrapNativeError,
|
|
9
|
+
DecibriError,
|
|
10
|
+
DeviceError,
|
|
11
|
+
OrtError,
|
|
12
|
+
OrtPathError,
|
|
13
|
+
} = require('./errors');
|
|
14
|
+
|
|
15
|
+
// The npm package version, reported as `binding` by version(). Read from
|
|
16
|
+
// package.json so it tracks the published package and cannot drift from a
|
|
17
|
+
// hardcoded string.
|
|
18
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
8
19
|
|
|
9
20
|
// ─── Bundled ONNX Runtime path resolution ────────────────────────────────────
|
|
10
21
|
|
|
@@ -63,9 +74,9 @@ function resolveBundledOrtPath() {
|
|
|
63
74
|
|
|
64
75
|
// ─── RMS helper ──────────────────────────────────────────────────────────────
|
|
65
76
|
|
|
66
|
-
function computeRMS(chunk,
|
|
77
|
+
function computeRMS(chunk, dtype) {
|
|
67
78
|
let sum = 0, n;
|
|
68
|
-
if (
|
|
79
|
+
if (dtype === 'float32') {
|
|
69
80
|
const samples = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
|
|
70
81
|
n = samples.length;
|
|
71
82
|
for (let i = 0; i < n; i++) sum += samples[i] * samples[i];
|
|
@@ -80,11 +91,11 @@ function computeRMS(chunk, format) {
|
|
|
80
91
|
return n > 0 ? Math.sqrt(sum / n) : 0;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
// ───
|
|
94
|
+
// ─── Microphone (Readable) ──────────────────────────────────────────────────
|
|
84
95
|
|
|
85
|
-
class
|
|
96
|
+
class Microphone extends Readable {
|
|
86
97
|
/**
|
|
87
|
-
* @param {import('./decibri').
|
|
98
|
+
* @param {import('./decibri').MicrophoneOptions} [options]
|
|
88
99
|
*/
|
|
89
100
|
constructor(options = {}) {
|
|
90
101
|
super({ highWaterMark: options.highWaterMark, objectMode: false });
|
|
@@ -106,33 +117,23 @@ class Decibri extends Readable {
|
|
|
106
117
|
throw new RangeError('frames per buffer must be between 64 and 65536');
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
throw new TypeError("
|
|
120
|
+
const dtype = options.dtype ?? 'int16';
|
|
121
|
+
if (dtype !== 'int16' && dtype !== 'float32') {
|
|
122
|
+
throw new TypeError("dtype must be 'int16' or 'float32'");
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
// ── Resolve device ──────────────────────────────────────────────────────
|
|
115
126
|
|
|
127
|
+
// Name and multi-match resolution are delegated to the core, which owns
|
|
128
|
+
// the renamed-vocabulary errors (MicrophoneNotFound / MultipleDevicesMatch).
|
|
129
|
+
// A string name and an { id } object are passed straight through to the
|
|
130
|
+
// native addon. Only the numeric index keeps a client-side bounds check,
|
|
131
|
+
// for a clean Node-side RangeError without a round-trip.
|
|
116
132
|
let resolvedDevice = options.device;
|
|
117
|
-
if (typeof options.device === '
|
|
118
|
-
const lower = options.device.toLowerCase();
|
|
119
|
-
const matches = DecibriBridge.devices().filter(d =>
|
|
120
|
-
d.name.toLowerCase().includes(lower)
|
|
121
|
-
);
|
|
122
|
-
if (matches.length === 0) {
|
|
123
|
-
throw new TypeError(`No audio input device found matching "${options.device}"`);
|
|
124
|
-
}
|
|
125
|
-
if (matches.length > 1) {
|
|
126
|
-
const names = matches.map(d => ` [${d.index}] ${d.name}`).join('\n');
|
|
127
|
-
throw new TypeError(
|
|
128
|
-
`Multiple devices match "${options.device}":\n${names}\nUse a more specific name or pass the device index directly.`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
resolvedDevice = matches[0].index;
|
|
132
|
-
} else if (typeof options.device === 'number') {
|
|
133
|
+
if (typeof options.device === 'number') {
|
|
133
134
|
const devices = DecibriBridge.devices();
|
|
134
135
|
if (options.device < 0 || options.device >= devices.length) {
|
|
135
|
-
throw new RangeError('device index out of range. Call
|
|
136
|
+
throw new RangeError('device index out of range. Call Microphone.devices() to list available devices');
|
|
136
137
|
}
|
|
137
138
|
resolvedDevice = options.device;
|
|
138
139
|
} else if (
|
|
@@ -151,14 +152,32 @@ class Decibri extends Readable {
|
|
|
151
152
|
|
|
152
153
|
// ── Validate VAD options ─────────────────────────────────────────────────
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Single vad union: false (disabled, default), 'silero', or 'energy'. The
|
|
156
|
+
// legacy two-flag form (vad: true plus vadMode) is rejected with a
|
|
157
|
+
// migration error. Energy and Silero are both computed in this wrapper;
|
|
158
|
+
// the union only selects which.
|
|
159
|
+
const vad = options.vad ?? false;
|
|
160
|
+
let vadEnabled;
|
|
161
|
+
let vadMode;
|
|
162
|
+
if (vad === false) {
|
|
163
|
+
vadEnabled = false;
|
|
164
|
+
vadMode = 'energy'; // inert placeholder; ignored while disabled
|
|
165
|
+
} else if (vad === true) {
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
"vad: true is no longer supported. Specify the mode explicitly: vad: 'silero' or vad: 'energy'."
|
|
168
|
+
);
|
|
169
|
+
} else if (vad === 'silero' || vad === 'energy') {
|
|
170
|
+
vadEnabled = true;
|
|
171
|
+
vadMode = vad;
|
|
172
|
+
} else {
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
`Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero', or 'energy'.`
|
|
175
|
+
);
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
let modelPath = undefined;
|
|
160
179
|
let ortLibraryPath = undefined;
|
|
161
|
-
if (vadMode === 'silero'
|
|
180
|
+
if (vadEnabled && vadMode === 'silero') {
|
|
162
181
|
modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
|
|
163
182
|
if (!fs.existsSync(modelPath)) {
|
|
164
183
|
throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
|
|
@@ -172,11 +191,12 @@ class Decibri extends Readable {
|
|
|
172
191
|
|
|
173
192
|
// ── Store config ───────────────────────────────────────────────────────
|
|
174
193
|
|
|
175
|
-
this.
|
|
176
|
-
this._vad =
|
|
194
|
+
this._dtype = dtype;
|
|
195
|
+
this._vad = vadEnabled;
|
|
177
196
|
this._vadMode = vadMode;
|
|
178
197
|
this._vadThreshold = options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01);
|
|
179
198
|
this._vadHoldoff = options.vadHoldoff ?? 300;
|
|
199
|
+
this._vadScore = 0;
|
|
180
200
|
this._isSpeaking = false;
|
|
181
201
|
this._silenceTimer = null;
|
|
182
202
|
this._started = false;
|
|
@@ -188,9 +208,9 @@ class Decibri extends Readable {
|
|
|
188
208
|
sampleRate,
|
|
189
209
|
channels,
|
|
190
210
|
framesPerBuffer,
|
|
191
|
-
format,
|
|
211
|
+
format: dtype,
|
|
192
212
|
device: resolvedDevice,
|
|
193
|
-
vadMode
|
|
213
|
+
vadMode,
|
|
194
214
|
modelPath,
|
|
195
215
|
ortLibraryPath,
|
|
196
216
|
});
|
|
@@ -230,7 +250,7 @@ class Decibri extends Readable {
|
|
|
230
250
|
|
|
231
251
|
/** @internal Energy-based VAD (RMS threshold) */
|
|
232
252
|
_processVadEnergy(chunk) {
|
|
233
|
-
const rms = computeRMS(chunk, this.
|
|
253
|
+
const rms = computeRMS(chunk, this._dtype);
|
|
234
254
|
this._processVadValue(rms);
|
|
235
255
|
}
|
|
236
256
|
|
|
@@ -241,6 +261,7 @@ class Decibri extends Readable {
|
|
|
241
261
|
|
|
242
262
|
/** @internal Common speech/silence state machine */
|
|
243
263
|
_processVadValue(value) {
|
|
264
|
+
this._vadScore = value;
|
|
244
265
|
if (value >= this._vadThreshold) {
|
|
245
266
|
clearTimeout(this._silenceTimer);
|
|
246
267
|
this._silenceTimer = null;
|
|
@@ -277,6 +298,16 @@ class Decibri extends Readable {
|
|
|
277
298
|
return this._native.isOpen;
|
|
278
299
|
}
|
|
279
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Most recent VAD score for the active mode: the Silero speech probability
|
|
303
|
+
* in 'silero' mode, the normalized RMS of the last chunk in 'energy' mode.
|
|
304
|
+
* 0 when VAD is disabled or before the first chunk is processed.
|
|
305
|
+
* @returns {number}
|
|
306
|
+
*/
|
|
307
|
+
get vadScore() {
|
|
308
|
+
return this._vadScore;
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
/**
|
|
281
312
|
* List all available input devices on the system.
|
|
282
313
|
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
@@ -286,15 +317,49 @@ class Decibri extends Readable {
|
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
/**
|
|
289
|
-
* Version information for decibri
|
|
290
|
-
* @returns {{ decibri: string,
|
|
320
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
321
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
291
322
|
*/
|
|
292
323
|
static version() {
|
|
293
|
-
|
|
324
|
+
const v = DecibriBridge.version();
|
|
325
|
+
return { decibri: v.decibri, audioBackend: v.audioBackend, binding: PACKAGE_VERSION };
|
|
294
326
|
}
|
|
295
327
|
}
|
|
296
328
|
|
|
297
|
-
const
|
|
298
|
-
|
|
329
|
+
const Speaker = require('./decibri-output.js');
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* List all available audio input devices.
|
|
333
|
+
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
334
|
+
*/
|
|
335
|
+
function inputDevices() {
|
|
336
|
+
return Microphone.devices();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* List all available audio output devices.
|
|
341
|
+
* @returns {Array<{index: number, name: string, maxOutputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
342
|
+
*/
|
|
343
|
+
function outputDevices() {
|
|
344
|
+
return Speaker.devices();
|
|
345
|
+
}
|
|
299
346
|
|
|
300
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
349
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
350
|
+
*/
|
|
351
|
+
function version() {
|
|
352
|
+
return Microphone.version();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = {
|
|
356
|
+
Microphone,
|
|
357
|
+
Speaker,
|
|
358
|
+
inputDevices,
|
|
359
|
+
outputDevices,
|
|
360
|
+
version,
|
|
361
|
+
DecibriError,
|
|
362
|
+
DeviceError,
|
|
363
|
+
OrtError,
|
|
364
|
+
OrtPathError,
|
|
365
|
+
};
|