decibri 5.6.0 → 5.8.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,39 @@ For other decibri packages, see:
9
9
  - Rust core: [crates/decibri/CHANGELOG.md](../../crates/decibri/CHANGELOG.md)
10
10
  - Python package: [bindings/python/CHANGELOG.md](../../bindings/python/CHANGELOG.md)
11
11
 
12
+ ## [5.8.0] - 2026-10-04
13
+
14
+ ### Breaking changes
15
+
16
+ - **The browser `Microphone` refuses the options it cannot serve: `modelPath`, `dcRemoval`, `denoise`, `highpass`, `agc`, `limiter` and `aec`.** It accepted each of them and did nothing with it, so the delivered audio was unconditioned with nothing to indicate the option was ignored. Passing any of them now throws `TypeError: <option> is not supported in the browser` from the constructor, naming the first one present in the order above, whatever its value; an explicit `undefined` counts as absent. Code passing one of these options to the browser entry has to drop it, and code sharing one options object between the Node and browser entries has to strip them for the browser. The Node entry accepts the same options as before. The browser entry's other options, `sampleRate`, `channels`, `channelMap`, `framesPerBuffer`, `device`, `dtype`, `vad`, `echoCancellation`, `noiseSuppression` and `workletUrl`, are unchanged, as is every other browser `Microphone` message.
17
+
18
+ ### Changed
19
+
20
+ - `File.save` and `AudioWriter` refuse an AIFF above 32767 channels, reported as a `DecibriError` with code `AUDIO_FORMAT_UNSUPPORTED` carrying the container layer's own text (`File.save` rejects with it; `AudioWriter` destroys the stream with it when the stream finishes), the refusal a FLAC above 8 channels and a WAV above 32767 already receive: an AIFF `COMM` chunk's `numChannels` is a signed 16-bit field, so 32767 is the format's own maximum. There is nothing a caller needs to do, because no container decibri writes accepts more. Reading an AIFF, every count up to 32767, WAV, FLAC and `File.buffer` delivery at any count are unchanged.
21
+ - A device failure during capture or playback is no longer written to stderr. The failure reaches the consumer as before: `Microphone` emits it on `'error'` with code `DEVICE_FAILED`, and `Speaker` reports it from the next `write` or `drain`.
22
+ - The `File.save` and `SaveReport.nonFiniteSamples` documentation states that when the conditioning chain runs it has already replaced every non-finite sample with silence at its entry, so the save's own replacement (NaN with silence, an infinity with full scale) and the `nonFiniteSamples` count cover the direct path only, a mono source already at `sampleRate` with no conditioning enabled. The behaviour is unchanged.
23
+ - The `Speaker.isPlaying` documentation states that it reports whether the output stream is running: `true` once a non-empty write opens it, until `stop()`, `end()` once the queued audio has played, or a device failure, with `drainAsync()` leaving it `true`. The behaviour is unchanged.
24
+
25
+ ### Fixed
26
+
27
+ - With a VAD active on a multichannel stream that has an enhancement step enabled, the detector feed holds two seconds of frames at every channel count and evicts whole frames. A stream whose delivered block exceeded the feed's bound (more than 20 channels at 16 kHz, or 60 at 48 kHz, at the default `framesPerBuffer` of 1600; fewer channels with a larger `framesPerBuffer`) lost feed frames on every block, and at a channel count that does not divide the bound also fed the detector a channel other than the one `vad: { source }` named. Mono streams and streams with no enhancement step are unchanged.
28
+ - The browser `Microphone` and `Speaker` resamplers deliver the exact number of samples for the audio they receive. Each carried the resampling position across blocks and clamped a negative carry to zero, so a sample whose time fell between one block's last input sample and the next block's first was moved onto that first sample, and one input sample's time was dropped at every such boundary: at a 48 kHz context capturing at 16 kHz the `Microphone` delivered 15,937.5 samples a second, 0.39 % short, about 14 seconds an hour, with 44.1 kHz contexts, 44.1 kHz targets and the `Speaker` at the common source rates short by between 0.03 % and 0.62 % depending on the block size. Both now keep the previous block's last sample and carry the position across the boundary as an exact integer phase, and the delivered count matches the input at every rate pair over any length of audio. Capture and playback at the context's own rate never resampled and are unchanged.
29
+ - On Windows, listing devices and constructing or starting a `Microphone` or `Speaker` work from any thread, including after a `worker_threads` worker that made the process's first device call has exited. The first device call in a process takes a usage reference on the process's multithreaded COM apartment and holds it for the life of the process. Device lists, identifiers and errors are unchanged, and so is every other platform.
30
+
31
+ ## [5.7.0] - 2026-08-21
32
+
33
+ ### Added
34
+
35
+ - Prebuilt binaries for Windows ARM64, published as the fifth platform package `@decibri/decibri-win32-arm64-msvc` and selected automatically on `win32` / `arm64`. It bundles ONNX Runtime 1.28.1 and ships the same `THIRD-PARTY-NOTICES.md` as the other platform packages. Native ARM64 Node.js previously failed at load with the napi `Cannot find native binding` message; x64 Node.js under emulation is unchanged.
36
+
37
+ ### Changed
38
+
39
+ - The bundled ONNX Runtime is 1.28.1 in every platform package, and the native addon is built against `ort` 2.0.0-rc.13 (`api-28`). An `ORT_DYLIB_PATH` override must point at ONNX Runtime 1.28 or newer.
40
+
41
+ ### Fixed
42
+
43
+ - A bundled or `ORT_DYLIB_PATH` ONNX Runtime that cannot be used (not a loadable library, or older than 1.28) rejects with `ORT_LOAD_FAILED` on every silero or denoise attempt; a second attempt after such a failure no longer aborts the process.
44
+
12
45
  ## [5.6.0] - 2026-08-16
13
46
 
14
47
  ### Breaking changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # decibri
2
2
 
3
- Cross-platform audio capture, conditioning, and playback for Node.js and browsers, with voice activity detection on live and recorded audio.
3
+ Cross-platform audio capture, conditioning, and playback for Node.js, with browser capture and voice activity detection on live and recorded audio.
4
4
 
5
5
  ## Installation
6
6
 
@@ -110,6 +110,7 @@ Creates a Readable stream that captures from the microphone.
110
110
  | `highpass` | `80` \| `100` | off | High-pass cutoff in Hz (second-order Butterworth) that removes low-frequency rumble. Runs after denoise. Out-of-set values throw a `RangeError` |
111
111
  | `agc` | number | off | AGC target level in dBFS, an integer in -40 to -3 (typical -18). Runs after the high-pass. Out-of-range throws a `RangeError` |
112
112
  | `limiter` | number | off | Peak limiter ceiling in dBFS, a number in -3.0 to 0.0 (typical -1.0). Runs last. Out-of-range throws a `RangeError` |
