decibri 5.0.0 → 5.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 CHANGED
@@ -9,6 +9,14 @@ For other decibri packages, see:
9
9
  - Rust crate: [crates/decibri/CHANGELOG.md](../../crates/decibri/CHANGELOG.md)
10
10
  - Python wheel: [bindings/python/CHANGELOG.md](../../bindings/python/CHANGELOG.md)
11
11
 
12
+ ## [5.1.0] - 2026-07-17
13
+
14
+ ### Added
15
+
16
+ - `File`, an offline source: the same conditioning options as `Microphone` over a WAV file (`new File(path)` synchronously, `await File.open(path)` off the event loop) or a `Float32Array` of samples (`File.buffer(samples, { inputRate })`; a raw `Buffer` of bytes is rejected as ambiguous), delivered as a finite Readable stream of conditioned chunks.
17
+ - Whole-file speech analysis: `file.analyze()` (also spelled `file.analyse()`) resolves to a `VadReport` with per-window `scores` (`{ start, end, vadScore, isSpeech }`) and merged speech `segments` (`{ start, end }`), all in seconds of file time. Requires `vad: 'silero'`; a `File` opened without `vad` rejects with `analysis requires VAD`.
18
+ - Per-chunk VAD on files: `vadScore` and the `speech` / `silence` events alongside the stream, with the speaking holdoff measured in file time (sample positions) rather than wall-clock time, so processing speed never changes the reported events.
19
+
12
20
  ## [5.0.0] - 2026-06-24
13
21
 
14
22
  ### Added
package/README.md CHANGED
@@ -30,6 +30,22 @@ mic.on('data', (chunk) => { /* Buffer of Int16 PCM samples */ });
30
30
  setTimeout(() => mic.stop(), 5000);
31
31
  ```
32
32
 
33
+ ### Condition and analyze a file
34
+
35
+ ```javascript
36
+ const { File } = require('decibri');
37
+
38
+ // The same conditioning chain as the live microphone, over a WAV file.
39
+ const file = await File.open('clip.wav', { denoise: 'fastenhancer-t', highpass: 80 });
40
+ file.on('data', (chunk) => { /* Buffer of conditioned Int16 PCM */ });
41
+ file.on('end', () => console.log('done'));
42
+
43
+ // Whole-file speech analysis (a live stream cannot do this).
44
+ const f = await File.open('clip.wav', { vad: 'silero' });
45
+ const report = await f.analyze();
46
+ for (const s of report.segments) console.log(s.start, s.end); // seconds
47
+ ```
48
+
33
49
  ### Play audio
34
50
 
35
51
  ```javascript
@@ -321,6 +337,18 @@ setTimeout(() => mic.stop(), 5000);
321
337
 
322
338
  VAD reads the signal before the chain, so `vadScore` and the `'speech'` / `'silence'` events are unaffected by which conditioning stages you enable. The conditioning chain runs in the native Node.js capture path; the browser build does not include it.
323
339
 
340
+ ## API: File (offline source)
341
+
342
+ Everything a `Microphone` does to live audio, `File` does to audio you already have: the same conditioning options, the same Readable stream of conditioned chunks (finite: it ends at EOF), and the same opt-in `vad`. Because a `File` is a complete recording, it can also analyze the whole recording for speech.
343
+
344
+ - `await File.open(path, options?)`: read a WAV off the event loop (recommended, like `Microphone.open`).
345
+ - `new File(path, options?)`: the same result, synchronous (blocks on disk I/O; fine for scripts).
346
+ - `File.buffer(samples, options)`: wrap a `Float32Array` of samples you already hold. `options.inputRate` is required (raw samples carry no header); a raw `Buffer` of bytes is rejected as ambiguous.
347
+ - `await file.analyze()` (also spelled `analyse()`): consume the source and resolve to a `VadReport` of per-window `scores` (`{ start, end, vadScore, isSpeech }`) and merged speech `segments` (`{ start, end }`), in seconds of file time. Requires `vad: 'silero'`; a `File` opened without `vad` rejects with `analysis requires VAD`.
348
+ - `file.vadScore`, `'speech'` / `'silence'` events: per-chunk VAD alongside the stream, with the holdoff measured in FILE time (sample positions), never wall-clock time, so processing speed does not change the reported events.
349
+
350
+ Options mirror `Microphone` (`sampleRate`, `dtype`, `vad`, `dcRemoval`, `denoise`, `highpass`, `agc`, `limiter`); the live-capture options (`device`, `channels`, `framesPerBuffer`) do not apply. Iteration and analysis are separate single passes: construct one `File` per operation. Note: Node also has a global `File` (the web File API); import decibri's explicitly to avoid shadowing surprises.
351
+
324
352
  ## Device Selection
325
353
 
