decibri 4.0.0 → 4.2.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 +13 -0
- package/MIGRATION.md +31 -0
- package/README.md +80 -0
- package/examples/README.md +79 -0
- package/examples/browser-speaker-test.html +159 -0
- package/examples/decibri.browser.js +594 -0
- package/index.d.ts +31 -0
- package/index.js +66 -56
- package/package.json +6 -6
- package/src/browser/decibri-browser.js +1 -1
- package/src/browser/decibri-output-browser.js +377 -0
- package/src/browser/index.d.ts +85 -0
- package/src/browser/index.js +2 -1
- package/src/browser/output-worklet-inline.js +16 -0
- package/src/browser/output-worklet-processor.js +168 -0
- package/src/decibri-output.js +114 -11
- package/src/decibri.d.ts +61 -0
- package/src/decibri.js +84 -18
package/src/decibri-output.js
CHANGED
|
@@ -13,10 +13,45 @@ const PACKAGE_VERSION = require('../package.json').version;
|
|
|
13
13
|
class Speaker extends Writable {
|
|
14
14
|
/**
|
|
15
15
|
* @param {import('./decibri').SpeakerOptions} [options]
|
|
16
|
+
* @param {{ prepared: object, native: object }} [_internal] Internal: a
|
|
17
|
+
* pre-resolved options bundle and an already-constructed native bridge,
|
|
18
|
+
* passed by the async `Speaker.open()` factory. Not part of the public API.
|
|
16
19
|
*/
|
|
17
|
-
constructor(options = {}) {
|
|
20
|
+
constructor(options = {}, _internal = undefined) {
|
|
18
21
|
super({ highWaterMark: options.highWaterMark || 16384 });
|
|
19
22
|
|
|
23
|
+
// Validate and resolve options once. The async factory passes its already
|
|
24
|
+
// resolved bundle through `_internal` to avoid recomputing it.
|
|
25
|
+
const prepared = _internal ? _internal.prepared : Speaker._prepareOptions(options);
|
|
26
|
+
|
|
27
|
+
// ── Store config ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
this._dtype = prepared.dtype;
|
|
30
|
+
this._started = false;
|
|
31
|
+
|
|
32
|
+
// ── Create or adopt native bridge ───────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
if (_internal) {
|
|
35
|
+
// Built off the event loop by Speaker.open(); already wrapped.
|
|
36
|
+
this._native = _internal.native;
|
|
37
|
+
} else {
|
|
38
|
+
try {
|
|
39
|
+
this._native = new DecibriOutputBridge(prepared.nativeOptions);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw wrapNativeError(err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate the constructor options and resolve them into the native options
|
|
48
|
+
* object plus the wrapper-side state. Throws the same `RangeError` /
|
|
49
|
+
* `TypeError` as the constructor on invalid input. Shared by the synchronous
|
|
50
|
+
* constructor and the async `open()` factory.
|
|
51
|
+
* @internal
|
|
52
|
+
* @param {import('./decibri').SpeakerOptions} options
|
|
53
|
+
*/
|
|
54
|
+
static _prepareOptions(options) {
|
|
20
55
|
// ── Validate options ───────────────────────────────────────────────────
|
|
21
56
|
|
|
22
57
|
const sampleRate = options.sampleRate ?? 16000;
|
|
@@ -61,23 +96,41 @@ class Speaker extends Writable {
|
|
|
61
96
|
resolvedDevice = options.device;
|
|
62
97
|
}
|
|
63
98
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this._started = false;
|
|
68
|
-
|
|
69
|
-
// ── Create native bridge ───────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
this._native = new DecibriOutputBridge({
|
|
99
|
+
return {
|
|
100
|
+
dtype,
|
|
101
|
+
nativeOptions: {
|
|
73
102
|
sampleRate,
|
|
74
103
|
channels,
|
|
75
104
|
format: dtype,
|
|
76
105
|
device: resolvedDevice,
|
|
77
|
-
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Construct a Speaker without blocking the event loop.
|
|
112
|
+
*
|
|
113
|
+
* Symmetric with `Microphone.open()` and the Python `AsyncSpeaker.open()`.
|
|
114
|
+
* The speaker loads no model, so the only open work is device resolution and
|
|
115
|
+
* the practical blocking risk is small; this factory exists chiefly so async
|
|
116
|
+
* callers can use one consistent construction pattern across both classes.
|
|
117
|
+
* The synchronous constructor remains available and unchanged.
|
|
118
|
+
*
|
|
119
|
+
* A failed open (unknown device) rejects the returned Promise with the
|
|
120
|
+
* matching error class rather than throwing synchronously.
|
|
121
|
+
*
|
|
122
|
+
* @param {import('./decibri').SpeakerOptions} [options]
|
|
123
|
+
* @returns {Promise<Speaker>}
|
|
124
|
+
*/
|
|
125
|
+
static async open(options = {}) {
|
|
126
|
+
const prepared = Speaker._prepareOptions(options);
|
|
127
|
+
let native;
|
|
128
|
+
try {
|
|
129
|
+
native = await DecibriOutputBridge.openAsync(prepared.nativeOptions);
|
|
78
130
|
} catch (err) {
|
|
79
131
|
throw wrapNativeError(err);
|
|
80
132
|
}
|
|
133
|
+
return new Speaker(options, { prepared, native });
|
|
81
134
|
}
|
|
82
135
|
|
|
83
136
|
/** @internal */
|
|
@@ -104,6 +157,56 @@ class Speaker extends Writable {
|
|
|
104
157
|
}
|
|
105
158
|
}
|
|
106
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Write PCM audio without blocking the event loop.
|
|
162
|
+
*
|
|
163
|
+
* The blocking part of a write is the backpressure wait when the native
|
|
164
|
+
* playback queue is full; the synchronous stream path (`write()` / `pipe()`)
|
|
165
|
+
* performs that wait on the event loop. This method performs it on the native
|
|
166
|
+
* thread pool and resolves when the samples are queued. The audio stream is
|
|
167
|
+
* created on the first call (a fast device open) and stays on its own thread;
|
|
168
|
+
* only the queue handoff runs off the event loop.
|
|
169
|
+
*
|
|
170
|
+
* Additive and non-blocking: the synchronous `write()` / `pipe()` stream
|
|
171
|
+
* interface is unchanged. This is a direct, opt-in alternative that bypasses
|
|
172
|
+
* the Writable buffer, so do not interleave it with `write()` / `pipe()` on
|
|
173
|
+
* the same instance; pick one path per instance. Await calls sequentially to
|
|
174
|
+
* preserve sample order. An empty buffer resolves immediately. A failed write
|
|
175
|
+
* (a closed or stopped stream) rejects with the matching error class.
|
|
176
|
+
*
|
|
177
|
+
* @param {Buffer} chunk PCM samples in the configured `dtype`.
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
async writeAsync(chunk) {
|
|
181
|
+
try {
|
|
182
|
+
await this._native.writeAsync(chunk);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
throw wrapNativeError(err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wait for all queued audio to finish playing without blocking the event
|
|
190
|
+
* loop.
|
|
191
|
+
*
|
|
192
|
+
* The synchronous drain (run by `end()` / `_final`) polls for completion on
|
|
193
|
+
* the event loop for the full playback tail; this method runs that wait on the
|
|
194
|
+
* native thread pool and resolves when the buffer has drained. If nothing has
|
|
195
|
+
* been written yet it resolves immediately.
|
|
196
|
+
*
|
|
197
|
+
* Additive and non-blocking: the synchronous drain via `end()` is unchanged.
|
|
198
|
+
* Pair this with `writeAsync()` for a fully non-blocking playback path.
|
|
199
|
+
*
|
|
200
|
+
* @returns {Promise<void>}
|
|
201
|
+
*/
|
|
202
|
+
async drainAsync() {
|
|
203
|
+
try {
|
|
204
|
+
await this._native.drainAsync();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw wrapNativeError(err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
/**
|
|
108
211
|
* Immediate stop. Discards remaining buffered audio.
|
|
109
212
|
*/
|
package/src/decibri.d.ts
CHANGED
|
@@ -124,6 +124,27 @@ export interface MicrophoneOptions extends ReadableOptions {
|
|
|
124
124
|
export declare class Microphone extends Readable {
|
|
125
125
|
constructor(options?: MicrophoneOptions);
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Construct a Microphone without blocking the event loop on the open work.
|
|
129
|
+
*
|
|
130
|
+
* The synchronous constructor loads the Silero VAD model inline when
|
|
131
|
+
* `vad: 'silero'` is set, blocking the event loop for roughly 100 to 500 ms
|
|
132
|
+
* on a cold cache. This factory runs that load (and device resolution) on the
|
|
133
|
+
* native thread pool and resolves to a ready instance. The synchronous
|
|
134
|
+
* constructor remains available and unchanged.
|
|
135
|
+
*
|
|
136
|
+
* Options are identical to the constructor. A failed open rejects the Promise
|
|
137
|
+
* with the matching error: `RangeError` / `TypeError` for invalid options, or
|
|
138
|
+
* a `DeviceError` / `OrtError` / `OrtPathError` for native failures.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```js
|
|
142
|
+
* const mic = await Microphone.open({ vad: 'silero' });
|
|
143
|
+
* mic.on('data', (chunk) => { ... });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
static open(options?: MicrophoneOptions): Promise<Microphone>;
|
|
147
|
+
|
|
127
148
|
/** Stop microphone capture and end the stream. Safe to call multiple times. */
|
|
128
149
|
stop(): void;
|
|
129
150
|
|
|
@@ -235,6 +256,46 @@ export interface SpeakerOptions extends WritableOptions {
|
|
|
235
256
|
export declare class Speaker extends Writable {
|
|
236
257
|
constructor(options?: SpeakerOptions);
|
|
237
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Construct a Speaker without blocking the event loop. Symmetric with
|
|
261
|
+
* `Microphone.open()`. The speaker loads no model, so the only open work is
|
|
262
|
+
* device resolution; this factory is provided so async callers can use one
|
|
263
|
+
* consistent construction pattern across both classes. The synchronous
|
|
264
|
+
* constructor remains available and unchanged.
|
|
265
|
+
*
|
|
266
|
+
* A failed open (unknown device) rejects the Promise with the matching error.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```js
|
|
270
|
+
* const speaker = await Speaker.open({ sampleRate: 24000 });
|
|
271
|
+
* speaker.write(pcmBuffer);
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
static open(options?: SpeakerOptions): Promise<Speaker>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Write PCM audio without blocking the event loop. Performs the backpressure
|
|
278
|
+
* wait (when the native playback queue is full) on the native thread pool and
|
|
279
|
+
* resolves when the samples are queued.
|
|
280
|
+
*
|
|
281
|
+
* Additive: the synchronous `write()` / `pipe()` stream interface is
|
|
282
|
+
* unchanged. This is a direct, opt-in alternative that bypasses the Writable
|
|
283
|
+
* buffer; do not interleave it with `write()` / `pipe()` on the same instance.
|
|
284
|
+
* Await calls sequentially to preserve sample order. An empty buffer resolves
|
|
285
|
+
* immediately; a closed or stopped stream rejects with the matching error.
|
|
286
|
+
*/
|
|
287
|
+
writeAsync(chunk: Buffer): Promise<void>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Wait for all queued audio to finish playing without blocking the event
|
|
291
|
+
* loop. Runs the drain wait on the native thread pool and resolves when the
|
|
292
|
+
* buffer has drained; resolves immediately if nothing was written.
|
|
293
|
+
*
|
|
294
|
+
* Additive: the synchronous drain via `end()` is unchanged. Pair with
|
|
295
|
+
* `writeAsync()` for a fully non-blocking playback path.
|
|
296
|
+
*/
|
|
297
|
+
drainAsync(): Promise<void>;
|
|
298
|
+
|
|
238
299
|
/** Immediate stop. Discards remaining buffered audio. */
|
|
239
300
|
stop(): void;
|
|
240
301
|
|
package/src/decibri.js
CHANGED
|
@@ -96,10 +96,56 @@ function computeRMS(chunk, dtype) {
|
|
|
96
96
|
class Microphone extends Readable {
|
|
97
97
|
/**
|
|
98
98
|
* @param {import('./decibri').MicrophoneOptions} [options]
|
|
99
|
+
* @param {{ prepared: object, native: object }} [_internal] Internal: a
|
|
100
|
+
* pre-resolved options bundle and an already-constructed native bridge,
|
|
101
|
+
* passed by the async `Microphone.open()` factory so the heavy open work
|
|
102
|
+
* (the Silero model load) is not repeated on the event loop. Not part of
|
|
103
|
+
* the public API.
|
|
99
104
|
*/
|
|
100
|
-
constructor(options = {}) {
|
|
105
|
+
constructor(options = {}, _internal = undefined) {
|
|
101
106
|
super({ highWaterMark: options.highWaterMark, objectMode: false });
|
|
102
107
|
|
|
108
|
+
// Validate and resolve options once. The async factory passes its already
|
|
109
|
+
// resolved bundle through `_internal` to avoid recomputing it.
|
|
110
|
+
const prepared = _internal ? _internal.prepared : Microphone._prepareOptions(options);
|
|
111
|
+
|
|
112
|
+
// ── Store config ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
this._dtype = prepared.dtype;
|
|
115
|
+
this._vad = prepared.vadEnabled;
|
|
116
|
+
this._vadMode = prepared.vadMode;
|
|
117
|
+
this._vadThreshold = prepared.vadThreshold;
|
|
118
|
+
this._vadHoldoff = prepared.vadHoldoff;
|
|
119
|
+
this._vadScore = 0;
|
|
120
|
+
this._isSpeaking = false;
|
|
121
|
+
this._silenceTimer = null;
|
|
122
|
+
this._started = false;
|
|
123
|
+
|
|
124
|
+
// ── Create or adopt native bridge ───────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
if (_internal) {
|
|
127
|
+
// Built off the event loop by Microphone.open(); already wrapped.
|
|
128
|
+
this._native = _internal.native;
|
|
129
|
+
} else {
|
|
130
|
+
try {
|
|
131
|
+
this._native = new DecibriBridge(prepared.nativeOptions);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw wrapNativeError(err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate the constructor options and resolve them into the native options
|
|
140
|
+
* object plus the wrapper-side state. Throws the same `RangeError` /
|
|
141
|
+
* `TypeError` / `Error` as the constructor on invalid input. Shared by the
|
|
142
|
+
* synchronous constructor and the async `open()` factory so both validate
|
|
143
|
+
* identically. Does no native open work beyond the numeric-device bounds
|
|
144
|
+
* check (a fast device enumeration).
|
|
145
|
+
* @internal
|
|
146
|
+
* @param {import('./decibri').MicrophoneOptions} options
|
|
147
|
+
*/
|
|
148
|
+
static _prepareOptions(options) {
|
|
103
149
|
// ── Validate options ───────────────────────────────────────────────────
|
|
104
150
|
|
|
105
151
|
const sampleRate = options.sampleRate ?? 16000;
|
|
@@ -189,22 +235,13 @@ class Microphone extends Readable {
|
|
|
189
235
|
ortLibraryPath = resolveBundledOrtPath();
|
|
190
236
|
}
|
|
191
237
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this._vadScore = 0;
|
|
200
|
-
this._isSpeaking = false;
|
|
201
|
-
this._silenceTimer = null;
|
|
202
|
-
this._started = false;
|
|
203
|
-
|
|
204
|
-
// ── Create native bridge ───────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
this._native = new DecibriBridge({
|
|
238
|
+
return {
|
|
239
|
+
dtype,
|
|
240
|
+
vadEnabled,
|
|
241
|
+
vadMode,
|
|
242
|
+
vadThreshold: options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
|
|
243
|
+
vadHoldoff: options.vadHoldoff ?? 300,
|
|
244
|
+
nativeOptions: {
|
|
208
245
|
sampleRate,
|
|
209
246
|
channels,
|
|
210
247
|
framesPerBuffer,
|
|
@@ -213,10 +250,39 @@ class Microphone extends Readable {
|
|
|
213
250
|
vadMode,
|
|
214
251
|
modelPath,
|
|
215
252
|
ortLibraryPath,
|
|
216
|
-
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Construct a Microphone without blocking the event loop on the open work.
|
|
259
|
+
*
|
|
260
|
+
* The synchronous `new Microphone(...)` constructor loads the Silero VAD
|
|
261
|
+
* model inline when `vad: 'silero'` is set, which blocks the event loop for
|
|
262
|
+
* roughly 100 to 500 ms on a cold cache. This static factory runs that load
|
|
263
|
+
* (and device resolution) on the native thread pool and resolves to a ready
|
|
264
|
+
* instance, so latency-sensitive callers (voice pipelines, websocket
|
|
265
|
+
* handlers) do not stall. The synchronous constructor remains available and
|
|
266
|
+
* unchanged.
|
|
267
|
+
*
|
|
268
|
+
* Mirrors the Python `AsyncMicrophone.open()` factory. Options are identical
|
|
269
|
+
* to the constructor. A failed open (bad model path, unknown device, ORT
|
|
270
|
+
* load failure) rejects the returned Promise with the matching error class
|
|
271
|
+
* (`RangeError` / `TypeError` for invalid options, `DeviceError` / `OrtError`
|
|
272
|
+
* / `OrtPathError` for native failures), rather than throwing synchronously.
|
|
273
|
+
*
|
|
274
|
+
* @param {import('./decibri').MicrophoneOptions} [options]
|
|
275
|
+
* @returns {Promise<Microphone>}
|
|
276
|
+
*/
|
|
277
|
+
static async open(options = {}) {
|
|
278
|
+
const prepared = Microphone._prepareOptions(options);
|
|
279
|
+
let native;
|
|
280
|
+
try {
|
|
281
|
+
native = await DecibriBridge.openAsync(prepared.nativeOptions);
|
|
217
282
|
} catch (err) {
|
|
218
283
|
throw wrapNativeError(err);
|
|
219
284
|
}
|
|
285
|
+
return new Microphone(options, { prepared, native });
|
|
220
286
|
}
|
|
221
287
|
|
|
222
288
|
/** @internal */
|