113
+ | `aec` | `'tau'` \| `AecOptions` | off | Acoustic echo cancellation of the far-end audio pushed through `pushAecReference`. `'tau'` names the model; an object `{ model, tailMs, suppression, referenceSampleRate, referenceChannels }` tunes it. Runs before voice activity detection, so `vadScore` and the `'speech'` / `'silence'` events read the echo-removed signal. Requires `sampleRate` in 8000 to 48000. See [Echo cancellation](#echo-cancellation) |
113
114
 
114
115
  Standard `ReadableOptions` (e.g. `highWaterMark`) are also accepted.
115
116
 
@@ -122,6 +123,8 @@ Multichannel capture delivers interleaved frames: each chunk holds `framesPerBuf
122
123
  | Method | Description |
123
124
  | --- | --- |
124
125
  | `mic.stop()` | Stop capture and end stream. Safe to call multiple times |
126
+ | `mic.pushAecReference(data)` | Queue far-end reference audio for the echo canceller, pushed as it is played, in played order. Accepts the same input shapes `Speaker.write` accepts. Never blocks and never throws on a full queue; a no-op when `aec` is unset. See [Echo cancellation](#echo-cancellation) |
127
+ | `mic.aecMetrics()` | The echo canceller's metrics, or `null` when `aec` is unset or capture is not running |
125
128
  | `Microphone.open(options?)` | Construct without blocking the event loop. Returns a `Promise<Microphone>`. See [Non-blocking API](#non-blocking-api) |
126
129
  | `Microphone.devices()` | List available input devices |
127
130
  | `Microphone.version()` | Version info: `{ decibri, audioBackend, binding }` |
@@ -168,7 +171,7 @@ Standard `WritableOptions` (e.g. `highWaterMark`) are also accepted.
168
171
  | `speaker.end()` | Signal end. Drains remaining audio, then emits `'finish'` |
169
172
  | `speaker.drainAsync()` | Wait for queued audio to finish without blocking the event loop. Returns a `Promise` |
170
173
  | `speaker.stop()` | Immediate stop. Discards remaining audio |
171
- | `speaker.isPlaying` | `true` while audio is being output |
174
+ | `speaker.isPlaying` | Whether the output stream is running: `true` once a non-empty write opens it, until `stop()`, `end()` once the queued audio has played, or a device failure. `drainAsync()` leaves it `true` |
172
175
  | `Speaker.open(options?)` | Construct without blocking the event loop. Returns a `Promise<Speaker>` |
173
176
  | `Speaker.devices()` | List available output devices |
174
177
  | `Speaker.version()` | Same as `Microphone.version()` |
@@ -229,15 +232,17 @@ The browser API uses `getUserMedia` and `AudioWorklet`. It differs from the Node
229
232
 
230
233
  ### `new Microphone(options?)` (browser)
231
234
 
232
- Same options as Node.js, plus:
235
+ Takes `sampleRate`, `channels`, `channelMap`, `framesPerBuffer`, `dtype` and `vad` as Node.js does, plus:
233
236
 
234
237
  | Option | Type | Default | Description |
235
238
  | --- | --- | --- | --- |
236
239
  | `device` | string | system default | Device ID from `Microphone.devices()` (not index) |
237
- | `echoCancellation` | boolean | true | Browser echo cancellation |
238
- | `noiseSuppression` | boolean | true | Browser noise suppression |
240
+ | `echoCancellation` | boolean | true | Browser echo cancellation, applied by the platform before decibri sees the audio |
241
+ | `noiseSuppression` | boolean | true | Browser noise suppression, applied by the platform before decibri sees the audio |
239
242
  | `workletUrl` | string | inline blob | Custom worklet URL for strict CSP |
240
243
 
244
+ The conditioning options run in the native Node.js capture path: `dcRemoval`, `denoise`, `highpass`, `agc`, `limiter`, `aec` and `modelPath`. The browser `Microphone` throws a `TypeError` naming the option if one is passed.
245
+
241
246
  The browser runs energy-mode VAD only, so its `vad` option accepts `false`, `'energy'`, or a config object `{ model: 'energy', threshold, holdoffMs, source }`. The browser `version()` returns `{ decibri }` only: the browser build has no native core, so `decibri` reports the installed package version. In Node, `version().decibri` reports the native core version.
242
247
 
243
248
  ### Key differences from Node.js
@@ -250,6 +255,8 @@ The browser runs energy-mode VAD only, so its `vad` option accepts `false`, `'en
250
255
  | `devices()` | Sync, returns array | Async, returns Promise |
251
256
  | Sample rate | Native device rate | Resampled from native rate |
252
257
  | VAD | `'silero'` or `'energy'` | `'energy'` only |
258
+ | Conditioning | ACE chain and `aec` | Platform `echoCancellation` and `noiseSuppression` |
259
+ | Offline audio | `File` and `AudioWriter` | Capture and playback only |
253
260
 
254
261
  ### `new Speaker(options?)` (browser)
255
262
 
@@ -340,6 +347,42 @@ setTimeout(() => mic.stop(), 5000);
340
347
 
341
348
  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.
342
349
 
350
+ ## Echo cancellation
351
+
352
+ Acoustic echo cancellation removes the echo of far-end audio (what your application is playing out) from the captured audio. It is opt-in: set `aec` on the `Microphone` and push the far-end audio through `pushAecReference` as it is played, in played order. With `aec` unset the capture path is unchanged, and with no reference pushed the captured audio passes through unchanged. The canceller runs before voice activity detection, so `vadScore` and the `'speech'` / `'silence'` events read the echo-removed signal and playback stops triggering detection. It requires `sampleRate` in 8000 to 48000.
353
+
354
+ `aec: 'tau'` selects the model with its defaults; an `AecOptions` object tunes it:
355
+
356
+ | Option | Default | Range |
357
+ | --- | --- | --- |
358
+ | `model` | required | `'tau'`, the one model |
359
+ | `tailMs` | 200 | 16 to 500 ms |
360
+ | `suppression` | `'conservative'` | `'conservative'` or `'off'` |
361
+ | `referenceSampleRate` | the capture `sampleRate` | 1000 to 384000 Hz |
362
+ | `referenceChannels` | 1 | 1 or more |
363
+
364
+ ```javascript
365
+ const { Microphone, Speaker } = require('decibri');
366
+
367
+ const mic = new Microphone({ sampleRate: 16000, aec: 'tau' });
368
+ const speaker = new Speaker({ sampleRate: 16000, channels: 1 });
369
+
370
+ // Push the far-end audio as it is played, in played order: the same
371
+ // input shapes Speaker.write accepts, in the microphone's dtype.
372
+ function playFarEnd(pcmBuffer) {
373
+ speaker.write(pcmBuffer);
374
+ mic.pushAecReference(pcmBuffer);
375
+ }
376
+
377
+ mic.on('data', (chunk) => { /* Buffer of echo-cancelled Int16 PCM */ });
378
+ ```
379
+
380
+ `pushAecReference` never blocks and never throws on a full queue: samples that do not fit are discarded and counted by `aecMetrics().referenceDropped`. Silence between played audio need not be pushed. The canceller reads one mono reference: a reference at a `referenceSampleRate` other than the capture rate is converted before the canceller sees it, and with `referenceChannels` above 1 each frame is averaged to one mono sample. With `channels` above 1, one canceller runs per delivered channel, each fed the same pushed reference, so one push serves every channel.
381
+
382
+ `aecMetrics()` returns the canceller's report merged with the reference queue's counters (`delaySamples`, `erleDb`, `doubleTalk`, `referenceStarved`, `acquisitionParked`, `referenceReanchors`, `referenceDropped`, `referenceSilence`), or `null` when `aec` is unset or capture is not running. Its `channels` array carries each delivered channel's report in delivered order; the top-level fields report the first delivered channel's. `erleDb` is not a quality ranking across channels: it rises with echo distance, so a far microphone reports a higher figure than a near one while removing less echo in absolute terms.
383
+
384
+ Echo cancellation runs in the native Node.js capture path; the browser `Microphone` does not take the option, because browser capture already carries the platform's own echo cancellation through its `echoCancellation` constraint, on by default.
385
+
343
386
  ## ONNX Runtime telemetry
344
387
 
345
388
  Silero mode (`vad: 'silero'`) and the ACE `denoise` stage run on ONNX Runtime, which carries its own telemetry, separate from anything decibri does. Decibri disables it on the environment it commits when it initializes the runtime. Set `DECIBRI_ORT_TELEMETRY=1` in the environment before first use to leave it enabled; every other value, an empty value, and an absent variable leave it disabled.
@@ -350,15 +393,32 @@ Neither ONNX Runtime nor this setting applies to the browser build, which has no
350
393
 
351
394
  ## API: File (offline source)
352
395
 
353
- 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.
396
+ `File` runs the same conditioning chain on audio you already have. Because a `File` is a complete recording, it can also analyze the whole recording for speech.
354
397
 
355
398
  - `await File.open(path, options?)`: read a file off the event loop (recommended, like `Microphone.open`). Reads WAV, AIFF, AIFF-C and FLAC, identified from the file's own bytes rather than its extension.
356
399
  - `new File(path, options?)`: the same result, synchronous (blocks on disk I/O; fine for scripts).
357
400
  - `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. `options.inputChannels` states the interleave of the samples, `1` by default, the channel counterpart of `inputRate`.
358
401
  - `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`.
402
+ - `await file.save(path, options?)`: write the conditioned recording to disk, off the event loop, as 16-bit PCM at `sampleRate` and at the delivered channel count, in WAV, AIFF or FLAC. The container comes from the path's extension (`.wav`, `.aiff`, `.aif`, `.aifc` or `.flac`) or from `options.format`; an extension it does not recognise rejects rather than defaulting. `options.compression` sets the FLAC compression level, 0 to 8 with 5 the default. Resolves to a `SaveReport` (`{ clippedSamples, nonFiniteSamples }`). Consumes the source, and rejects with `FILE_ENGAGED` once the stream is engaged, exactly as `analyze()` does.
359
403
  - `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.
360
404
 
361
- Options mirror `Microphone` (`sampleRate`, `channels`, `channelMap`, `dtype`, `vad`, `dcRemoval`, `denoise`, `highpass`, `agc`, `limiter`), with `channels` and `channelMap` read against the source's own channel count (the file's header, or `inputChannels` for `File.buffer`) where the live path reads the device's report, and the vad `source` naming a delivered channel exactly as on `Microphone`; the live-capture options (`device`, `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.
405
+ Options mirror `Microphone` (`sampleRate`, `channels`, `channelMap`, `dtype`, `vad`, `dcRemoval`, `denoise`, `highpass`, `agc`, `limiter`), with `channels` and `channelMap` read against the source's own channel count (the file's header, or `inputChannels` for `File.buffer`) where the live path reads the device's report, and the vad `source` naming a delivered channel exactly as on `Microphone`; the live-capture options (`device`, `framesPerBuffer`, `aec`) do not apply. A save clamps finite samples outside full scale to `[-1.0, 1.0]` and counts them in `clippedSamples`, and replaces non-finite samples before they reach the file (NaN as silence, an infinity as full scale), counted in `nonFiniteSamples`. When the conditioning chain runs it has already replaced every non-finite sample with silence at its entry, so `nonFiniteSamples` covers the direct path only, a mono source already at `sampleRate` with no conditioning enabled. Iteration, analysis and saving 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.
406
+
407
+ ## API: AudioWriter (file sink)
408
+
409
+ `AudioWriter` is a Writable file sink for PCM audio: the stream to pair with decibri's Readable sources (`File`, `Microphone`) and with any other stream of PCM bytes. It collects the whole stream, then writes it as one audio file when the stream finishes, exactly as `File.save` writes: the same containers from the same extension rule, the same 16-bit PCM encoding, the same clamp and non-finite handling, the same bytes.
410
+
411
+ ```javascript
412
+ const { pipeline } = require('node:stream/promises');
413
+ const { File, AudioWriter } = require('decibri');
414
+
415
+ await pipeline(
416
+ new File('noisy.wav', { denoise: 'fastenhancer-t' }),
417
+ new AudioWriter('clean.flac', { sampleRate: 16000 }),
418
+ );
419
+ ```
420
+
421
+ `sampleRate` is required (raw audio carries no header to read a rate from). `channels` (default 1) and `dtype` (default `'int16'`) describe the incoming bytes: the stream's total sample count must be a whole number of frames at `channels`, and each container's own channel ceiling applies at the write. The `File.save` options (`format`, `compression`) are accepted as well. `'finish'` fires after the file is on disk, and `writer.report` then carries the `SaveReport`; a failure destroys the stream with the error.
362
422
 