326
354
  ```javascript
@@ -70,7 +70,7 @@ var decibri = (function() {
70
70
  var require_decibri_browser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
71
71
  const { Emitter } = require_emitter();
72
72
  const { WORKLET_SOURCE } = require_worklet_inline();
73
- const VERSION = "5.0.0";
73
+ const VERSION = "5.1.0";
74
74
  /**
75
75
  * Browser microphone capture.
76
76
  *
package/index.d.ts CHANGED
@@ -78,6 +78,52 @@ export declare class DecibriOutputBridge {
78
78
  static version(): VersionInfoJs
79
79
  }
80
80
 
81
+ /**
82
+ * Native offline-source handle exposed to Node.js via napi-rs. The public
83
+ * `File` Readable lives in the JS wrapper; consumers construct that, not
84
+ * this handle, directly.
85
+ */
86
+ export declare class FileHandle {
87
+ /**
88
+ * Open a WAV path as an offline source, synchronously (blocks on disk
89
+ * I/O; the JS wrapper's async `File.open` uses `openAsync` instead).
90
+ */
91
+ static open(path: string, options?: FileOptions | undefined | null): FileHandle
92
+ /**
93
+ * Open a WAV path without blocking the JS event loop: the disk read,
94
+ * WAV parse, and chain construction run on the libuv thread pool.
95
+ */
96
+ static openAsync(path: string, options?: FileOptions | undefined | null): Promise<unknown>
97
+ /**
98
+ * Wrap in-memory samples as an offline source. `samples` are mono f32 in
99
+ * [-1.0, 1.0]; `inputRate` is their native rate (raw samples carry no
100
+ * header). No I/O, so construction is synchronous.
101
+ */
102
+ static buffer(samples: Float32Array, inputRate: number, options?: FileOptions | undefined | null): FileHandle
103
+ /**
104
+ * Pull the next conditioned chunk, advancing the per-chunk VAD score on
105
+ * the pre-conditioning feed. Returns `null` once the source is fully
106
+ * delivered (after the end-of-stream tail) or already consumed.
107
+ */
108
+ readChunk(): Buffer | null
109
+ /**
110
+ * Consume the source with the core's whole-recording analysis, off the
111
+ * JS event loop. Resolves to the `VadReport`; a `File` built without VAD
112
+ * rejects with the core's typed error, never a silently constructed
113
+ * detector.
114
+ */
115
+ analyze(): Promise<unknown>
116
+ /** Release the source. Idempotent; a closed File reads as ended. */
117
+ close(): void
118
+ /**
119
+ * Most recent per-chunk VAD score (0.0 to 1.0), computed on the
120
+ * pre-conditioning feed. 0.0 before the first chunk or with VAD off.
121
+ */
122
+ get vadProbability(): number
123
+ /** The target output rate every delivered chunk carries. */
124
+ get sampleRate(): number
125
+ }
126
+
81
127
  /**
82
128
  * Options passed from JS constructor.
83
129
  *
@@ -170,6 +216,26 @@ export interface DeviceInfoJs {
170
216
  isDefault: boolean
171
217
  }
172
218
 
219
+ /**
220
+ * Options passed from the JS `File` wrapper. The conditioning fields mirror
221
+ * `DecibriOptions` exactly; the live-capture-only fields (device, channels,
222
+ * framesPerBuffer) do not apply to an offline source. `vadThreshold` and
223
+ * `vadHoldoffMs` are internal plumbing (the user passes them on the `vad`
224
+ * config object; the wrapper resolves them), hidden from the generated
225
+ * TypeScript like `ortLibraryPath`.
226
+ */
227
+ export interface FileOptions {
228
+ sampleRate?: number
229
+ format?: string
230
+ vadMode?: string
231
+ modelPath?: string
232
+ dcRemoval?: boolean
233
+ denoise?: string
234
+ highpass?: number
235
+ agc?: number
236
+ limiter?: number
237
+ }
238
+
173
239
  /** Output device info returned to JS. */
174
240
  export interface OutputDeviceInfoJs {
175
241
  index: number
@@ -184,6 +250,32 @@ export interface OutputDeviceInfoJs {
184
250
  isDefault: boolean
185
251
  }
186
252
 
253
+ /** One merged speech region of a recording, in seconds of file time. */
254
+ export interface Segment {
255
+ start: number
256
+ end: number
257
+ }
258
+
259
+ /**
260
+ * The whole-recording analysis `File.analyze()` resolves to: per-window
261
+ * scores and merged speech segments, in file order.
262
+ */
263
+ export interface VadReport {
264
+ scores: Array<VadWindow>
265
+ segments: Array<Segment>
266
+ }
267
+
268
+ /**
269
+ * One scored voice-activity window of a recording: `start` / `end` in
270
+ * seconds of file time, the speech probability, and the raw threshold test.
271
+ */
272
+ export interface VadWindow {
273
+ start: number
274
+ end: number
275
+ vadScore: number
276
+ isSpeech: boolean
277
+ }
278
+
187
279
  /** Version info returned to JS. */
188
280
  export interface VersionInfoJs {
189
281
  decibri: string
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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '5.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 5.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 !== '5.0.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 5.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '5.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 5.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
@@ -588,3 +588,4 @@ if (!nativeBinding) {
588
588
  module.exports = nativeBinding
589
589
  module.exports.DecibriBridge = nativeBinding.DecibriBridge
590
590
  module.exports.DecibriOutputBridge = nativeBinding.DecibriOutputBridge
591
+ module.exports.FileHandle = nativeBinding.FileHandle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decibri",
3
- "version": "5.0.0",
3
+ "version": "5.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,10 +72,10 @@
72
72
  "MIGRATION.md"
73
73
  ],
74
74
  "optionalDependencies": {
75
- "@decibri/decibri-win32-x64-msvc": "5.0.0",
76
- "@decibri/decibri-darwin-arm64": "5.0.0",
77
- "@decibri/decibri-linux-x64-gnu": "5.0.0",
78
- "@decibri/decibri-linux-arm64-gnu": "5.0.0"
75
+ "@decibri/decibri-win32-x64-msvc": "5.1.0",
76
+ "@decibri/decibri-darwin-arm64": "5.1.0",
77
+ "@decibri/decibri-linux-x64-gnu": "5.1.0",
78
+ "@decibri/decibri-linux-arm64-gnu": "5.1.0"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@napi-rs/cli": "^3.7.0"
@@ -6,7 +6,7 @@ const { WORKLET_SOURCE } = require('./worklet-inline.js');
6
6
  // Browser build version. Keep in sync with package.json on each release; the
7
7
  // browser bundle cannot read package.json at runtime the way the Node wrapper
8
8
  // does, so this is a maintained constant.
9
- const VERSION = '5.0.0';
9
+ const VERSION = '5.1.0';
10
10
 
11
11
  /**
12
12
  * Browser microphone capture.
package/src/decibri.d.ts CHANGED
@@ -276,6 +276,235 @@ export declare class Microphone extends Readable {
276
276
  }
277
277
 
278
278
  /** Information about an available audio output device. */
279
+ /** Constructor options for `File`. */
280
+ export interface FileOptions extends ReadableOptions {
281
+ /**
282
+ * Target output rate in Hz: the rate every delivered chunk carries. The
283
+ * source's input rate (from the WAV header, or `inputRate` for
284
+ * `File.buffer`) is resampled to this rate, so a 44.1 kHz recording comes
285
+ * out at 16 kHz unless you set `sampleRate`. The same meaning the option
286
+ * has on `Microphone`.
287
+ * @default 16000
288
+ * @range 1000–384000
289
+ */
290
+ sampleRate?: number;
291
+
292
+ /**
293
+ * Sample encoding data type of the delivered chunks.
294
+ * - `'int16'`: 16-bit signed integer, little-endian (2 bytes per sample)
295
+ * - `'float32'`: 32-bit IEEE 754 float, little-endian (4 bytes per sample)
296
+ * @default 'int16'
297
+ */
298
+ dtype?: 'int16' | 'float32';
299
+
300
+ /**
301
+ * Voice activity detection, opt-in exactly as on `Microphone`: `false`
302
+ * (default), `'silero'`, `'energy'`, or a `VadOptions` config object.
303
+ * When enabled, per-chunk detection runs alongside the stream (the
304
+ * `'speech'` / `'silence'` events and `vadScore`, with the holdoff
305
+ * measured in FILE time rather than wall-clock time) and `'silero'`
306
+ * additionally enables the whole-file `analyze()`. With no `vad` set the
307
+ * File simply conditions audio: no scores, no segments, no speech events.
308
+ * @default false
309
+ */
310
+ vad?: false | 'silero' | 'energy' | VadOptions;
311
+
312
+ /**
313
+ * Path to the Silero VAD ONNX model file.
314
+ * Only used when `vad` is `'silero'`.
315
+ * Defaults to `models/silero_vad.onnx` relative to the package.
316
+ */
317
+ modelPath?: string;
318
+
319
+ /**
320
+ * Remove a constant (DC) offset with a one-pole DC-blocking high-pass,
321
+ * exactly as on `Microphone`.
322
+ * @default undefined
323
+ */
324
+ dcRemoval?: boolean;
325
+
326
+ /**
327
+ * Single-channel speech enhancement (denoise) model, exactly as on
328
+ * `Microphone`. The only accepted value is `'fastenhancer-t'`.
329
+ * @default undefined
330
+ */
331
+ denoise?: 'fastenhancer-t';
332
+
333
+ /**
334
+ * High-pass filter cutoff in Hz (`80` or `100`), exactly as on
335
+ * `Microphone`.
336
+ * @default undefined
337
+ */
338
+ highpass?: 80 | 100;
339
+
340
+ /**
341
+ * Automatic gain control target level in dBFS, exactly as on `Microphone`.
342
+ * @default undefined
343
+ * @range -40 to -3
344
+ */
345
+ agc?: number;
346
+
347
+ /**
348
+ * Peak limiter ceiling in dBFS (sample-peak), exactly as on `Microphone`.
349
+ * @default undefined
350
+ * @range -3.0 to 0.0
351
+ */
352
+ limiter?: number;
353
+ }
354
+
355
+ /** Options for `File.buffer`: `FileOptions` plus the samples' native rate. */
356
+ export interface FileBufferOptions extends FileOptions {
357
+ /**
358
+ * The native rate of the in-memory samples in Hz. Required: raw samples
359
+ * carry no header to read a rate from. The samples are resampled from this
360
+ * rate to `sampleRate`.
361
+ * @range 1000–384000
362
+ */
363
+ inputRate: number;
364
+ }
365
+
366
+ /**
367
+ * One scored voice-activity window of a recording, produced by
368
+ * `File.analyze()`. Windows tile the recording from the start in fixed steps
369
+ * (512 samples at 16 kHz, 32 ms per window); a trailing remainder shorter
370
+ * than one window is not scored, exactly as live detection leaves a
371
+ * sub-window remainder unscored.
372
+ */
373
+ export interface VadWindow {
374
+ /** Window start, in seconds of file time. */
375
+ start: number;
376
+ /** Window end, in seconds of file time. */
377
+ end: number;
378
+ /**
379
+ * Speech probability for this window (0 to 1). The same quantity the live
380
+ * per-chunk `vadScore` reports, here per window across the whole recording.
381
+ */
382
+ vadScore: number;
383
+ /**
384
+ * Whether `vadScore` meets the configured threshold. The raw per-window
385
+ * test, not the debounced speaking state.
386
+ */
387
+ isSpeech: boolean;
388
+ }
389
+
390
+ /**
391
+ * One merged speech region of a recording, produced by `File.analyze()`:
392
+ * consecutive speech windows whose silence gaps are within the configured
393
+ * holdoff collapse into one segment. The segment ends at the last speech
394
+ * window, not at the holdoff expiry.
395
+ */
396
+ export interface Segment {
397
+ /** Region start, in seconds of file time. */
398
+ start: number;
399
+ /** Region end, in seconds of file time. */
400
+ end: number;
401
+ }
402
+
403
+ /** The whole-recording voice-activity analysis `File.analyze()` resolves to. */
404
+ export interface VadReport {
405
+ /** Per-window speech scores across the whole recording, in file order. */
406
+ scores: VadWindow[];
407
+ /** Merged speech regions across the whole recording, in file order. */
408
+ segments: Segment[];
409
+ }
410
+
411
+ /**
412
+ * Offline audio source: conditions a recording or in-memory samples through
413
+ * the same chain as the live `Microphone`, delivered as a finite Readable
414
+ * stream of conditioned chunks that ends at EOF (after the chain's
415
+ * end-of-stream tail). Because a `File` is a complete recording, it can also
416
+ * analyze the whole recording for speech with `analyze()` / `analyse()`,
417
+ * which a live stream cannot do.
418
+ *
419
+ * Construction: `new File(path)` reads the WAV synchronously (fine for a
420
+ * script; it blocks the event loop on disk I/O), `await File.open(path)` reads
421
+ * it off the event loop (the recommended form, mirroring `Microphone.open`),
422
+ * and `File.buffer(samples, { inputRate })` wraps a `Float32Array` of samples
423
+ * you already hold (a raw `Buffer` of bytes is rejected as ambiguous).
424
+ *
425
+ * Iteration and analysis are separate single passes: each consumes the source
426
+ * once, so construct one `File` per operation.
427
+ *
428
+ * Note: Node also has a global `File` (the web File API). Import decibri's
429
+ * explicitly (`const { File } = require('decibri')`) or reference it as
430
+ * `decibri.File` to avoid shadowing surprises.
431
+ *
432
+ * @example
433
+ * const { File } = require('decibri');
434
+ * const file = await File.open('clip.wav', { denoise: 'fastenhancer-t' });
435
+ * file.on('data', (chunk) => { /* Buffer of conditioned Int16 PCM *\/ });
436
+ * file.on('end', () => console.log('done'));
437
+ *
438
+ * @example
439
+ * // Where is the speech?
440
+ * const f = await File.open('clip.wav', { vad: 'silero' });
441
+ * const report = await f.analyze();
442
+ * for (const s of report.segments) console.log(s.start, s.end);
443
+ */
444
+ export declare class File extends Readable {
445
+ /**
446
+ * Open a WAV file synchronously (blocks on disk I/O; prefer `File.open`
447
+ * in servers). Supports 16-bit PCM and 32-bit float WAV files; the input
448
+ * rate and channel count come from the header.
449
+ */
450
+ constructor(path: string, options?: FileOptions);
451
+
452
+ /**
453
+ * Open a WAV file without blocking the event loop: the disk read, WAV
454
+ * parse, and chain construction run on the native thread pool. The
455
+ * recommended form, mirroring `Microphone.open`.
456
+ */
457
+ static open(path: string, options?: FileOptions): Promise<File>;
458
+
459
+ /**
460
+ * Wrap in-memory samples as an offline source. `samples` must be a
461
+ * `Float32Array` of mono samples in [-1.0, 1.0]; a raw `Buffer` of PCM
462
+ * bytes is rejected as ambiguous. `inputRate` is required (raw samples
463
+ * carry no header). Synchronous: no I/O is involved.
464
+ */
465
+ static buffer(samples: Float32Array, options: FileBufferOptions): File;
466
+
467
+ /**
468
+ * Most recent per-chunk VAD score for the active mode: the Silero speech
469
+ * probability in `'silero'` mode, the normalized RMS of the
470
+ * pre-conditioning signal in `'energy'` mode. 0 when VAD is disabled or
471
+ * before the first chunk.
472
+ */
473
+ readonly vadScore: number;
474
+
475
+ /**
476
+ * Analyze the whole recording for speech, off the event loop. Resolves to
477
+ * a `VadReport` with per-window `scores` and merged speech `segments`,
478
+ * all in seconds of file time. Consumes the source (a `File` is a single
479
+ * pass). Requires `vad: 'silero'`: a File opened without `vad` rejects
480
+ * with the core's "analysis requires VAD" error, and the energy mode has
481
+ * no whole-file analysis.
482
+ */
483
+ analyze(): Promise<VadReport>;
484
+
485
+ /** The same whole-recording analysis under the international spelling. */
486
+ analyse(): Promise<VadReport>;
487
+
488
+ /** Release the source. Idempotent; a closed File reads as ended. */
489
+ close(): void;
490
+
491
+ on(event: 'data', listener: (chunk: Buffer) => void): this;
492
+ on(event: 'end', listener: () => void): this;
493
+ on(event: 'error', listener: (err: Error) => void): this;
494
+ /** Speech detected (VAD enabled), at a FILE-time boundary. */
495
+ on(event: 'speech', listener: () => void): this;
496
+ /** Silence holdoff elapsed (VAD enabled), in FILE time. */
497
+ on(event: 'silence', listener: () => void): this;
498
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
499
+
500
+ once(event: 'data', listener: (chunk: Buffer) => void): this;
501
+ once(event: 'end', listener: () => void): this;
502
+ once(event: 'error', listener: (err: Error) => void): this;
503
+ once(event: 'speech', listener: () => void): this;
504
+ once(event: 'silence', listener: () => void): this;
505
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
506
+ }
507
+
279
508
  export interface SpeakerInfo {
280
509
  /** Device index (pass to constructor as `device`). */
281
510
  index: number;
package/src/decibri.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { Readable } = require('stream');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const { DecibriBridge } = require('../index.js');
6
+ const { DecibriBridge, FileHandle } = require('../index.js');
7
7
  const {
8
8
  wrapNativeError,
9
9
  DecibriError,
@@ -560,9 +560,394 @@ function version() {
560
560
  return Microphone.version();
561
561
  }
562
562
 
563
+ // ─── File (Readable): offline source ─────────────────────────────────────────
564
+
565
+ class File extends Readable {
566
+ /**
567
+ * Open a WAV file as an offline source, synchronously. Everything a
568
+ * `Microphone` does to live audio, a `File` does to audio you already
569
+ * have: the same conditioning options, the same stream of conditioned
570
+ * chunks, and (with `vad` set) the same per-chunk speech events, plus the
571
+ * whole-file `analyze()` a live stream cannot offer.
572
+ *
573
+ * The bare constructor reads the WAV inline, blocking the event loop on
574
+ * disk I/O; prefer `await File.open(path, options)` in servers and other
575
+ * latency-sensitive code, exactly as `Microphone.open` is preferred over
576
+ * `new Microphone`. Iteration and analysis are separate single passes:
577
+ * each consumes the source once, so use one `File` per operation.
578
+ *
579
+ * @param {string} filePath Path to a WAV file (16-bit PCM or 32-bit float).
580
+ * @param {import('./decibri').FileOptions} [options]
581
+ * @param {{ prepared: object, native: object }} [_internal] Internal: a
582
+ * pre-resolved options bundle and an already-constructed native handle,
583
+ * passed by the async `File.open()` factory and by `File.buffer()`. Not
584
+ * part of the public API.
585
+ */
586
+ constructor(filePath, options = {}, _internal = undefined) {
587
+ super({ highWaterMark: options.highWaterMark, objectMode: false });
588
+
589
+ const prepared = _internal ? _internal.prepared : File._prepareOptions(options);
590
+
591
+ // ── Store config ───────────────────────────────────────────────────────
592
+
593
+ this._vad = prepared.vadEnabled;
594
+ this._vadMode = prepared.vadMode;
595
+ this._vadThreshold = prepared.vadThreshold;
596
+ // The speaking holdoff on a File is measured in FILE time (sample
597
+ // positions converted to seconds), never wall-clock time: a file
598
+ // processes faster than real time, so a wall-clock timer would collapse
599
+ // the reported speech timing. Positions advance as chunks are pulled.
600
+ this._vadHoldoffSeconds = prepared.vadHoldoff / 1000;
601
+ this._vadScore = 0;
602
+ this._isSpeaking = false;
603
+ this._silenceStartPos = null;
604
+ this._position = 0;
605
+ this._sampleRate = prepared.nativeOptions.sampleRate;
606
+ this._bytesPerSample = prepared.dtype === 'int16' ? 2 : 4;
607
+ this._ended = false;
608
+
609
+ // ── Create or adopt native handle ───────────────────────────────────────
610
+
611
+ if (_internal) {
612
+ this._native = _internal.native;
613
+ } else {
614
+ if (typeof filePath !== 'string') {
615
+ throw new TypeError('path must be a string');
616
+ }
617
+ try {
618
+ this._native = FileHandle.open(filePath, prepared.nativeOptions);
619
+ } catch (err) {
620
+ throw wrapNativeError(err);
621
+ }
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Validate the constructor options and resolve them into the native options
627
+ * object plus the wrapper-side state. The checks and messages mirror
628
+ * `Microphone._prepareOptions` exactly for every shared option; the
629
+ * live-capture-only options (device, channels, framesPerBuffer) do not
630
+ * apply to an offline source.
631
+ * @internal
632
+ * @param {import('./decibri').FileOptions} options
633
+ */
634
+ static _prepareOptions(options) {
635
+ const sampleRate = options.sampleRate ?? 16000;
636
+ if (sampleRate < 1000 || sampleRate > 384000) {
637
+ throw new RangeError('sample rate must be between 1000 and 384000');
638
+ }
639
+
640
+ const dtype = options.dtype ?? 'int16';
641
+ if (dtype !== 'int16' && dtype !== 'float32') {
642
+ throw new TypeError("dtype must be 'int16' or 'float32'");
643
+ }
644
+
645
+ // ── Validate VAD options (same acceptance as Microphone) ────────────────
646
+
647
+ const vad = options.vad ?? false;
648
+ let vadEnabled;
649
+ let vadMode;
650
+ let vadThreshold;
651
+ let vadHoldoff;
652
+ if (vad === false) {
653
+ vadEnabled = false;
654
+ vadMode = 'energy'; // inert placeholder; ignored while disabled
655
+ } else if (vad === true) {
656
+ throw new TypeError(
657
+ "vad: true is no longer supported. Specify the mode explicitly: vad: 'silero' or vad: 'energy'."
658
+ );
659
+ } else if (vad === 'silero' || vad === 'energy') {
660
+ vadEnabled = true;
661
+ vadMode = vad;
662
+ } else if (vad !== null && typeof vad === 'object' && !Array.isArray(vad)) {
663
+ const { model, threshold, holdoffMs } = vad;
664
+ if (model !== 'silero' && model !== 'energy') {
665
+ throw new TypeError(
666
+ `Invalid vad model: ${JSON.stringify(model)}. Expected 'silero' or 'energy'.`
667
+ );
668
+ }
669
+ vadEnabled = true;
670
+ vadMode = model;
671
+ if (threshold !== undefined) {
672
+ if (typeof threshold !== 'number' || Number.isNaN(threshold)) {
673
+ throw new TypeError('vad threshold must be a number');
674
+ }
675
+ if (threshold < 0 || threshold > 1) {
676
+ throw new RangeError('vad threshold must be between 0 and 1');
677
+ }
678
+ vadThreshold = threshold;
679
+ }
680
+ if (holdoffMs !== undefined) {
681
+ if (typeof holdoffMs !== 'number' || Number.isNaN(holdoffMs)) {
682
+ throw new TypeError('vad holdoffMs must be a number');
683
+ }
684
+ if (holdoffMs < 0) {
685
+ throw new RangeError('vad holdoffMs must be non-negative');
686
+ }
687
+ vadHoldoff = holdoffMs;
688
+ }
689
+ } else {
690
+ throw new TypeError(
691
+ `Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero', 'energy', or a config object { model, threshold, holdoffMs }.`
692
+ );
693
+ }
694
+
695
+ let modelPath = undefined;
696
+ if (vadEnabled && vadMode === 'silero') {
697
+ modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
698
+ if (!fs.existsSync(modelPath)) {
699
+ throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
700
+ }
701
+ }
702
+
703
+ // ── Conditioning options (identical checks to Microphone) ───────────────
704
+
705
+ const dcRemoval = options.dcRemoval;
706
+
707
+ const denoise = options.denoise;
708
+ let denoiseModelPath = undefined;
709
+ if (denoise !== undefined) {
710
+ if (denoise !== 'fastenhancer-t') {
711
+ throw new TypeError(
712
+ `Invalid denoise value: ${JSON.stringify(denoise)}. Expected 'fastenhancer-t'.`
713
+ );
714
+ }
715
+ denoiseModelPath = path.join(__dirname, '..', 'models', 'fastenhancer_t.onnx');
716
+ if (!fs.existsSync(denoiseModelPath)) {
717
+ throw new Error(`Denoise model not found at ${denoiseModelPath}. Ensure the models/ directory is included in your installation.`);
718
+ }
719
+ }
720
+
721
+ const highpass = options.highpass;
722
+ if (highpass !== undefined && highpass !== 80 && highpass !== 100) {
723
+ throw new RangeError('highpass must be one of: 80, 100');
724
+ }
725
+
726
+ const agc = options.agc;
727
+ if (agc !== undefined && (agc < -40 || agc > -3)) {
728
+ throw new RangeError('agc target level must be between -40 and -3');
729
+ }
730
+
731
+ const limiter = options.limiter;
732
+ if (limiter !== undefined && (limiter < -3.0 || limiter > 0.0)) {
733
+ throw new RangeError('limiter ceiling must be between -3.0 and 0.0');
734
+ }
735
+
736
+ let ortLibraryPath = undefined;
737
+ if ((vadEnabled && vadMode === 'silero') || denoise !== undefined) {
738
+ ortLibraryPath = resolveBundledOrtPath();
739
+ }
740
+
741
+ return {
742
+ dtype,
743
+ vadEnabled,
744
+ vadMode,
745
+ vadThreshold: vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
746
+ vadHoldoff: vadHoldoff ?? 300,
747
+ nativeOptions: {
748
+ sampleRate,
749
+ format: dtype,
750
+ // Pass the mode to native only when VAD is enabled, exactly as the
751
+ // Microphone options do; absent means VAD off in native.
752
+ vadMode: vadEnabled ? vadMode : undefined,
753
+ // The whole-file analysis applies threshold and holdoff in the core
754
+ // (segment merging in file time), so both cross the boundary here,
755
+ // unlike the live path where the policy is wrapper-only.
756
+ vadThreshold: vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
757
+ vadHoldoffMs: vadHoldoff ?? 300,
758
+ modelPath,
759
+ dcRemoval,
760
+ denoise,
761
+ denoiseModelPath,
762
+ ortLibraryPath,
763
+ highpass,
764
+ agc,
765
+ limiter,
766
+ },
767
+ };
768
+ }
769
+
770
+ /**
771
+ * Open a WAV file without blocking the event loop: the disk read, WAV
772
+ * parse, and chain construction run on the native thread pool. The
773
+ * recommended form in Node, mirroring `Microphone.open`. The synchronous
774
+ * `new File(path)` remains available for scripts.
775
+ *
776
+ * @param {string} filePath
777
+ * @param {import('./decibri').FileOptions} [options]
778
+ * @returns {Promise<File>}
779
+ */
780
+ static async open(filePath, options = {}) {
781
+ if (typeof filePath !== 'string') {
782
+ throw new TypeError('path must be a string');
783
+ }
784
+ const prepared = File._prepareOptions(options);
785
+ let native;
786
+ try {
787
+ native = await FileHandle.openAsync(filePath, prepared.nativeOptions);
788
+ } catch (err) {
789
+ throw wrapNativeError(err);
790
+ }
791
+ return new File(filePath, options, { prepared, native });
792
+ }
793
+
794
+ /**
795
+ * Wrap in-memory samples as an offline source. `samples` must be a
796
+ * `Float32Array` of mono samples in [-1.0, 1.0]; a raw `Buffer` of PCM
797
+ * bytes is rejected as ambiguous (encoded bytes, int16 PCM, and f32
798
+ * samples are indistinguishable, and decibri's own capture output is a
799
+ * `Buffer`). Raw samples carry no header, so `inputRate` (their native
800
+ * rate) is required; `sampleRate` stays the target output rate. No I/O,
801
+ * so construction is synchronous.
802
+ *
803
+ * @param {Float32Array} samples
804
+ * @param {import('./decibri').FileBufferOptions} [options]
805
+ * @returns {File}
806
+ */
807
+ static buffer(samples, options = {}) {
808
+ if (Buffer.isBuffer(samples)) {
809
+ throw new TypeError(
810
+ 'File.buffer requires a Float32Array of samples, not a Buffer of bytes'
811
+ );
812
+ }
813
+ if (!(samples instanceof Float32Array)) {
814
+ throw new TypeError('File.buffer requires a Float32Array of samples');
815
+ }
816
+ const inputRate = options.inputRate;
817
+ if (typeof inputRate !== 'number' || Number.isNaN(inputRate)) {
818
+ throw new TypeError('inputRate is required for File.buffer (samples carry no header)');
819
+ }
820
+ if (inputRate < 1000 || inputRate > 384000) {
821
+ throw new RangeError('inputRate must be between 1000 and 384000');
822
+ }
823
+ const prepared = File._prepareOptions(options);
824
+ let native;
825
+ try {
826
+ native = FileHandle.buffer(samples, inputRate, prepared.nativeOptions);
827
+ } catch (err) {
828
+ throw wrapNativeError(err);
829
+ }
830
+ return new File(null, options, { prepared, native });
831
+ }
832
+
833
+ /** @internal */
834
+ _read() {
835
+ if (this._ended) {
836
+ return;
837
+ }
838
+ let chunk;
839
+ try {
840
+ // One chunk per pull: the conditioning compute runs synchronously
841
+ // here, so pulling one chunk at a time keeps the event loop breathing
842
+ // between chunks while the stream machinery re-calls _read on demand.
843
+ chunk = this._native.readChunk();
844
+ } catch (err) {
845
+ this.destroy(wrapNativeError(err));
846
+ return;
847
+ }
848
+ if (chunk === null || chunk === undefined) {
849
+ this._ended = true;
850
+ this.push(null); // finite source: the stream ends at EOF
851
+ return;
852
+ }
853
+ if (this._vad) {
854
+ // Both modes read the score from native, computed on the signal
855
+ // before the opt-in conditioning step, exactly as the live pump does.
856
+ this._processVadValue(this._native.vadProbability, chunk.length);
857
+ } else {
858
+ this._position += chunk.length / this._bytesPerSample / this._sampleRate;
859
+ }
860
+ this.push(chunk);
861
+ }
862
+
863
+ /**
864
+ * @internal Speech/silence state machine in FILE time. The same policy as
865
+ * the Microphone's wall-clock machine, with the holdoff measured in
866
+ * seconds of audio position instead of a timer: state flips only as the
867
+ * file's own timeline passes the holdoff, so processing speed never
868
+ * changes the reported events.
869
+ */
870
+ _processVadValue(value, chunkBytes) {
871
+ const chunkStart = this._position;
872
+ const chunkEnd = chunkStart + chunkBytes / this._bytesPerSample / this._sampleRate;
873
+ this._position = chunkEnd;
874
+ this._vadScore = value;
875
+ if (value >= this._vadThreshold) {
876
+ this._silenceStartPos = null;
877
+ if (!this._isSpeaking) {
878
+ this._isSpeaking = true;
879
+ this.emit('speech');
880
+ }
881
+ } else if (this._isSpeaking) {
882
+ if (this._silenceStartPos === null) {
883
+ this._silenceStartPos = chunkStart;
884
+ }
885
+ if (chunkEnd - this._silenceStartPos >= this._vadHoldoffSeconds) {
886
+ this._isSpeaking = false;
887
+ this._silenceStartPos = null;
888
+ this.emit('silence');
889
+ }
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Most recent per-chunk VAD score for the active mode: the Silero speech
895
+ * probability in 'silero' mode, the normalized RMS of the pre-conditioning
896
+ * signal in 'energy' mode. 0 when VAD is disabled or before the first
897
+ * chunk. The same quantity the live `Microphone.vadScore` reports.
898
+ * @returns {number}
899
+ */
900
+ get vadScore() {
901
+ return this._vadScore;
902
+ }
903
+
904
+ /**
905
+ * Analyze the whole recording for speech. Runs the recording once through
906
+ * the conditioning pass off the event loop and resolves to a `VadReport`:
907
+ * per-window `scores` (`{ start, end, vadScore, isSpeech }`) and merged
908
+ * speech `segments` (`{ start, end }`), all in seconds of file time.
909
+ * Consumes the source: analysis and iteration are separate single passes.
910
+ *
911
+ * Requires VAD: a `File` opened without `vad` rejects with the core's
912
+ * "analysis requires VAD" error (a `RangeError`); the energy mode has no
913
+ * whole-file analysis and rejects likewise. Never constructs a detector
914
+ * silently.
915
+ *
916
+ * @returns {Promise<import('./decibri').VadReport>}
917
+ */
918
+ async analyze() {
919
+ if (this._vad && this._vadMode === 'energy') {
920
+ throw new RangeError(
921
+ "analyze() requires vad: 'silero'; energy mode does not support whole-file analysis"
922
+ );
923
+ }
924
+ try {
925
+ return await this._native.analyze();
926
+ } catch (err) {
927
+ throw wrapNativeError(err);
928
+ }
929
+ }
930
+
931
+ /**
932
+ * The same whole-recording analysis under the international spelling.
933
+ * @returns {Promise<import('./decibri').VadReport>}
934
+ */
935
+ analyse() {
936
+ return this.analyze();
937
+ }
938
+
939
+ /**
940
+ * Release the source. Idempotent; a closed File reads as ended.
941
+ */
942
+ close() {
943
+ this._native.close();
944
+ }
945
+ }
946
+
563
947
  module.exports = {
564
948
  Microphone,
565
949
  Speaker,
950
+ File,
566
951
  inputDevices,
567
952
  outputDevices,
568
953
  version,
package/src/errors.js CHANGED
@@ -99,7 +99,8 @@ function wrapNativeError(err) {
99
99
  msg.startsWith('frames per buffer must be between') ||
100
100
  msg.startsWith('Silero VAD only supports') ||
101
101
  msg.startsWith('VAD threshold must be between') ||
102
- msg.startsWith('device index out of range')
102
+ msg.startsWith('device index out of range') ||
103
+ msg.startsWith('analysis requires VAD')
103
104
  ) {
104
105
  return new RangeError(msg);
105
106
  }