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 +33 -0
- package/README.md +68 -7
- package/examples/decibri.browser.js +35 -17
- package/index.d.ts +7 -5
- package/index.js +52 -52
- package/package.json +8 -6
- package/src/browser/decibri-browser.js +12 -1
- package/src/browser/decibri-output-browser.js +36 -15
- package/src/browser/worklet-inline.js +1 -1
- package/src/browser/worklet-processor.js +93 -47
- package/src/decibri-output.js +3 -1
- package/src/decibri.d.ts +33 -22
- package/src/decibri.js +13 -7
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
406
|
-
* continuous
|
|
407
|
-
*
|
|
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.
|
|
412
|
-
this.
|
|
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.
|
|
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
|
|
419
|
-
while (
|
|
435
|
+
let phase = this.phase;
|
|
436
|
+
while (phase < end) {
|
|
420
437
|
count++;
|
|
421
|
-
|
|
438
|
+
phase += from;
|
|
422
439
|
}
|
|
423
440
|
const output = new Float32Array(count);
|
|
424
|
-
|
|
441
|
+
phase = this.phase;
|
|
425
442
|
for (let i = 0; i < count; i++) {
|
|
426
|
-
const idx = Math.floor(
|
|
427
|
-
const frac =
|
|
428
|
-
output[i] = input[idx] * (1 - frac) + input[idx + 1] * frac;
|
|
429
|
-
|
|
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.
|
|
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
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
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.
|
|
81
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
97
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
118
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
134
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
151
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
167
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
186
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
202
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
218
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
238
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
254
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
275
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
291
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
309
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
325
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
343
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
359
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
377
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
393
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
411
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
427
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
444
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
460
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
480
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
496
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
512
|
-
throw new Error(`Native binding package version mismatch, expected 5.
|
|
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.
|
|
4
|
-
"description": "Cross-platform audio capture,
|
|
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.
|
|
76
|
-
"@decibri/decibri-
|
|
77
|
-
"@decibri/decibri-
|
|
78
|
-
"@decibri/decibri-linux-
|
|
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.
|
|
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
|
|
14
|
-
* continuous
|
|
15
|
-
*
|
|
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.
|
|
20
|
-
this.
|
|
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.
|
|
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
|
|
30
|
-
while (
|
|
45
|
+
let phase = this.phase;
|
|
46
|
+
while (phase < end) {
|
|
31
47
|
count++;
|
|
32
|
-
|
|
48
|
+
phase += from;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
const output = new Float32Array(count);
|
|
36
|
-
|
|
52
|
+
phase = this.phase;
|
|
37
53
|
|
|
38
54
|
for (let i = 0; i < count; i++) {
|
|
39
|
-
const idx = Math.floor(
|
|
40
|
-
const frac =
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
58
|
-
// lockstep, interleaved at accumulation.
|
|
59
|
-
|
|
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
|
|
92
|
+
planar[j] = input[this.channelMap[j]];
|
|
77
93
|
}
|
|
78
94
|
} else if (this.channels === 1) {
|
|
79
95
|
if (granted === 1) {
|
|
80
|
-
planar =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
130
|
+
this.resample(planar);
|
|
131
|
+
} else {
|
|
132
|
+
this.accumulate(planar);
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 <
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
203
|
+
if (this.bufferIndex >= this.samplesPerChunk) {
|
|
204
|
+
this.flush();
|
|
205
|
+
}
|
|
206
|
+
phase += native;
|
|
165
207
|
}
|
|
166
208
|
|
|
167
|
-
// Carry
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/decibri-output.js
CHANGED
|
@@ -242,7 +242,9 @@ class Speaker extends Writable {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Whether
|
|
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
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
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
|
|
832
|
-
*
|
|
833
|
-
*
|
|
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
|
|
880
|
-
* comes from the path's
|
|
881
|
-
* `.flac`), or from
|
|
882
|
-
*
|
|
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
|
|
932
|
-
*
|
|
933
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
29
|
-
* versioned upstream tarball file (e.g. libonnxruntime.1.
|
|
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
|
|
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,
|
|
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
|
|
1426
|
-
* source: a save is a single
|
|
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
|