363
423
  ## Device Selection
364
424
 
@@ -409,6 +469,7 @@ decibri 4.0.0 renames the API to a microphone and speaker vocabulary and switche
409
469
  | Platform | Architecture | Audio Backend |
410
470
  | --- | --- | --- |
411
471
  | Windows | x64 | WASAPI |
472
+ | Windows | arm64 | WASAPI |
412
473
  | macOS | arm64 | CoreAudio |
413
474
  | Linux | x64 | ALSA |
414
475
  | Linux | arm64 | ALSA |
@@ -63,14 +63,23 @@ var decibri = (function() {
63
63
  //#endregion
64
64
  //#region npm/decibri/src/browser/worklet-inline.js
65
65
  var require_worklet_inline = /* @__PURE__ */ __commonJSMin(((exports, module) => {
66
- module.exports = { WORKLET_SOURCE: "var e=class extends AudioWorkletProcessor{constructor(e){super();let t=e.processorOptions;this.framesPerBuffer=t.framesPerBuffer,this.format=t.format,this.ratio=t.nativeSampleRate/t.targetSampleRate,this.needsResample=t.nativeSampleRate!==t.targetSampleRate,this.channelMap=t.channelMap??null,this.channels=this.channelMap?this.channelMap.length:t.channels??1,this.channelError=!1,this.position=0,this.samplesPerChunk=this.framesPerBuffer*this.channels,this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}process(e,t,n){let r=e[0];if(!r||r.length===0||!r[0]||r[0].length===0)return!0;if(this.channelError)return!1;let i=r.length,a;if(this.channelMap){for(let e=0;e<this.channelMap.length;e++)if(this.channelMap[e]>=i)return this.refuse(`the channel map names device channel `+this.channelMap[e]+`; the device reports `+i+` input channels`);a=[];for(let e=0;e<this.channelMap.length;e++)a.push(r[this.channelMap[e]])}else if(this.channels===1)if(i===1)a=[r[0]];else{let e=r[0].length,t=new Float32Array(e);for(let n=0;n<e;n++){let e=0;for(let t=0;t<i;t++)e=Math.fround(e+r[t][n]);t[n]=e/i}a=[t]}else if(this.channels===i){a=[];for(let e=0;e<i;e++)a.push(r[e])}else if(this.channels>i)return this.refuse(`the input device does not support `+this.channels+` delivered channels; it reports `+i);else return this.refuse(`a channel map is required to deliver `+this.channels+` of the device's `+i+` input channels`);this.needsResample&&(a=this.resample(a));let o=a[0].length;for(let e=0;e<o;e++){for(let t=0;t<this.channels;t++)this.buffer[this.bufferIndex++]=a[t][e];this.bufferIndex>=this.samplesPerChunk&&this.flush()}return!0}refuse(e){return this.channelError=!0,this.port.postMessage({type:`error`,message:e}),!1}resample(e){let t=e[0].length,n=0,r=this.position;for(;r<t-1;)n++,r+=this.ratio;let i=e.map(()=>new Float32Array(n));r=this.position;for(let t=0;t<n;t++){let n=Math.floor(r),a=r-n;for(let r=0;r<e.length;r++)i[r][t]=e[r][n]*(1-a)+e[r][n+1]*a;r+=this.ratio}return this.position=Math.max(0,r-t),i}flush(){let e;if(this.format===`int16`){let t=new Int16Array(this.samplesPerChunk);for(let e=0;e<this.samplesPerChunk;e++)t[e]=Math.max(-32768,Math.min(32767,Math.round(this.buffer[e]*32768)));e=t.buffer}else e=this.buffer.slice(0,this.samplesPerChunk).buffer;this.port.postMessage(e,[e]),this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}};registerProcessor(`decibri-processor`,e);" };
66
+ module.exports = { WORKLET_SOURCE: "var e=class extends AudioWorkletProcessor{constructor(e){super();let t=e.processorOptions;this.framesPerBuffer=t.framesPerBuffer,this.format=t.format,this.native=t.nativeSampleRate,this.target=t.targetSampleRate,this.needsResample=t.nativeSampleRate!==t.targetSampleRate,this.channelMap=t.channelMap??null,this.channels=this.channelMap?this.channelMap.length:t.channels??1,this.channelError=!1,this.phase=0,this.last=new Float32Array(this.channels),this.planar=Array(this.channels),this.mono=null,this.samplesPerChunk=this.framesPerBuffer*this.channels,this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}process(e,t,n){let r=e[0];if(!r||r.length===0||!r[0]||r[0].length===0)return!0;if(this.channelError)return!1;let i=r.length,a=this.planar;if(this.channelMap){for(let e=0;e<this.channelMap.length;e++)if(this.channelMap[e]>=i)return this.refuse(`the channel map names device channel `+this.channelMap[e]+`; the device reports `+i+` input channels`);for(let e=0;e<this.channelMap.length;e++)a[e]=r[this.channelMap[e]]}else if(this.channels===1)if(i===1)a[0]=r[0];else{let e=r[0].length;(this.mono===null||this.mono.length!==e)&&(this.mono=new Float32Array(e));let t=this.mono;for(let n=0;n<e;n++){let e=0;for(let t=0;t<i;t++)e=Math.fround(e+r[t][n]);t[n]=e/i}a[0]=t}else if(this.channels===i)for(let e=0;e<i;e++)a[e]=r[e];else if(this.channels>i)return this.refuse(`the input device does not support `+this.channels+` delivered channels; it reports `+i);else return this.refuse(`a channel map is required to deliver `+this.channels+` of the device's `+i+` input channels`);return this.needsResample?this.resample(a):this.accumulate(a),!0}accumulate(e){let t=e[0].length,n=this.channels;for(let r=0;r<t;r++){for(let t=0;t<n;t++)this.buffer[this.bufferIndex++]=e[t][r];this.bufferIndex>=this.samplesPerChunk&&this.flush()}}refuse(e){return this.channelError=!0,this.port.postMessage({type:`error`,message:e}),!1}resample(e){let t=e[0].length,n=this.channels,r=this.native,i=this.target,a=this.last,o=this.phase,s=(t-1)*i;for(;o<s;){let t=Math.floor(o/i),s=(o-t*i)/i;if(t<0)for(let t=0;t<n;t++)this.buffer[this.bufferIndex++]=a[t]*(1-s)+e[t][0]*s;else for(let r=0;r<n;r++)this.buffer[this.bufferIndex++]=e[r][t]*(1-s)+e[r][t+1]*s;this.bufferIndex>=this.samplesPerChunk&&this.flush(),o+=r}this.phase=o-t*i;for(let r=0;r<n;r++)a[r]=e[r][t-1]}flush(){let e;if(this.format===`int16`){let t=new Int16Array(this.samplesPerChunk);for(let e=0;e<this.samplesPerChunk;e++)t[e]=Math.max(-32768,Math.min(32767,Math.round(this.buffer[e]*32768)));e=t.buffer}else e=this.buffer.buffer,this.buffer=new Float32Array(this.samplesPerChunk);this.port.postMessage(e,[e]),this.bufferIndex=0}};registerProcessor(`decibri-processor`,e);" };
67
67
  }));
68
68
  //#endregion
69
69
  //#region npm/decibri/src/browser/decibri-browser.js
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.6.0";
73
+ const VERSION = "5.8.0";
74
+ const UNSUPPORTED_OPTIONS = [
75
+ "modelPath",
76
+ "dcRemoval",
77
+ "denoise",
78
+ "highpass",
79
+ "agc",
80
+ "limiter",
81
+ "aec"
82
+ ];
74
83
  /**
75
84
  * Browser microphone capture.
76
85
  *
@@ -100,6 +109,7 @@ var decibri = (function() {
100
109
  this._starting = null;
101
110
  this._stopRequested = false;
102
111
  if (options.vadThreshold !== void 0 || options.vadHoldoff !== void 0) throw new TypeError("vadThreshold and vadHoldoff are no longer supported. Pass them on the vad config object: vad: { model: 'energy', threshold: 0.01, holdoffMs: 300 }.");
112
+ for (const key of UNSUPPORTED_OPTIONS) if (options[key] !== void 0) throw new TypeError(`${key} is not supported in the browser`);
103
113
  const vad = options.vad ?? false;
104
114
  let vadThreshold = .01;
105
115
  let vadHoldoff = 300;
@@ -402,33 +412,41 @@ var decibri = (function() {
402
412
  /**
403
413
  * Linear-interpolation resampler, the inverse of the capture worklet's: it
404
414
  * takes samples at the user's rate and produces samples at the context rate.
405
- * Carries a fractional position across calls so successive writes stay
406
- * continuous. Logic mirrors worklet-processor.js resample(), with from and to
407
- * swapped (from = user rate, to = context rate).
415
+ * Carries its phase and the previous call's last sample across calls, so
416
+ * successive writes stay continuous and the delivered count over any number
417
+ * of writes is exact. Logic mirrors worklet-processor.js resample(), with
418
+ * from and to swapped (from = user rate, to = context rate).
408
419
  */
409
420
  var Resampler = class {
410
421
  constructor(fromRate, toRate) {
411
- this.ratio = fromRate / toRate;
412
- this.position = 0;
422
+ this.from = fromRate;
423
+ this.to = toRate;
424
+ this.identity = fromRate === toRate;
425
+ this.phase = 0;
426
+ this.last = 0;
413
427
  }
414
428
  process(input) {
415
- if (this.ratio === 1) return input;
429
+ if (this.identity) return input;
416
430
  const inputLength = input.length;
431
+ const from = this.from;
432
+ const to = this.to;
433
+ const end = (inputLength - 1) * to;
417
434
  let count = 0;
418
- let pos = this.position;
419
- while (pos < inputLength - 1) {
435
+ let phase = this.phase;
436
+ while (phase < end) {
420
437
  count++;
421
- pos += this.ratio;
438
+ phase += from;
422
439
  }
423
440
  const output = new Float32Array(count);
424
- pos = this.position;
441
+ phase = this.phase;
425
442
  for (let i = 0; i < count; i++) {
426
- const idx = Math.floor(pos);
427
- const frac = pos - idx;
428
- output[i] = input[idx] * (1 - frac) + input[idx + 1] * frac;
429
- pos += this.ratio;
443
+ const idx = Math.floor(phase / to);
444
+ const frac = (phase - idx * to) / to;
445
+ output[i] = (idx < 0 ? this.last : input[idx]) * (1 - frac) + input[idx + 1] * frac;
446
+ phase += from;
430
447
  }
431
- this.position = Math.max(0, pos - inputLength);
448
+ this.phase = phase - inputLength * to;
449
+ if (inputLength > 0) this.last = input[inputLength - 1];
432
450
  return output;
433
451
  }
434
452
  };
package/index.d.ts CHANGED
@@ -320,11 +320,10 @@ export interface DecibriOptions {
320
320
  channels?: number
321
321
  /**
322
322
  * Capture channel map: 0-based device channel indices, one per delivered
323
- * channel (the shape of CoreAudio AUHAL's channel map, an index list, not
324
- * miniaudio's spatial `channelMap`). Absent delivers the documented
325
- * average of every opened channel. Entries are validated against the
326
- * resolved device's own report when the stream starts; no fixed maximum
327
- * exists.
323
+ * channel, selecting device channels by index rather than describing a
324
+ * spatial layout. Absent delivers the documented average of every opened
325
+ * channel. Entries are validated against the resolved device's own report
326
+ * when the stream starts; no fixed maximum exists.
328
327
  */
329
328
  channelMap?: Array<number>
330
329
  framesPerBuffer?: number
@@ -486,6 +485,9 @@ export interface SaveOptions {
486
485
  * What a save did to the samples on their way into the file: finite
487
486
  * samples outside full scale clamped and counted, non-finite samples
488
487
  * replaced (NaN with silence, an infinity with full scale) and counted.
488
+ * When the conditioning chain runs it has already replaced every
489
+ * non-finite sample with silence at its entry, so the non-finite count is
490
+ * what the save's own repair saw, not what the source carried.
489
491
  */
490
492
  export interface SaveReport {
491
493
  clippedSamples: number
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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
80
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
96
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
117
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
133
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
150
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
166
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
185
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
201
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
217
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
237
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
253
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
274
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
290
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
308
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
324
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
342
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
358
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
376
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
392
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
410
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
426
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
443
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
459
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
479
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
495
+ if (bindingPackageVersion !== '5.8.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.8.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.6.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.6.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
511
+ if (bindingPackageVersion !== '5.8.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.8.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
513
513
  }
514
514
  return binding
515
515
  } catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "decibri",
3
- "version": "5.6.0",
4
- "description": "Cross-platform audio capture, playback, and processing for Node.js and browsers",
3
+ "version": "5.8.0",
4
+ "description": "Cross-platform audio capture, conditioning, and playback for Node.js, with browser capture and voice activity detection on live and recorded audio",
5
5
  "main": "src/decibri.js",
6
6
  "types": "src/decibri.d.ts",
7
7
  "browser": "./src/browser/index.js",
@@ -56,6 +56,7 @@
56
56
  "binaryName": "decibri",
57
57
  "targets": [
58
58
  "x86_64-pc-windows-msvc",
59
+ "aarch64-pc-windows-msvc",
59
60
  "aarch64-apple-darwin",
60
61
  "x86_64-unknown-linux-gnu",
61
62
  "aarch64-unknown-linux-gnu"
@@ -72,10 +73,11 @@
72
73
  "MIGRATION.md"
73
74
  ],
74
75
  "optionalDependencies": {
75
- "@decibri/decibri-win32-x64-msvc": "5.6.0",
76
- "@decibri/decibri-darwin-arm64": "5.6.0",
77
- "@decibri/decibri-linux-x64-gnu": "5.6.0",
78
- "@decibri/decibri-linux-arm64-gnu": "5.6.0"
76
+ "@decibri/decibri-win32-x64-msvc": "5.8.0",
77
+ "@decibri/decibri-win32-arm64-msvc": "5.8.0",
78
+ "@decibri/decibri-darwin-arm64": "5.8.0",
79
+ "@decibri/decibri-linux-x64-gnu": "5.8.0",
80
+ "@decibri/decibri-linux-arm64-gnu": "5.8.0"
79
81
  },
80
82
  "devDependencies": {
81
83
  "@napi-rs/cli": "^3.7.0"
@@ -6,7 +6,13 @@ 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.6.0';
9
+ const VERSION = '5.8.0';
10
+
11
+ // The node entry's options this entry cannot serve: the conditioning chain
12
+ // and the detector model file. Each is refused on presence, whatever its
13
+ // value; an explicit undefined is absence, as on the node entry. Node option
14
+ // table order, so the first present key is the one the error names.
15
+ const UNSUPPORTED_OPTIONS = ['modelPath', 'dcRemoval', 'denoise', 'highpass', 'agc', 'limiter', 'aec'];
10
16
 
11
17
  /**
12
18
  * Browser microphone capture.
@@ -50,6 +56,11 @@ class Microphone extends Emitter {
50
56
  "vadThreshold and vadHoldoff are no longer supported. Pass them on the vad config object: vad: { model: 'energy', threshold: 0.01, holdoffMs: 300 }."
51
57
  );
52
58
  }
59
+ for (const key of UNSUPPORTED_OPTIONS) {
60
+ if (options[key] !== undefined) {
61
+ throw new TypeError(`${key} is not supported in the browser`);
62
+ }
63
+ }
53
64
  const vad = options.vad ?? false;
54
65
  let vadThreshold = 0.01;
55
66
  let vadHoldoff = 300;
@@ -10,39 +10,60 @@ const BUFFER_SECONDS = 2;
10
10
  /**
11
11
  * Linear-interpolation resampler, the inverse of the capture worklet's: it
12
12
  * takes samples at the user's rate and produces samples at the context rate.
13
- * Carries a fractional position across calls so successive writes stay
14
- * continuous. Logic mirrors worklet-processor.js resample(), with from and to
15
- * swapped (from = user rate, to = context rate).
13
+ * Carries its phase and the previous call's last sample across calls, so
14
+ * successive writes stay continuous and the delivered count over any number
15
+ * of writes is exact. Logic mirrors worklet-processor.js resample(), with
16
+ * from and to swapped (from = user rate, to = context rate).
16
17
  */
17
18
  class Resampler {
18
19
  constructor(fromRate, toRate) {
19
- this.ratio = fromRate / toRate;
20
- this.position = 0;
20
+ this.from = fromRate;
21
+ this.to = toRate;
22
+ this.identity = fromRate === toRate;
23
+ // The position of the next output sample in input samples relative to
24
+ // the current call's first sample, held as an integer numerator over the
25
+ // context rate so it is exact across any number of calls. It may sit
26
+ // below zero, down to one input sample before the call, for a sample
27
+ // whose time falls between the previous call's last sample, kept in
28
+ // `last`, and this call's first. The first call starts at zero, on its
29
+ // own first sample.
30
+ this.phase = 0;
31
+ this.last = 0;
21
32
  }
22
33
 
23
34
  process(input) {
24
- if (this.ratio === 1) return input;
35
+ if (this.identity) return input;
25
36
 
26
37
  const inputLength = input.length;
38
+ const from = this.from;
39
+ const to = this.to;
40
+ // A sample needs the input sample after its floor, so the call serves
41
+ // every position below inputLength - 1.
42
+ const end = (inputLength - 1) * to;
27
43
 
28
44
  let count = 0;
29
- let pos = this.position;
30
- while (pos < inputLength - 1) {
45
+ let phase = this.phase;
46
+ while (phase < end) {
31
47
  count++;
32
- pos += this.ratio;
48
+ phase += from;
33
49
  }
34
50
 
35
51
  const output = new Float32Array(count);
36
- pos = this.position;
52
+ phase = this.phase;
37
53
 
38
54
  for (let i = 0; i < count; i++) {
39
- const idx = Math.floor(pos);
40
- const frac = pos - idx;
41
- output[i] = input[idx] * (1 - frac) + input[idx + 1] * frac;
42
- pos += this.ratio;
55
+ const idx = Math.floor(phase / to);
56
+ const frac = (phase - idx * to) / to;
57
+ const before = idx < 0 ? this.last : input[idx];
58
+ output[i] = before * (1 - frac) + input[idx + 1] * frac;
59
+ phase += from;
43
60
  }
44
61
 
45
- this.position = Math.max(0, pos - inputLength);
62
+ // Carry the phase relative to the next call's first sample, and keep
63
+ // this call's last sample for the output that may fall before that call
64
+ // begins. An empty call leaves both as they are.
65
+ this.phase = phase - inputLength * to;
66
+ if (inputLength > 0) this.last = input[inputLength - 1];
46
67
 
47
68
  return output;
48
69
  }
@@ -9,6 +9,6 @@
9
9
  *
10
10
  * Logic identical to worklet-processor.js.
11
11
  */
12
- const WORKLET_SOURCE = "var e=class extends AudioWorkletProcessor{constructor(e){super();let t=e.processorOptions;this.framesPerBuffer=t.framesPerBuffer,this.format=t.format,this.ratio=t.nativeSampleRate/t.targetSampleRate,this.needsResample=t.nativeSampleRate!==t.targetSampleRate,this.channelMap=t.channelMap??null,this.channels=this.channelMap?this.channelMap.length:t.channels??1,this.channelError=!1,this.position=0,this.samplesPerChunk=this.framesPerBuffer*this.channels,this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}process(e,t,n){let r=e[0];if(!r||r.length===0||!r[0]||r[0].length===0)return!0;if(this.channelError)return!1;let i=r.length,a;if(this.channelMap){for(let e=0;e<this.channelMap.length;e++)if(this.channelMap[e]>=i)return this.refuse(`the channel map names device channel `+this.channelMap[e]+`; the device reports `+i+` input channels`);a=[];for(let e=0;e<this.channelMap.length;e++)a.push(r[this.channelMap[e]])}else if(this.channels===1)if(i===1)a=[r[0]];else{let e=r[0].length,t=new Float32Array(e);for(let n=0;n<e;n++){let e=0;for(let t=0;t<i;t++)e=Math.fround(e+r[t][n]);t[n]=e/i}a=[t]}else if(this.channels===i){a=[];for(let e=0;e<i;e++)a.push(r[e])}else if(this.channels>i)return this.refuse(`the input device does not support `+this.channels+` delivered channels; it reports `+i);else return this.refuse(`a channel map is required to deliver `+this.channels+` of the device's `+i+` input channels`);this.needsResample&&(a=this.resample(a));let o=a[0].length;for(let e=0;e<o;e++){for(let t=0;t<this.channels;t++)this.buffer[this.bufferIndex++]=a[t][e];this.bufferIndex>=this.samplesPerChunk&&this.flush()}return!0}refuse(e){return this.channelError=!0,this.port.postMessage({type:`error`,message:e}),!1}resample(e){let t=e[0].length,n=0,r=this.position;for(;r<t-1;)n++,r+=this.ratio;let i=e.map(()=>new Float32Array(n));r=this.position;for(let t=0;t<n;t++){let n=Math.floor(r),a=r-n;for(let r=0;r<e.length;r++)i[r][t]=e[r][n]*(1-a)+e[r][n+1]*a;r+=this.ratio}return this.position=Math.max(0,r-t),i}flush(){let e;if(this.format===`int16`){let t=new Int16Array(this.samplesPerChunk);for(let e=0;e<this.samplesPerChunk;e++)t[e]=Math.max(-32768,Math.min(32767,Math.round(this.buffer[e]*32768)));e=t.buffer}else e=this.buffer.slice(0,this.samplesPerChunk).buffer;this.port.postMessage(e,[e]),this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}};registerProcessor(`decibri-processor`,e);";
12
+ const WORKLET_SOURCE = "var e=class extends AudioWorkletProcessor{constructor(e){super();let t=e.processorOptions;this.framesPerBuffer=t.framesPerBuffer,this.format=t.format,this.native=t.nativeSampleRate,this.target=t.targetSampleRate,this.needsResample=t.nativeSampleRate!==t.targetSampleRate,this.channelMap=t.channelMap??null,this.channels=this.channelMap?this.channelMap.length:t.channels??1,this.channelError=!1,this.phase=0,this.last=new Float32Array(this.channels),this.planar=Array(this.channels),this.mono=null,this.samplesPerChunk=this.framesPerBuffer*this.channels,this.buffer=new Float32Array(this.samplesPerChunk),this.bufferIndex=0}process(e,t,n){let r=e[0];if(!r||r.length===0||!r[0]||r[0].length===0)return!0;if(this.channelError)return!1;let i=r.length,a=this.planar;if(this.channelMap){for(let e=0;e<this.channelMap.length;e++)if(this.channelMap[e]>=i)return this.refuse(`the channel map names device channel `+this.channelMap[e]+`; the device reports `+i+` input channels`);for(let e=0;e<this.channelMap.length;e++)a[e]=r[this.channelMap[e]]}else if(this.channels===1)if(i===1)a[0]=r[0];else{let e=r[0].length;(this.mono===null||this.mono.length!==e)&&(this.mono=new Float32Array(e));let t=this.mono;for(let n=0;n<e;n++){let e=0;for(let t=0;t<i;t++)e=Math.fround(e+r[t][n]);t[n]=e/i}a[0]=t}else if(this.channels===i)for(let e=0;e<i;e++)a[e]=r[e];else if(this.channels>i)return this.refuse(`the input device does not support `+this.channels+` delivered channels; it reports `+i);else return this.refuse(`a channel map is required to deliver `+this.channels+` of the device's `+i+` input channels`);return this.needsResample?this.resample(a):this.accumulate(a),!0}accumulate(e){let t=e[0].length,n=this.channels;for(let r=0;r<t;r++){for(let t=0;t<n;t++)this.buffer[this.bufferIndex++]=e[t][r];this.bufferIndex>=this.samplesPerChunk&&this.flush()}}refuse(e){return this.channelError=!0,this.port.postMessage({type:`error`,message:e}),!1}resample(e){let t=e[0].length,n=this.channels,r=this.native,i=this.target,a=this.last,o=this.phase,s=(t-1)*i;for(;o<s;){let t=Math.floor(o/i),s=(o-t*i)/i;if(t<0)for(let t=0;t<n;t++)this.buffer[this.bufferIndex++]=a[t]*(1-s)+e[t][0]*s;else for(let r=0;r<n;r++)this.buffer[this.bufferIndex++]=e[r][t]*(1-s)+e[r][t+1]*s;this.bufferIndex>=this.samplesPerChunk&&this.flush(),o+=r}this.phase=o-t*i;for(let r=0;r<n;r++)a[r]=e[r][t-1]}flush(){let e;if(this.format===`int16`){let t=new Int16Array(this.samplesPerChunk);for(let e=0;e<this.samplesPerChunk;e++)t[e]=Math.max(-32768,Math.min(32767,Math.round(this.buffer[e]*32768)));e=t.buffer}else e=this.buffer.buffer,this.buffer=new Float32Array(this.samplesPerChunk);this.port.postMessage(e,[e]),this.bufferIndex=0}};registerProcessor(`decibri-processor`,e);";
13
13
 
14
14
  module.exports = { WORKLET_SOURCE };
@@ -23,7 +23,8 @@ class DecibriProcessor extends AudioWorkletProcessor {
23
23
  const opts = options.processorOptions;
24
24
  this.framesPerBuffer = opts.framesPerBuffer;
25
25
  this.format = opts.format;
26
- this.ratio = opts.nativeSampleRate / opts.targetSampleRate;
26
+ this.native = opts.nativeSampleRate;
27
+ this.target = opts.targetSampleRate;
27
28
  this.needsResample = opts.nativeSampleRate !== opts.targetSampleRate;
28
29
  // Optional list of 0-based channel indices into the granted track's
29
30
  // channels; delivered channel j carries granted channel channelMap[j].
@@ -38,7 +39,23 @@ class DecibriProcessor extends AudioWorkletProcessor {
38
39
  // channel, so its length is the count when one is present.
39
40
  this.channels = this.channelMap ? this.channelMap.length : (opts.channels ?? 1);
40
41
  this.channelError = false;
41
- this.position = 0;
42
+ // The resampler's phase: the position of the next delivered frame in
43
+ // input samples relative to the current block's first sample, held as an
44
+ // integer numerator over the target rate so it is exact across any
45
+ // number of blocks. It may sit below zero, down to one input sample
46
+ // before the block, for a frame whose time falls between the previous
47
+ // block's last sample and this block's first; that last sample is kept
48
+ // per delivered channel in `last`. The first block starts at zero, on
49
+ // its own first sample.
50
+ this.phase = 0;
51
+ this.last = new Float32Array(this.channels);
52
+ // The delivered channels of the current block, planar, gathered into
53
+ // this array in place so a block allocates nothing.
54
+ this.planar = new Array(this.channels);
55
+ // The average of every granted channel, for one delivered channel
56
+ // derived from more than one granted: sized to the block on first use
57
+ // and reused while the block length holds.
58
+ this.mono = null;
42
59
  // The accumulation buffer holds framesPerBuffer frames of the delivered
43
60
  // count, interleaved frame by frame; bufferIndex counts samples. Chunks
44
61
  // are flushed at whole frames only.
@@ -54,9 +71,9 @@ class DecibriProcessor extends AudioWorkletProcessor {
54
71
 
55
72
  const granted = input.length;
56
73
  // The delivered channels, planar: one Float32Array per delivered
57
- // channel, equal lengths. Gathered here, resampled per channel in
58
- // lockstep, interleaved at accumulation.
59
- let planar;
74
+ // channel, equal lengths. Gathered here into the preallocated array,
75
+ // resampled per channel in lockstep, interleaved at accumulation.
76
+ const planar = this.planar;
60
77
 
61
78
  if (this.channelMap) {
62
79
  // The granted track's channel count is the only ceiling. A map entry
@@ -71,20 +88,22 @@ class DecibriProcessor extends AudioWorkletProcessor {
71
88
  // Delivered channel j is granted channel channelMap[j], in map order.
72
89
  // Entries may repeat and may appear in any order, so a map both
73
90
  // selects and permutes.
74
- planar = [];
75
91
  for (let j = 0; j < this.channelMap.length; j++) {
76
- planar.push(input[this.channelMap[j]]);
92
+ planar[j] = input[this.channelMap[j]];
77
93
  }
78
94
  } else if (this.channels === 1) {
79
95
  if (granted === 1) {
80
- planar = [input[0]];
96
+ planar[0] = input[0];
81
97
  } else {
82
98
  // The documented average of every granted channel: each frame's
83
99
  // arithmetic mean, accumulated at single precision (Math.fround per
84
100
  // step) and stored as f32, matching the engine's average sample for
85
101
  // sample.
86
102
  const frames = input[0].length;
87
- const mono = new Float32Array(frames);
103
+ if (this.mono === null || this.mono.length !== frames) {
104
+ this.mono = new Float32Array(frames);
105
+ }
106
+ const mono = this.mono;
88
107
  for (let i = 0; i < frames; i++) {
89
108
  let sum = 0;
90
109
  for (let c = 0; c < granted; c++) {
@@ -92,12 +111,11 @@ class DecibriProcessor extends AudioWorkletProcessor {
92
111
  }
93
112
  mono[i] = sum / granted;
94
113
  }
95
- planar = [mono];
114
+ planar[0] = mono;
96
115
  }
97
116
  } else if (this.channels === granted) {
98
117
  // Every granted channel, in granted order: the unmapped identity.
99
- planar = [];
100
- for (let c = 0; c < granted; c++) planar.push(input[c]);
118
+ for (let c = 0; c < granted; c++) planar[c] = input[c];
101
119
  } else if (this.channels > granted) {
102
120
  return this.refuse('the input device does not support ' + this.channels +
103
121
  ' delivered channels; it reports ' + granted);
@@ -109,23 +127,30 @@ class DecibriProcessor extends AudioWorkletProcessor {
109
127
  }
110
128
 
111
129
  if (this.needsResample) {
112
- planar = this.resample(planar);
130
+ this.resample(planar);
131
+ } else {
132
+ this.accumulate(planar);
113
133
  }
114
134
 
115
- // Interleave the planar channels into the accumulation buffer frame by
116
- // frame, flushing at whole chunks, so every posted chunk is a whole
117
- // number of frames.
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Interleave the block's frames into the accumulation buffer as they are,
140
+ * flushing at whole chunks, so every posted chunk is a whole number of
141
+ * frames.
142
+ */
143
+ accumulate(planar) {
118
144
  const frames = planar[0].length;
145
+ const channels = this.channels;
119
146
  for (let i = 0; i < frames; i++) {
120
- for (let c = 0; c < this.channels; c++) {
147
+ for (let c = 0; c < channels; c++) {
121
148
  this.buffer[this.bufferIndex++] = planar[c][i];
122
149
  }
123
150
  if (this.bufferIndex >= this.samplesPerChunk) {
124
151
  this.flush();
125
152
  }
126
153
  }
127
-
128
- return true;
129
154
  }
130
155
 
131
156
  /**
@@ -139,54 +164,75 @@ class DecibriProcessor extends AudioWorkletProcessor {
139
164
  return false;
140
165
  }
141
166
 
167
+ /**
168
+ * Resample the block by linear interpolation at one phase shared by every
169
+ * channel, so a delivered frame stays a frame, interleaving each frame
170
+ * straight into the accumulation buffer and flushing at whole chunks.
171
+ * Frame k of the stream sits at k * native / target input samples. The
172
+ * phase carries that position across blocks as an exact integer numerator
173
+ * over the target rate, without clamping, and the previous block's last
174
+ * sample per channel is kept, so a frame whose time falls between two
175
+ * blocks is interpolated across the boundary. Every input sample's time is
176
+ * represented once, and the delivered count over any length of capture is
177
+ * exact.
178
+ */
142
179
  resample(planar) {
143
180
  const inputLength = planar[0].length;
144
-
145
- // Calculate how many output frames we can produce. One position shared
146
- // by every channel: the channels advance in lockstep, so a delivered
147
- // frame stays a frame.
148
- let count = 0;
149
- let pos = this.position;
150
- while (pos < inputLength - 1) {
151
- count++;
152
- pos += this.ratio;
153
- }
154
-
155
- const output = planar.map(() => new Float32Array(count));
156
- pos = this.position;
157
-
158
- for (let i = 0; i < count; i++) {
159
- const idx = Math.floor(pos);
160
- const frac = pos - idx;
161
- for (let c = 0; c < planar.length; c++) {
162
- output[c][i] = planar[c][idx] * (1 - frac) + planar[c][idx + 1] * frac;
181
+ const channels = this.channels;
182
+ const native = this.native;
183
+ const target = this.target;
184
+ const last = this.last;
185
+ let phase = this.phase;
186
+
187
+ // A frame needs the input sample after its floor, so the block serves
188
+ // every position below inputLength - 1.
189
+ const end = (inputLength - 1) * target;
190
+ while (phase < end) {
191
+ const idx = Math.floor(phase / target);
192
+ const frac = (phase - idx * target) / target;
193
+ if (idx < 0) {
194
+ // Between the stored sample and this block's first.
195
+ for (let c = 0; c < channels; c++) {
196
+ this.buffer[this.bufferIndex++] = last[c] * (1 - frac) + planar[c][0] * frac;
197
+ }
198
+ } else {
199
+ for (let c = 0; c < channels; c++) {
200
+ this.buffer[this.bufferIndex++] = planar[c][idx] * (1 - frac) + planar[c][idx + 1] * frac;
201
+ }
163
202
  }
164
- pos += this.ratio;
203
+ if (this.bufferIndex >= this.samplesPerChunk) {
204
+ this.flush();
205
+ }
206
+ phase += native;
165
207
  }
166
208
 
167
- // Carry fractional remainder relative to consumed input.
168
- this.position = Math.max(0, pos - inputLength);
169
-
170
- return output;
209
+ // Carry the phase relative to the next block's first sample; it sits in
210
+ // [-target, native - target). Keep the block's last sample for the frame
211
+ // that may fall before that block begins.
212
+ this.phase = phase - inputLength * target;
213
+ for (let c = 0; c < channels; c++) last[c] = planar[c][inputLength - 1];
171
214
  }
172
215
 
173
216
  flush() {
174
217
  let transferBuffer;
175
218
 
176
219
  if (this.format === 'int16') {
220
+ // The posted buffer is transferred, so each chunk converts into a
221
+ // fresh Int16Array; the accumulation buffer stays and is refilled from
222
+ // its start.
177
223
  const int16 = new Int16Array(this.samplesPerChunk);
178
224
  for (let i = 0; i < this.samplesPerChunk; i++) {
179
225
  int16[i] = Math.max(-32768, Math.min(32767, Math.round(this.buffer[i] * 32768)));
180
226
  }
181
227
  transferBuffer = int16.buffer;
182
228
  } else {
183
- transferBuffer = this.buffer.slice(0, this.samplesPerChunk).buffer;
229
+ // The accumulation buffer itself is transferred, so a fresh one
230
+ // replaces it.
231
+ transferBuffer = this.buffer.buffer;
232
+ this.buffer = new Float32Array(this.samplesPerChunk);
184
233
  }
185
234
 
186
235
  this.port.postMessage(transferBuffer, [transferBuffer]);
187
-
188
- // Reset accumulation buffer
189
- this.buffer = new Float32Array(this.samplesPerChunk);
190
236
  this.bufferIndex = 0;
191
237
  }
192
238
  }
@@ -242,7 +242,9 @@ class Speaker extends Writable {
242
242
  }
243
243
 
244
244
  /**
245
- * Whether audio is currently being output.
245
+ * Whether the output stream is running: `true` once a non-empty write opens
246
+ * it, until `stop()`, `end()` once the queued audio has played, or a device
247
+ * failure. `drainAsync()` leaves it `true`.
246
248
  * @returns {boolean}
247
249
  */
248
250
  get isPlaying() {
package/src/decibri.d.ts CHANGED
@@ -296,14 +296,12 @@ export interface MicrophoneOptions extends ReadableOptions {
296
296
  * and may name more delivered channels than the device has. Absent derives
297
297
  * the delivered channels from `channels` as documented there.
298
298
  *
299
- * The same shape as CoreAudio AUHAL's channel map
300
- * (`kAudioOutputUnitProperty_ChannelMap`: an array of device channel
301
- * indices, one entry per client channel). NOT miniaudio's `channelMap`,
302
- * which names a spatial layout. Entries are validated against the resolved
303
- * device's own report when the stream starts: an entry the device does not
304
- * have throws a `DecibriError` with code `'CHANNEL_MAP_OUT_OF_RANGE'` naming
305
- * the entry and the count the device reports. The device's report is the
306
- * only ceiling; no fixed maximum exists.
299
+ * The map selects device channels by index, one entry per delivered
300
+ * channel; it does not describe a spatial layout. Entries are validated
301
+ * against the resolved device's own report when the stream starts: an entry
302
+ * the device does not have throws a `DecibriError` with code
303
+ * `'CHANNEL_MAP_OUT_OF_RANGE'` naming the entry and the count the device
304
+ * reports. The device's report is the only ceiling; no fixed maximum exists.
307
305
  * @default undefined (the derivation `channels` documents)
308
306
  */
309
307
  channelMap?: number[];
@@ -772,7 +770,11 @@ export interface SaveReport {
772
770
  clippedSamples: number;
773
771
  /**
774
772
  * Non-finite samples replaced before writing: NaN with silence, an
775
- * infinity with full scale. The same replacement on every format.
773
+ * infinity with full scale. The same replacement on every format. When
774
+ * the conditioning chain runs it has already replaced every non-finite
775
+ * sample with silence at its entry, so this count covers the direct path
776
+ * only (a mono source already at `sampleRate` with no conditioning
777
+ * enabled).
776
778
  */
777
779
  nonFiniteSamples: number;
778
780
  }
@@ -828,9 +830,10 @@ export declare class File extends Readable {
828
830
 
829
831
  /**
830
832
  * Wrap in-memory samples as an offline source. `samples` must be a
831
- * `Float32Array` of mono samples in [-1.0, 1.0]; a raw `Buffer` of PCM
832
- * bytes is rejected as ambiguous. `inputRate` is required (raw samples
833
- * carry no header). Synchronous: no I/O is involved.
833
+ * `Float32Array` of samples in [-1.0, 1.0], frame-interleaved at
834
+ * `inputChannels` (1, mono, by default); a raw `Buffer` of PCM bytes is
835
+ * rejected as ambiguous. `inputRate` is required (raw samples carry no
836
+ * header). Synchronous: no I/O is involved.
834
837
  */
835
838
  static buffer(samples: Float32Array, options: FileBufferOptions): File;
836
839
 
@@ -876,15 +879,18 @@ export declare class File extends Readable {
876
879
  /**
877
880
  * Write the conditioned recording to disk, off the event loop. Runs the
878
881
  * recording once through the same conditioning pass iteration delivers,
879
- * whole, and writes it as 16-bit PCM mono at `sampleRate`. The container
880
- * comes from the path's extension (`.wav`, `.aiff`, `.aif`, `.aifc` or
881
- * `.flac`), or from `options.format`: decibri reads a file by its content
882
- * and writes one by its name. Consumes the source (a `File` is a single
883
- * pass).
882
+ * whole, and writes it as 16-bit PCM at `sampleRate`, frame-interleaved at
883
+ * the delivered channel count. The container comes from the path's
884
+ * extension (`.wav`, `.aiff`, `.aif`, `.aifc` or `.flac`), or from
885
+ * `options.format`: decibri reads a file by its content and writes one by
886
+ * its name. Consumes the source (a `File` is a single pass).
884
887
  *
885
888
  * Resolves to a `SaveReport`: how many samples were clamped to full scale
886
889
  * and how many non-finite samples were replaced (NaN as silence, an
887
- * infinity as full scale).
890
+ * infinity as full scale). When the conditioning chain runs it has already
891
+ * replaced every non-finite sample with silence at its entry, so the file
892
+ * carries silence where one was and the count covers the direct path only
893
+ * (a mono source already at `sampleRate` with no conditioning enabled).
888
894
  *
889
895
  * Requires a File that is not already being streamed: once the stream has
890
896
  * been engaged this rejects with a `DecibriError` carrying the code
@@ -928,9 +934,10 @@ export interface AudioWriterOptions extends SaveOptions, WritableOptions {
928
934
  * number of frames at this count. Bounded below at `1` (the default);
929
935
  * above it, each container's own ceiling applies (a FLAC frame carries at
930
936
  * most 8 channels; a WAV `fmt ` chunk's `nBlockAlign` is a 16-bit field,
931
- * so 16-bit samples allow at most 32767), reported when the stream
932
- * finishes as the container layer's own refusal. decibri enforces no
933
- * ceiling of its own.
937
+ * so 16-bit samples allow at most 32767; an AIFF `COMM` chunk's
938
+ * `numChannels` is a signed 16-bit field, so at most 32767), reported
939
+ * when the stream finishes as the container layer's own refusal. decibri
940
+ * enforces no ceiling of its own.
934
941
  * @default 1
935
942
  */
936
943
  channels?: number;
@@ -1087,7 +1094,11 @@ export declare class Speaker extends Writable {
1087
1094
  /** Immediate stop. Discards remaining buffered audio. */
1088
1095
  stop(): void;
1089
1096
 
1090
- /** Whether audio is currently being output. */
1097
+ /**
1098
+ * Whether the output stream is running: `true` once a non-empty write opens
1099
+ * it, until `stop()`, `end()` once the queued audio has played, or a device
1100
+ * failure. `drainAsync()` leaves it `true`.
1101
+ */
1091
1102
  readonly isPlaying: boolean;
1092
1103
 
1093
1104
  /**
package/src/decibri.js CHANGED
@@ -25,18 +25,19 @@ const PACKAGE_VERSION = require('../package.json').version;
25
25
  * file: bundled ORT dylib filename }
26
26
  *
27
27
  * Dylib filenames are unversioned across all platforms for consistency. The
28
- * release workflow (.github/workflows/release.yml) copies Microsoft's
29
- * versioned upstream tarball file (e.g. libonnxruntime.1.24.4.dylib) into the
28
+ * publish workflow (.github/workflows/publish-npm.yml) copies Microsoft's
29
+ * versioned upstream tarball file (e.g. libonnxruntime.1.28.1.dylib) into the
30
30
  * platform package with the unversioned name listed here.
31
31
  *
32
32
  * If you add a new platform, update this table AND the matching platform job
33
- * in release.yml.
33
+ * in publish-npm.yml.
34
34
  */
35
35
  const PLATFORM_DYLIB = {
36
36
  'darwin-arm64': { pkg: '@decibri/decibri-darwin-arm64', file: 'libonnxruntime.dylib' },
37
37
  'linux-x64': { pkg: '@decibri/decibri-linux-x64-gnu', file: 'libonnxruntime.so' },
38
38
  'linux-arm64': { pkg: '@decibri/decibri-linux-arm64-gnu', file: 'libonnxruntime.so' },
39
39
  'win32-x64': { pkg: '@decibri/decibri-win32-x64-msvc', file: 'onnxruntime.dll' },
40
+ 'win32-arm64': { pkg: '@decibri/decibri-win32-arm64-msvc', file: 'onnxruntime.dll' },
40
41
  };
41
42
 
42
43
  /**
@@ -899,7 +900,7 @@ class File extends Readable {
899
900
  * Validate the constructor options and resolve them into the native options
900
901
  * object plus the wrapper-side state. The checks and messages mirror
901
902
  * `Microphone._prepareOptions` exactly for every shared option; the
902
- * live-capture-only options (device, channels, framesPerBuffer) do not
903
+ * live-capture-only options (device, framesPerBuffer, aec) do not
903
904
  * apply to an offline source.
904
905
  * @internal
905
906
  * @param {import('./decibri').FileOptions} options
@@ -1422,8 +1423,9 @@ class File extends Readable {
1422
1423
  /**
1423
1424
  * Write the conditioned recording to disk, off the event loop. Runs the
1424
1425
  * recording once through the same conditioning pass iteration delivers,
1425
- * whole, and writes it as 16-bit PCM mono at `sampleRate`. Consumes the
1426
- * source: a save is a single pass, separate from iteration and analysis.
1426
+ * whole, and writes it as 16-bit PCM at `sampleRate`, frame-interleaved at
1427
+ * the delivered channel count. Consumes the source: a save is a single
1428
+ * pass, separate from iteration and analysis.
1427
1429
  *
1428
1430
  * The container comes from the path's extension (`.wav`, `.aiff`, `.aif`,
1429
1431
  * `.aifc` or `.flac`), or from `options.format`: decibri reads a file by
@@ -1435,7 +1437,11 @@ class File extends Readable {
1435
1437
  * outside full scale clamped to `[-1.0, 1.0]` (AGC or AEC without a
1436
1438
  * limiter can overshoot, and 16-bit PCM cannot hold it), and
1437
1439
  * `nonFiniteSamples` counts NaN samples written as silence and infinite
1438
- * samples written as full scale.
1440
+ * samples written as full scale. When the conditioning chain runs it has
1441
+ * already replaced every non-finite sample with silence at its entry, so
1442
+ * the file carries silence where one was and `nonFiniteSamples` covers
1443
+ * the direct path only (a mono source already at `sampleRate` with no
1444
+ * conditioning enabled).
1439
1445
  *
1440
1446
  * Requires a `File` that is not already being streamed: once the stream
1441
1447
  * has been engaged this rejects with a `DecibriError` carrying the code