decibri 4.0.0 → 4.1.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 +7 -0
- package/MIGRATION.md +17 -0
- package/README.md +34 -0
- package/index.d.ts +31 -0
- package/index.js +66 -56
- package/package.json +6 -6
- package/src/decibri-output.js +114 -11
- package/src/decibri.d.ts +61 -0
- package/src/decibri.js +84 -18
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,13 @@ For other decibri packages, see:
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [4.1.0] - 2026-05-31
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Async factories `Microphone.open(options)` and `Speaker.open(options)`, each returning a `Promise` that resolves to a constructed instance. They perform the blocking open work (the Silero VAD model load for the microphone; device resolution for both) on the native thread pool instead of the event loop, so latency-sensitive callers do not stall during construction. A failed open rejects with the matching error (`RangeError` / `TypeError` for invalid options, `DeviceError` / `OrtError` / `OrtPathError` for native failures). The synchronous `new Microphone(...)` and `new Speaker(...)` constructors are unchanged; the factories are an additive, non-blocking alternative that mirrors the Python `AsyncMicrophone.open()` / `AsyncSpeaker.open()` surface.
|
|
19
|
+
- Non-blocking `Speaker.writeAsync(chunk)` and `Speaker.drainAsync()` methods, each returning a `Promise`. They run the blocking parts of playback off the event loop: `writeAsync` performs the backpressure wait when the native playback queue is full, and `drainAsync` performs the wait for queued audio to finish playing. The audio stream stays on its own thread; only the thread-safe sample channel and drain state are used off the event loop. A failed write or drain rejects with the matching error class. The synchronous `write()` / `pipe()` / `end()` Writable interface is unchanged; the async methods are an additive, direct alternative (do not interleave the two paths on one instance).
|
|
20
|
+
|
|
14
21
|
## [4.0.0] - 2026-05-30
|
|
15
22
|
|
|
16
23
|
### Changed
|
package/MIGRATION.md
CHANGED
|
@@ -5,6 +5,23 @@ vocabulary that matches the Rust and Python packages, and tidies several option
|
|
|
5
5
|
and return shapes. This guide lists every breaking change with before and after
|
|
6
6
|
code.
|
|
7
7
|
|
|
8
|
+
## New in 4.1.0 (additive, nothing to migrate)
|
|
9
|
+
|
|
10
|
+
decibri 4.1.0 is a non-breaking, additive release. Code written for 4.0.0 keeps
|
|
11
|
+
working unchanged; there is nothing to migrate. The release adds an opt-in
|
|
12
|
+
non-blocking API for event-loop-sensitive code:
|
|
13
|
+
|
|
14
|
+
- `Microphone.open(options)` and `Speaker.open(options)`: async factories that
|
|
15
|
+
construct an instance without blocking the event loop and resolve to a ready
|
|
16
|
+
instance. The synchronous `new Microphone(...)` and `new Speaker(...)`
|
|
17
|
+
constructors are unchanged.
|
|
18
|
+
- `speaker.writeAsync(chunk)` and `speaker.drainAsync()`: write and drain
|
|
19
|
+
without blocking the event loop. The synchronous `write()` / `pipe()` /
|
|
20
|
+
`end()` interface is unchanged.
|
|
21
|
+
|
|
22
|
+
See the Non-blocking API section of the README for examples. The rest of this
|
|
23
|
+
guide covers the 4.0.0 changes from 3.x.
|
|
24
|
+
|
|
8
25
|
## Named exports
|
|
9
26
|
|
|
10
27
|
The package no longer has a single default export. Destructure what you need.
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ Standard `ReadableOptions` (e.g. `highWaterMark`) are also accepted.
|
|
|
87
87
|
| Method | Description |
|
|
88
88
|
| --- | --- |
|
|
89
89
|
| `mic.stop()` | Stop capture and end stream. Safe to call multiple times |
|
|
90
|
+
| `Microphone.open(options?)` | Construct without blocking the event loop. Returns a `Promise<Microphone>`. See [Non-blocking API](#non-blocking-api) |
|
|
90
91
|
| `Microphone.devices()` | List available input devices |
|
|
91
92
|
| `Microphone.version()` | Version info: `{ decibri, audioBackend, binding }` |
|
|
92
93
|
|
|
@@ -128,14 +129,47 @@ Standard `WritableOptions` (e.g. `highWaterMark`) are also accepted.
|
|
|
128
129
|
| Method / Property | Description |
|
|
129
130
|
| --- | --- |
|
|
130
131
|
| `speaker.write(chunk)` | Write PCM data for playback |
|
|
132
|
+
| `speaker.writeAsync(chunk)` | Write without blocking the event loop. Returns a `Promise`. See [Non-blocking API](#non-blocking-api) |
|
|
131
133
|
| `speaker.end()` | Signal end. Drains remaining audio, then emits `'finish'` |
|
|
134
|
+
| `speaker.drainAsync()` | Wait for queued audio to finish without blocking the event loop. Returns a `Promise` |
|
|
132
135
|
| `speaker.stop()` | Immediate stop. Discards remaining audio |
|
|
133
136
|
| `speaker.isPlaying` | `true` while audio is being output |
|
|
137
|
+
| `Speaker.open(options?)` | Construct without blocking the event loop. Returns a `Promise<Speaker>` |
|
|
134
138
|
| `Speaker.devices()` | List available output devices |
|
|
135
139
|
| `Speaker.version()` | Same as `Microphone.version()` |
|
|
136
140
|
|
|
137
141
|
The module-level `outputDevices()` free function is equivalent to `Speaker.devices()`.
|
|
138
142
|
|
|
143
|
+
## Non-blocking API
|
|
144
|
+
|
|
145
|
+
The synchronous constructors and `write` / `drain` do their work on the event loop, which is fine for most apps. For event-loop-sensitive code (servers, real-time voice pipelines), 4.1.0 adds async variants that perform the blocking work without stalling the event loop. They are additive: the synchronous API is unchanged, and you opt in only where you need it.
|
|
146
|
+
|
|
147
|
+
### Non-blocking construction
|
|
148
|
+
|
|
149
|
+
`Microphone.open(options?)` and `Speaker.open(options?)` are async factories that return a Promise of a ready instance. They take the same options as the constructors. For a microphone with Silero VAD, the model load that the constructor does inline runs without blocking the event loop.
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const { Microphone, Speaker } = require('decibri');
|
|
153
|
+
|
|
154
|
+
const mic = await Microphone.open({ sampleRate: 16000, vad: 'silero' });
|
|
155
|
+
const speaker = await Speaker.open({ sampleRate: 16000, channels: 1 });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The synchronous `new Microphone(...)` and `new Speaker(...)` still work unchanged. A failed open rejects the Promise with the same typed error a failed constructor throws.
|
|
159
|
+
|
|
160
|
+
### Non-blocking playback
|
|
161
|
+
|
|
162
|
+
`speaker.writeAsync(chunk)` resolves once the audio is queued, performing the backpressure wait (when the playback buffer is full) without blocking the event loop. `speaker.drainAsync()` resolves when all queued audio has finished playing, again without blocking.
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
const speaker = await Speaker.open({ sampleRate: 16000, channels: 1 });
|
|
166
|
+
|
|
167
|
+
await speaker.writeAsync(pcmBuffer);
|
|
168
|
+
await speaker.drainAsync(); // resolves when playback finishes
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
These are a direct alternative to the synchronous `write()` / `pipe()` / `end()` stream interface, which is unchanged. Use one path per instance (the stream methods or the async methods, not both at once), and await calls in sequence to keep samples in order.
|
|
172
|
+
|
|
139
173
|
## Errors
|
|
140
174
|
|
|
141
175
|
Construction errors come as typed classes you can catch:
|
package/index.d.ts
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
/** Native bridge class exposed to Node.js via napi-rs. */
|
|
4
4
|
export declare class DecibriBridge {
|
|
5
5
|
constructor(options?: DecibriOptions | undefined | null)
|
|
6
|
+
/**
|
|
7
|
+
* Construct a microphone bridge without blocking the JS event loop. The
|
|
8
|
+
* device resolution and Silero model load run on the libuv thread pool;
|
|
9
|
+
* the returned Promise resolves to a fully constructed bridge, or rejects
|
|
10
|
+
* with the matching error. The synchronous `new` remains available and
|
|
11
|
+
* unchanged.
|
|
12
|
+
*/
|
|
13
|
+
static openAsync(options?: DecibriOptions | undefined | null): Promise<unknown>
|
|
6
14
|
/** Start capturing audio. The callback receives `(err, chunk)` for each buffer. */
|
|
7
15
|
start(callback: (err: Error | null, chunk: Buffer) => void): void
|
|
8
16
|
/** Stop capturing audio. */
|
|
@@ -23,13 +31,36 @@ export declare class DecibriBridge {
|
|
|
23
31
|
/** Native bridge class for audio output, exposed to Node.js via napi-rs. */
|
|
24
32
|
export declare class DecibriOutputBridge {
|
|
25
33
|
constructor(options?: DecibriOutputOptions | undefined | null)
|
|
34
|
+
/**
|
|
35
|
+
* Construct a speaker bridge without blocking the JS event loop. The device
|
|
36
|
+
* resolution runs on the libuv thread pool; the returned Promise resolves
|
|
37
|
+
* to a constructed bridge, or rejects with the matching error. The
|
|
38
|
+
* synchronous `new` remains available and unchanged.
|
|
39
|
+
*/
|
|
40
|
+
static openAsync(options?: DecibriOutputOptions | undefined | null): Promise<unknown>
|
|
26
41
|
/**
|
|
27
42
|
* Write PCM data for playback. Starts the output stream on first call.
|
|
28
43
|
* Empty buffers are a no-op.
|
|
29
44
|
*/
|
|
30
45
|
write(buffer: Buffer): void
|
|
46
|
+
/**
|
|
47
|
+
* Non-blocking write: convert the samples and start the stream on the JS
|
|
48
|
+
* thread (a fast device open, same as the synchronous first write), then
|
|
49
|
+
* perform the blocking channel `send` (which stalls under backpressure when
|
|
50
|
+
* the queue is full) on the libuv thread pool. The returned Promise
|
|
51
|
+
* resolves when the samples are queued, or rejects with the matching error.
|
|
52
|
+
* Empty buffers resolve immediately. The synchronous `write` is unchanged.
|
|
53
|
+
*/
|
|
54
|
+
writeAsync(buffer: Buffer): Promise<unknown>
|
|
31
55
|
/** Graceful drain: blocks until all queued samples have been played. */
|
|
32
56
|
drain(): void
|
|
57
|
+
/**
|
|
58
|
+
* Non-blocking drain: the poll loop that waits for the cpal callback to play
|
|
59
|
+
* everything queued runs on the libuv thread pool instead of the event loop.
|
|
60
|
+
* The returned Promise resolves when the buffer has drained. With no stream
|
|
61
|
+
* yet created it resolves immediately. The synchronous `drain` is unchanged.
|
|
62
|
+
*/
|
|
63
|
+
drainAsync(): Promise<unknown>
|
|
33
64
|
/** Immediate stop. Discards remaining samples. */
|
|
34
65
|
stop(): void
|
|
35
66
|
/** Whether audio is currently being output. */
|
package/index.js
CHANGED
|
@@ -77,8 +77,8 @@ function requireNative() {
|
|
|
77
77
|
try {
|
|
78
78
|
const binding = require('@decibri/decibri-android-arm64')
|
|
79
79
|
const bindingPackageVersion = require('@decibri/decibri-android-arm64/package.json').version
|
|
80
|
-
if (bindingPackageVersion !== '4.
|
|
81
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
80
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
81
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
82
82
|
}
|
|
83
83
|
return binding
|
|
84
84
|
} catch (e) {
|
|
@@ -93,8 +93,8 @@ function requireNative() {
|
|
|
93
93
|
try {
|
|
94
94
|
const binding = require('@decibri/decibri-android-arm-eabi')
|
|
95
95
|
const bindingPackageVersion = require('@decibri/decibri-android-arm-eabi/package.json').version
|
|
96
|
-
if (bindingPackageVersion !== '4.
|
|
97
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
96
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
97
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
98
98
|
}
|
|
99
99
|
return binding
|
|
100
100
|
} catch (e) {
|
|
@@ -114,8 +114,8 @@ function requireNative() {
|
|
|
114
114
|
try {
|
|
115
115
|
const binding = require('@decibri/decibri-win32-x64-gnu')
|
|
116
116
|
const bindingPackageVersion = require('@decibri/decibri-win32-x64-gnu/package.json').version
|
|
117
|
-
if (bindingPackageVersion !== '4.
|
|
118
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
117
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
118
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
119
119
|
}
|
|
120
120
|
return binding
|
|
121
121
|
} catch (e) {
|
|
@@ -130,8 +130,8 @@ function requireNative() {
|
|
|
130
130
|
try {
|
|
131
131
|
const binding = require('@decibri/decibri-win32-x64-msvc')
|
|
132
132
|
const bindingPackageVersion = require('@decibri/decibri-win32-x64-msvc/package.json').version
|
|
133
|
-
if (bindingPackageVersion !== '4.
|
|
134
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
133
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
134
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
135
135
|
}
|
|
136
136
|
return binding
|
|
137
137
|
} catch (e) {
|
|
@@ -147,8 +147,8 @@ function requireNative() {
|
|
|
147
147
|
try {
|
|
148
148
|
const binding = require('@decibri/decibri-win32-ia32-msvc')
|
|
149
149
|
const bindingPackageVersion = require('@decibri/decibri-win32-ia32-msvc/package.json').version
|
|
150
|
-
if (bindingPackageVersion !== '4.
|
|
151
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
150
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
151
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
152
152
|
}
|
|
153
153
|
return binding
|
|
154
154
|
} catch (e) {
|
|
@@ -163,8 +163,8 @@ function requireNative() {
|
|
|
163
163
|
try {
|
|
164
164
|
const binding = require('@decibri/decibri-win32-arm64-msvc')
|
|
165
165
|
const bindingPackageVersion = require('@decibri/decibri-win32-arm64-msvc/package.json').version
|
|
166
|
-
if (bindingPackageVersion !== '4.
|
|
167
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
166
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
167
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
168
168
|
}
|
|
169
169
|
return binding
|
|
170
170
|
} catch (e) {
|
|
@@ -182,8 +182,8 @@ function requireNative() {
|
|
|
182
182
|
try {
|
|
183
183
|
const binding = require('@decibri/decibri-darwin-universal')
|
|
184
184
|
const bindingPackageVersion = require('@decibri/decibri-darwin-universal/package.json').version
|
|
185
|
-
if (bindingPackageVersion !== '4.
|
|
186
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
185
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
186
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
187
187
|
}
|
|
188
188
|
return binding
|
|
189
189
|
} catch (e) {
|
|
@@ -198,8 +198,8 @@ function requireNative() {
|
|
|
198
198
|
try {
|
|
199
199
|
const binding = require('@decibri/decibri-darwin-x64')
|
|
200
200
|
const bindingPackageVersion = require('@decibri/decibri-darwin-x64/package.json').version
|
|
201
|
-
if (bindingPackageVersion !== '4.
|
|
202
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
201
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
202
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
203
203
|
}
|
|
204
204
|
return binding
|
|
205
205
|
} catch (e) {
|
|
@@ -214,8 +214,8 @@ function requireNative() {
|
|
|
214
214
|
try {
|
|
215
215
|
const binding = require('@decibri/decibri-darwin-arm64')
|
|
216
216
|
const bindingPackageVersion = require('@decibri/decibri-darwin-arm64/package.json').version
|
|
217
|
-
if (bindingPackageVersion !== '4.
|
|
218
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
217
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
218
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
219
219
|
}
|
|
220
220
|
return binding
|
|
221
221
|
} catch (e) {
|
|
@@ -234,8 +234,8 @@ function requireNative() {
|
|
|
234
234
|
try {
|
|
235
235
|
const binding = require('@decibri/decibri-freebsd-x64')
|
|
236
236
|
const bindingPackageVersion = require('@decibri/decibri-freebsd-x64/package.json').version
|
|
237
|
-
if (bindingPackageVersion !== '4.
|
|
238
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
237
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
238
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
239
239
|
}
|
|
240
240
|
return binding
|
|
241
241
|
} catch (e) {
|
|
@@ -250,8 +250,8 @@ function requireNative() {
|
|
|
250
250
|
try {
|
|
251
251
|
const binding = require('@decibri/decibri-freebsd-arm64')
|
|
252
252
|
const bindingPackageVersion = require('@decibri/decibri-freebsd-arm64/package.json').version
|
|
253
|
-
if (bindingPackageVersion !== '4.
|
|
254
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
253
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
254
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
255
255
|
}
|
|
256
256
|
return binding
|
|
257
257
|
} catch (e) {
|
|
@@ -271,8 +271,8 @@ function requireNative() {
|
|
|
271
271
|
try {
|
|
272
272
|
const binding = require('@decibri/decibri-linux-x64-musl')
|
|
273
273
|
const bindingPackageVersion = require('@decibri/decibri-linux-x64-musl/package.json').version
|
|
274
|
-
if (bindingPackageVersion !== '4.
|
|
275
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
274
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
275
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
276
276
|
}
|
|
277
277
|
return binding
|
|
278
278
|
} catch (e) {
|
|
@@ -287,8 +287,8 @@ function requireNative() {
|
|
|
287
287
|
try {
|
|
288
288
|
const binding = require('@decibri/decibri-linux-x64-gnu')
|
|
289
289
|
const bindingPackageVersion = require('@decibri/decibri-linux-x64-gnu/package.json').version
|
|
290
|
-
if (bindingPackageVersion !== '4.
|
|
291
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
290
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
291
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
292
292
|
}
|
|
293
293
|
return binding
|
|
294
294
|
} catch (e) {
|
|
@@ -305,8 +305,8 @@ function requireNative() {
|
|
|
305
305
|
try {
|
|
306
306
|
const binding = require('@decibri/decibri-linux-arm64-musl')
|
|
307
307
|
const bindingPackageVersion = require('@decibri/decibri-linux-arm64-musl/package.json').version
|
|
308
|
-
if (bindingPackageVersion !== '4.
|
|
309
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
308
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
309
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
310
310
|
}
|
|
311
311
|
return binding
|
|
312
312
|
} catch (e) {
|
|
@@ -321,8 +321,8 @@ function requireNative() {
|
|
|
321
321
|
try {
|
|
322
322
|
const binding = require('@decibri/decibri-linux-arm64-gnu')
|
|
323
323
|
const bindingPackageVersion = require('@decibri/decibri-linux-arm64-gnu/package.json').version
|
|
324
|
-
if (bindingPackageVersion !== '4.
|
|
325
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
324
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
325
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
326
326
|
}
|
|
327
327
|
return binding
|
|
328
328
|
} catch (e) {
|
|
@@ -339,8 +339,8 @@ function requireNative() {
|
|
|
339
339
|
try {
|
|
340
340
|
const binding = require('@decibri/decibri-linux-arm-musleabihf')
|
|
341
341
|
const bindingPackageVersion = require('@decibri/decibri-linux-arm-musleabihf/package.json').version
|
|
342
|
-
if (bindingPackageVersion !== '4.
|
|
343
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
342
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
343
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
344
344
|
}
|
|
345
345
|
return binding
|
|
346
346
|
} catch (e) {
|
|
@@ -355,8 +355,8 @@ function requireNative() {
|
|
|
355
355
|
try {
|
|
356
356
|
const binding = require('@decibri/decibri-linux-arm-gnueabihf')
|
|
357
357
|
const bindingPackageVersion = require('@decibri/decibri-linux-arm-gnueabihf/package.json').version
|
|
358
|
-
if (bindingPackageVersion !== '4.
|
|
359
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
358
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
359
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
360
360
|
}
|
|
361
361
|
return binding
|
|
362
362
|
} catch (e) {
|
|
@@ -373,8 +373,8 @@ function requireNative() {
|
|
|
373
373
|
try {
|
|
374
374
|
const binding = require('@decibri/decibri-linux-loong64-musl')
|
|
375
375
|
const bindingPackageVersion = require('@decibri/decibri-linux-loong64-musl/package.json').version
|
|
376
|
-
if (bindingPackageVersion !== '4.
|
|
377
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
376
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
377
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
378
378
|
}
|
|
379
379
|
return binding
|
|
380
380
|
} catch (e) {
|
|
@@ -389,8 +389,8 @@ function requireNative() {
|
|
|
389
389
|
try {
|
|
390
390
|
const binding = require('@decibri/decibri-linux-loong64-gnu')
|
|
391
391
|
const bindingPackageVersion = require('@decibri/decibri-linux-loong64-gnu/package.json').version
|
|
392
|
-
if (bindingPackageVersion !== '4.
|
|
393
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
392
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
393
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
394
394
|
}
|
|
395
395
|
return binding
|
|
396
396
|
} catch (e) {
|
|
@@ -407,8 +407,8 @@ function requireNative() {
|
|
|
407
407
|
try {
|
|
408
408
|
const binding = require('@decibri/decibri-linux-riscv64-musl')
|
|
409
409
|
const bindingPackageVersion = require('@decibri/decibri-linux-riscv64-musl/package.json').version
|
|
410
|
-
if (bindingPackageVersion !== '4.
|
|
411
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
410
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
411
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
412
412
|
}
|
|
413
413
|
return binding
|
|
414
414
|
} catch (e) {
|
|
@@ -423,8 +423,8 @@ function requireNative() {
|
|
|
423
423
|
try {
|
|
424
424
|
const binding = require('@decibri/decibri-linux-riscv64-gnu')
|
|
425
425
|
const bindingPackageVersion = require('@decibri/decibri-linux-riscv64-gnu/package.json').version
|
|
426
|
-
if (bindingPackageVersion !== '4.
|
|
427
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
426
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
427
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
428
428
|
}
|
|
429
429
|
return binding
|
|
430
430
|
} catch (e) {
|
|
@@ -440,8 +440,8 @@ function requireNative() {
|
|
|
440
440
|
try {
|
|
441
441
|
const binding = require('@decibri/decibri-linux-ppc64-gnu')
|
|
442
442
|
const bindingPackageVersion = require('@decibri/decibri-linux-ppc64-gnu/package.json').version
|
|
443
|
-
if (bindingPackageVersion !== '4.
|
|
444
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
443
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
444
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
445
445
|
}
|
|
446
446
|
return binding
|
|
447
447
|
} catch (e) {
|
|
@@ -456,8 +456,8 @@ function requireNative() {
|
|
|
456
456
|
try {
|
|
457
457
|
const binding = require('@decibri/decibri-linux-s390x-gnu')
|
|
458
458
|
const bindingPackageVersion = require('@decibri/decibri-linux-s390x-gnu/package.json').version
|
|
459
|
-
if (bindingPackageVersion !== '4.
|
|
460
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
459
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
460
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
461
461
|
}
|
|
462
462
|
return binding
|
|
463
463
|
} catch (e) {
|
|
@@ -476,8 +476,8 @@ function requireNative() {
|
|
|
476
476
|
try {
|
|
477
477
|
const binding = require('@decibri/decibri-openharmony-arm64')
|
|
478
478
|
const bindingPackageVersion = require('@decibri/decibri-openharmony-arm64/package.json').version
|
|
479
|
-
if (bindingPackageVersion !== '4.
|
|
480
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
479
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
480
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
481
481
|
}
|
|
482
482
|
return binding
|
|
483
483
|
} catch (e) {
|
|
@@ -492,8 +492,8 @@ function requireNative() {
|
|
|
492
492
|
try {
|
|
493
493
|
const binding = require('@decibri/decibri-openharmony-x64')
|
|
494
494
|
const bindingPackageVersion = require('@decibri/decibri-openharmony-x64/package.json').version
|
|
495
|
-
if (bindingPackageVersion !== '4.
|
|
496
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
495
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
496
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
497
497
|
}
|
|
498
498
|
return binding
|
|
499
499
|
} catch (e) {
|
|
@@ -508,8 +508,8 @@ function requireNative() {
|
|
|
508
508
|
try {
|
|
509
509
|
const binding = require('@decibri/decibri-openharmony-arm')
|
|
510
510
|
const bindingPackageVersion = require('@decibri/decibri-openharmony-arm/package.json').version
|
|
511
|
-
if (bindingPackageVersion !== '4.
|
|
512
|
-
throw new Error(`Native binding package version mismatch, expected 4.
|
|
511
|
+
if (bindingPackageVersion !== '4.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
|
512
|
+
throw new Error(`Native binding package version mismatch, expected 4.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
|
513
513
|
}
|
|
514
514
|
return binding
|
|
515
515
|
} catch (e) {
|
|
@@ -525,23 +525,33 @@ function requireNative() {
|
|
|
525
525
|
|
|
526
526
|
nativeBinding = requireNative()
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
// NAPI_RS_FORCE_WASI is a tri-state flag:
|
|
529
|
+
// unset / any other value → native binding preferred, WASI is only a fallback
|
|
530
|
+
// 'true' → force WASI fallback even if native loaded
|
|
531
|
+
// 'error' → force WASI and throw if no WASI binding is found
|
|
532
|
+
// Treating any non-empty string as truthy (the historical behavior) meant
|
|
533
|
+
// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
|
|
534
|
+
// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
|
|
535
|
+
const forceWasi =
|
|
536
|
+
process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error'
|
|
537
|
+
|
|
538
|
+
if (!nativeBinding || forceWasi) {
|
|
529
539
|
let wasiBinding = null
|
|
530
540
|
let wasiBindingError = null
|
|
531
541
|
try {
|
|
532
542
|
wasiBinding = require('./decibri.wasi.cjs')
|
|
533
543
|
nativeBinding = wasiBinding
|
|
534
544
|
} catch (err) {
|
|
535
|
-
if (
|
|
545
|
+
if (forceWasi) {
|
|
536
546
|
wasiBindingError = err
|
|
537
547
|
}
|
|
538
548
|
}
|
|
539
|
-
if (!nativeBinding ||
|
|
549
|
+
if (!nativeBinding || forceWasi) {
|
|
540
550
|
try {
|
|
541
551
|
wasiBinding = require('@decibri/decibri-wasm32-wasi')
|
|
542
552
|
nativeBinding = wasiBinding
|
|
543
553
|
} catch (err) {
|
|
544
|
-
if (
|
|
554
|
+
if (forceWasi) {
|
|
545
555
|
if (!wasiBindingError) {
|
|
546
556
|
wasiBindingError = err
|
|
547
557
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decibri",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Cross-platform audio capture, playback, and processing for Node.js and browsers",
|
|
5
5
|
"main": "src/decibri.js",
|
|
6
6
|
"types": "src/decibri.d.ts",
|
|
@@ -72,13 +72,13 @@
|
|
|
72
72
|
"MIGRATION.md"
|
|
73
73
|
],
|
|
74
74
|
"optionalDependencies": {
|
|
75
|
-
"@decibri/decibri-win32-x64-msvc": "4.
|
|
76
|
-
"@decibri/decibri-darwin-arm64": "4.
|
|
77
|
-
"@decibri/decibri-linux-x64-gnu": "4.
|
|
78
|
-
"@decibri/decibri-linux-arm64-gnu": "4.
|
|
75
|
+
"@decibri/decibri-win32-x64-msvc": "4.1.0",
|
|
76
|
+
"@decibri/decibri-darwin-arm64": "4.1.0",
|
|
77
|
+
"@decibri/decibri-linux-x64-gnu": "4.1.0",
|
|
78
|
+
"@decibri/decibri-linux-arm64-gnu": "4.1.0"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@napi-rs/cli": "^3.
|
|
81
|
+
"@napi-rs/cli": "^3.7.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "napi build --platform --release -p decibri-node --manifest-path ../../bindings/node/Cargo.toml --js-package-name @decibri/decibri -o .",
|
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 */
|