Complete reference for the decibri Node.js API. For installation and basic usage, see Getting started.
libasound.so.2: install libasound2 on Debian and Ubuntu (libasound2t64 from Debian 13 and Ubuntu 24.04 onwards), or alsa-lib on Fedora and Arch. On Windows, it is the Microsoft Visual C++ Redistributable matching your Node.js architecture (x64 or ARM64). On macOS the addon needs nothing beyond the operating system.
new Microphone(options?)Creates a Node.js Readable stream that captures raw PCM audio from the system microphone.
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate |
number |
16000 |
Samples per second (1,000 to 384,000 Hz) |
channels |
number |
1 |
Number of channels each 'data' chunk delivers, interleaved frame by frame. At least 1, with no fixed maximum; without a channelMap, the device's own channel count is the ceiling. 1 is Decibri's own average of every device channel, where earlier releases let the platform collapse the channels first, so a capture from a stereo or multichannel device is not the same signal it was. Below 1 throws RangeError. See Channels for the counts that work without a channelMap and the ones that are refused |
channelMap |
number[] |
(none) | 0-based device channel indices, one per delivered channel: delivered channel j carries device channel channelMap[j]. The length must equal channels. Entries may repeat and their order is significant. Omit it to take the derivation channels describes. See Channels |
framesPerBuffer |
number |
1600 |
Frames per audio callback. Controls chunk size and latency (64 to 65,536) |
device |
number | string | { id: string } |
system default | Device index or case-insensitive name substring from Microphone.devices(), or { id } with a stable per-host device id |
dtype |
'int16' | 'float32' |
'int16' |
Sample encoding format |
vad |
false | 'energy' | 'silero' | VadOptions |
false |
Voice activity detection. false disables it. 'energy' uses RMS thresholding. 'silero' uses the bundled Silero v6.2 ONNX model for ML-based detection. Pass a VadOptions object to tune the threshold, the holdoff, and the delivered channel the detector reads |
modelPath |
string |
(bundled) | Path to a custom Silero VAD ONNX model. Only used when vad is 'silero'. When omitted, uses the model bundled with the package |
dcRemoval |
boolean |
false |
Remove a constant DC offset from the captured audio. See Audio Capture Engine |
denoise |
'fastenhancer-t' |
off | Neural speech enhancement using the bundled model. Unknown value throws TypeError. See Audio Capture Engine |
highpass |
80 | 100 |
off | Butterworth high-pass cutoff in Hz. Any other value throws RangeError. See Audio Capture Engine |
agc |
number |
off | Automatic gain control target in dBFS, -40 to -3. Out-of-range throws RangeError. See Audio Capture Engine |
limiter |
number |
off | Peak limiter ceiling in dBFS, -3.0 to 0.0. Out-of-range throws RangeError. See Audio Capture Engine |
aec |
'tau' | AecOptions |
off | Acoustic echo cancellation. Omit it to leave the stage off. 'tau' selects the model with its defaults. Pass an AecOptions object to tune it. See Acoustic Echo Cancellation (AEC) |
The five conditioning options (dcRemoval, denoise, highpass, agc, limiter) are off by default and run as a fixed chain before each chunk is delivered. They are documented in full on the Audio Capture Engine page. Above one delivered channel, DC removal, denoise and the high-pass run on each channel with its own state, while AGC and the limiter are linked: one detector measures across all the channels and one gain is applied to all of them, which keeps the balance between the channels.
Echo cancellation is a separate opt-in capture option, aec, documented in Acoustic Echo Cancellation (AEC) below. It is the one capture option that also needs a second audio stream from you at run time.
Standard Node.js ReadableOptions (e.g., highWaterMark) are also accepted. Voice activity detection (the vad and modelPath options) is documented in the Voice activity detection section below.
To construct without blocking the event loop on the open work (notably the Silero model load when vad: 'silero' is set), use the Microphone.open() async factory.
channels is a delivered count: the number of channels in every 'data' chunk, interleaved frame by frame, so a chunk holds framesPerBuffer * channels samples. It works the way sampleRate does. The device itself is always opened at its own native channel count, the maxInputChannels figure that Microphone.devices() reports, and Decibri derives the delivered channels from what the device gives it.
Without a channelMap, two counts can be derived:
channels: 1, the default, delivers the average of every device channel.Any other count above 1 without a map is refused when capture starts, because the device's channel count is only read when the stream opens. A count above the device's own fails with the code MICROPHONE_CHANNELS_UNSUPPORTED, because Decibri cannot deliver a channel the device does not have. A count above 1 but below the device's own fails with CHANNEL_SELECTION_AMBIGUOUS, because it does not say which of the device's channels it means. Both arrive as a DecibriError on the 'error' event, not from the constructor. A channelMap lifts both refusals, because it names every delivered channel.
channelMap names device channels. It is an array of 0-based device channel indices with one entry per delivered channel, so its length must equal channels: delivered channel j carries device channel channelMap[j]. Entries may repeat and their order is significant, so a map can select, reorder and duplicate device channels, and it can deliver more channels than the device has. channelMap: [1] with channels: 1 delivers the device's second channel alone, and channelMap: [1, 0] with channels: 2 swaps a stereo pair. An entry the device does not have fails with CHANNEL_MAP_OUT_OF_RANGE on the 'error' event.
vad.source names a delivered channel. It is a position in the 'data' chunk, counted after channelMap has been applied, and never a device index. Omitted, the detector reads the frame average of every delivered channel; set, it reads that one delivered channel. It must be below channels, and it changes only what the detector reads, not the audio the stream emits.
channelMap counts the device's channels; vad.source counts the channels of the chunk you receive. With channelMap: [2, 0], delivered channel 0 is device channel 2, so source: 0 runs the detector on device channel 2, and source: 2 throws a RangeError, because only two channels are delivered.
const { Microphone } = require('decibri');
// Needs a device with at least three input channels.
// Deliver device channels 2 and 0, in that order. source: 0 names
// delivered channel 0, which carries device channel 2.
const mic = new Microphone({
channels: 2,
channelMap: [2, 0],
vad: { model: 'silero', source: 0 },
});
mic.on('error', (err) => console.error(err.code, err.message));
mic.on('data', (chunk) => {
// Int16 LE frames, interleaved: device channel 2, then device channel 0
});
The constructor checks everything that does not need the device, and Microphone.open() rejects with the same errors. The device checks run when the stream starts, which is when you begin consuming it, so attach an 'error' listener first.
| Condition | Class | Message |
|---|---|---|
channels below 1 (constructor) |
RangeError |
channels must be at least 1 |
channelMap is not an array (constructor) |
TypeError |
Invalid channelMap value: <value>. Expected an array of 0-based device channel indices, such as [0]. |
A channelMap entry that is not an integer (constructor) |
TypeError |
channelMap entries must be integers |
A channelMap entry outside 0 to 65535 (constructor) |
RangeError |
channelMap entries must be between 0 and 65535 |
channelMap length differs from channels (constructor) |
RangeError |
channelMap must have exactly one entry per channel |
vad.source is not an integer (constructor) |
TypeError |
vad source must be an integer |
vad.source outside 0 to 65535 (constructor) |
RangeError |
vad source must be between 0 and 65535 |
vad.source not below channels (constructor) |
RangeError |
the detector source names delivered channel <i>; the delivered channel count is <n> |
No channelMap, and channels above the device's own count ('error') |
DecibriError, code: 'MICROPHONE_CHANNELS_UNSUPPORTED' |
the input device does not support <requested> delivered channels; it reports <available> |
No channelMap, and channels above 1 but below the device's own count ('error') |
DecibriError, code: 'CHANNEL_SELECTION_AMBIGUOUS' |
a channel map is required to deliver <requested> of the device's <available> input channels |
A channelMap entry the device does not have ('error') |
DecibriError, code: 'CHANNEL_MAP_OUT_OF_RANGE' |
the channel map names device channel <index>; the device reports <available> input channels |
The three device refusals are a DecibriError carrying a code, not a DeviceError. A 'data' handler that reads each chunk as a run of mono samples needs channels: 1; any higher count that the device accepts delivers interleaved frames.
Microphone.open(options?)Static async factory. Returns Promise<Microphone>. Runs the open work (device resolution, and the Silero model load when vad: 'silero' is set) on the native thread pool instead of the event loop, then resolves to a ready instance. The synchronous constructor blocks for roughly 100 to 500 milliseconds on a cold Silero load; open() does not. Options are identical to the constructor. A failed open rejects with the matching error: RangeError or TypeError for invalid options, or DeviceError, OrtError, or OrtPathError for native failures.
const mic = await Microphone.open({ vad: 'silero' });
mic.on('data', (chunk) => { /* ... */ });
mic.stop()Stops microphone capture and ends the stream. Safe to call multiple times; subsequent calls are no-ops.
Microphone.devices()Returns an array of available audio input devices on the system.
const devices = Microphone.devices();
console.log(devices);
// [
// { index: 0, name: 'Built-in Microphone', id: '...', maxInputChannels: 1,
// defaultSampleRate: 44100, isDefault: true },
// ...
// ]
Each device object contains:
| Property | Type | Description |
|---|---|---|
index |
number |
Device index, used as options.device |
name |
string |
Human-readable device name reported by the OS |
id |
string |
Stable per-host device ID (WASAPI endpoint ID on Windows, CoreAudio UID on macOS, ALSA PCM identifier on Linux). Pass via device: { id }. Empty string when the backend cannot produce a stable ID |
maxInputChannels |
number |
Maximum number of input channels supported: the count the device opens at, which channels and channelMap are checked against when capture starts |
defaultSampleRate |
number |
Device's native/preferred sample rate in Hz |
isDefault |
boolean |
Whether this is the current system default input device |
Microphone.version()Returns version information for decibri and the audio backend. The audioBackend field reports the cpal version, and the binding field reports the npm package version.
Microphone.version();
// { decibri: '<version>', audioBackend: 'cpal <version>', binding: '<version>' }
mic.isOpenboolean (read-only). Returns true while the microphone is actively capturing audio.
mic.vadScorenumber (read-only). Most recent VAD score for the active mode: the Silero speech probability in 'silero' mode, the normalised RMS of the last chunk in 'energy' mode. Above one delivered channel both modes score the frame average of the delivered channels, or the one channel vad.source names. 0 when VAD is disabled or before the first chunk is processed. This is the raw per-chunk view; the debounced speaking state is surfaced through the 'speech' and 'silence' events. See Voice activity detection.
mic.overrunCountnumber (read-only). Number of capture buffers dropped because the consumer could not keep pace. 0 while the consumer keeps up, before capture starts, and after stop(), which releases the stream the counter lives on, so read it before stopping to see a session's total. A rising value means audio is being dropped to bound memory.
Microphone extends Node.js Readable, so all standard stream events are available:
'data': emitted when audio data is available (callback receives Buffer)'end': emitted when the stream ends (after stop())'error': emitted on microphone errors, including device disconnection (for example unplugging the microphone). When a device fails, isOpen becomes false and the 'error' event fires, rather than capture freezing silently.'close': emitted when the stream is closedWhen vad is enabled, the stream additionally emits 'speech' and 'silence'. Those are documented in the Voice activity detection section.
'backpressure'Emitted when the stream's internal buffer is full and the consumer is reading too slowly. Because a microphone cannot be paused, audio chunks continue to arrive. Drain the stream or drop data to avoid unbounded memory growth.
mic.on('backpressure', () => {
console.warn('Consumer is too slow, audio may be lost');
});
Decibri ships two VAD modes plus a disabled default, selected with the vad option on the Microphone constructor. When VAD is enabled, the microphone emits 'speech' and 'silence' events and updates the vadScore property. Both the energy detector and the Silero model run locally; there is no cloud call.
| Mode | Description | Default threshold | Threshold range |
|---|---|---|---|
false |
VAD disabled. No 'speech' or 'silence' events; vadScore stays 0. |
n/a | n/a |
'energy' |
Lightweight RMS-energy threshold computed natively over each chunk. | 0.01 | 0.0 to 1.0 |
'silero' |
ML-based detector using the bundled Silero v6.2 ONNX model, run through ONNX Runtime. | 0.5 | 0.0 to 1.0 |
Both detectors read one mono signal. Above one delivered channel that signal is the frame average of every delivered channel, unless vad.source names one delivered channel for the detector to read instead. vadScore and the 'speech' and 'silence' events describe that signal; the audio the stream emits is not changed by it.
VadOptions objectTo tune the threshold or holdoff, or to choose the delivered channel the detector reads, pass a VadOptions object as the vad option instead of a bare mode string. The vad: 'silero' and vad: 'energy' shorthands are equivalent to the object with its default tuning.
| Field | Type | Default | Description |
|---|---|---|---|
model |
'silero' | 'energy' |
required | Which detector to run |
threshold |
number |
mode default | Speech-detection threshold in [0, 1]. Defaults to 0.5 for 'silero' and 0.01 for 'energy'. A value outside the range throws RangeError |
holdoffMs |
number |
300 |
Milliseconds of sub-threshold audio after a speech period before 'silence' is emitted. A negative value throws RangeError |
source |
number |
(frame average) | The delivered channel the detector reads: a position in the 'data' chunk, counted after any channelMap, never a device index. Omitted, the detector reads the frame average of every delivered channel. A non-integer throws TypeError; a value outside 0 to 65535, or not below channels, throws RangeError. See Channels |
const mic = new Microphone({ vad: { model: 'silero', threshold: 0.6, holdoffMs: 200 } });
'speech' and 'silence' eventsNode surfaces speaking state through events rather than a polled boolean. 'speech' fires when the VAD score crosses the configured threshold. 'silence' fires when the score then stays below the threshold for holdoffMs milliseconds after a speech period. Both require vad to be enabled. Because Microphone is a Readable stream, attach a 'data' listener (or otherwise consume the stream) so audio keeps flowing and the detector runs.
const mic = new Microphone({ sampleRate: 16000, vad: 'energy' });
mic.on('speech', () => console.log('Speech detected'));
mic.on('silence', () => console.log('Silence detected'));
mic.on('data', () => {}); // keep the stream flowing so VAD runs
For the raw per-chunk signal, read the mic.vadScore property documented above. vadScore is the underlying score; the 'speech' and 'silence' events are the debounced state derived from it via the threshold and holdoff.
The Silero VAD ONNX model and ONNX Runtime ship inside the npm package. No separate download, no API key, and no system onnxruntime dependency are required for vad: 'silero'. To use a different Silero ONNX variant, pass an absolute path on the modelPath option; it is consulted only when vad is 'silero'.
The Silero detector and the denoise conditioning stage run on ONNX Runtime, which carries its own telemetry, separate from anything Decibri does. Decibri disables it when it initialises the runtime. Set DECIBRI_ORT_TELEMETRY=1 in the environment before the first load to leave it enabled; any other value, an empty value, and an absent variable leave it disabled.
Two limits apply on Windows, and Decibri can close neither, so it does not claim that no telemetry is emitted. ONNX Runtime logs one process-information event while its environment is being created, before the setting is applied, once per process, so that event is emitted whichever way the setting is left. The runtime also takes its telemetry state from the Windows tracing session through an ETW callback, so the platform can re-enable telemetry after Decibri has disabled it. On other platforms ONNX Runtime's telemetry provider does nothing.
Echo cancellation removes the sound of your own loudspeaker from the microphone capture. It is opt-in through the aec option on the Microphone constructor. Unlike every other capture option, it also needs a second audio stream at run time: the audio you are playing, pushed block by block for as long as you are playing it.
The plumbing, the diagnostics procedure, and the limits of the technique are on the Acoustic Echo Cancellation (AEC) feature page. This section is the Node.js surface.
aec option| Option | Type | Default | Description |
|---|---|---|---|
aec |
'tau' | AecOptions |
undefined (off) |
Echo cancellation. Omit it to leave the stage off; pass 'tau' for the model with its defaults; pass an AecOptions object to tune it |
The declared signature is aec?: 'tau' | AecOptions.
const { Microphone } = require('decibri');
// Short form: the model with its defaults.
const a = new Microphone({ sampleRate: 16000, aec: 'tau' });
// Object form: the same model, every option set to its default.
const b = new Microphone({
sampleRate: 16000,
aec: {
model: 'tau',
tailMs: 200,
suppression: 'conservative',
referenceSampleRate: 16000,
referenceChannels: 1,
},
});
Echo cancellation requires sampleRate between 8000 and 48000, narrower than the range sampleRate otherwise accepts. It runs on every delivered channel: one canceller per delivered channel, each fed the same pushed reference and each finding its own channel's echo delay, so its processing cost grows with channels. It is available on native capture only and is not a File option. The browser build does not carry it; browser capture has the browser's own echoCancellation constraint instead, which is on by default. See the Browser API.
AecOptionsinterface AecOptions {
model: 'tau'; // required in the object form
tailMs?: number; // 16 to 500, default 200
suppression?: 'conservative' | 'off'; // default 'conservative'
referenceSampleRate?: number; // 1000 to 384000, default: the capture rate
referenceChannels?: number; // at least 1, default 1
}
| Field | Type | Default | Description |
|---|---|---|---|
model |
'tau' |
required in the object form | The model to run. 'tau' is the model available today; it carries no model file and needs no download. An unrecognised name is rejected when the microphone is constructed |
tailMs |
number |
200 |
How much echo delay spread the canceller can account for, in milliseconds. 16 to 500 |
suppression |
'conservative' | 'off' |
'conservative' |
'conservative' attenuates the residue the linear stage leaves behind. 'off' delivers the linear output as it stands |
referenceSampleRate |
number |
the capture rate | The rate of the audio you push, in Hz. 1000 to 384000. Set it whenever your playback rate differs from your capture rate |
referenceChannels |
number |
1 |
The channel count of the audio you push, interleaved frame by frame. At least 1; below 1 throws RangeError. Above 1, Decibri averages each frame to one mono sample before the canceller reads it. Set it whenever you push more than one channel |
The declared referenceChannels must match what you push. A mismatch is not detected and throws nothing: the frames are misread, nothing is cancelled, and aecMetrics().delaySamples stays null. The canceller reads one mono reference, so with playback through more than one loudspeaker whose echo paths differ, some echo remains that adaptation does not remove.
mic.pushAecReference(data)Signature pushAecReference(data: Buffer | NodeJS.ArrayBufferView): void. Push the audio you played, as you played it, in the order it was played, in the same dtype the microphone was constructed with, interleaved at referenceChannels (mono by default). Pass a Buffer, Uint8Array or DataView of PCM bytes, or the typed array that carries the configured dtype: Int16Array for 'int16', Float32Array for 'float32'. One push serves every delivered channel.
The push method never blocks and never throws on a full queue. Samples that do not fit are discarded and counted in referenceDropped. A push made while capture is not running, before it starts or after stop(), is discarded too and added to referenceDropped, which you can read once capture runs. With the option unset, a push does nothing and is not counted. A bad input type, including a typed array of the other dtype, throws TypeError regardless of capture state. Because a Microphone stream starts when you begin consuming it, push from inside your 'data' handler or your playback loop, not immediately after the constructor returns.
mic.aecMetrics()Signature aecMetrics(): AecMetrics | null. Returns null when the aec option is unset or capture is not running, and an AecMetrics otherwise. Read it while capture is still running; after stop() it returns null.
AecMetricsinterface AecMetrics {
delaySamples: number | null;
erleDb: number;
doubleTalk: boolean;
referenceStarved: number;
acquisitionParked: number;
referenceReanchors: number;
referenceDropped: number;
referenceSilence: number;
channels: AecChannelMetrics[];
}
| Field | Type | Meaning |
|---|---|---|
delaySamples |
number | null |
Active alignment between reference and capture. null while searching. |
erleDb |
number |
Smoothed estimate of how much echo is being removed, in dB. |
doubleTalk |
boolean |
Whether the near-end talker is believed active. Adaptation is held while true. |
referenceStarved |
number |
Near-end samples with no far-end sample available while aligned. |
acquisitionParked |
number |
Near-end samples processed while no alignment was active. |
referenceReanchors |
number |
Times the alignment was rebuilt after a capture discontinuity. |
referenceDropped |
number |
Far-end samples discarded, at the reference rate: a push that exceeded the queue, or one made while capture was not running. |
referenceSilence |
number |
Far-end samples supplied as silence because none were pushed, at the capture rate. |
channels |
AecChannelMetrics[] |
One entry per delivered channel, in delivered order. See AecChannelMetrics below. |
The first six fields report the canceller on the first delivered channel. referenceDropped and referenceSilence describe the one reference queue that every channel's canceller shares. On a one-channel capture, channels holds a single entry that agrees with the top-level fields.
The diagnostics section of the feature page reads these fields as a procedure rather than a field list. This is that procedure as code:
function diagnose(mic) {
const m = mic.aecMetrics();
if (m === null) {
return 'echo cancellation is off, or capture is not running';
}
if (m.delaySamples !== null) {
return `aligned at ${m.delaySamples} samples, double talk ${m.doubleTalk}`;
}
if (m.referenceSilence >= m.acquisitionParked) {
return 'no reference is reaching the canceller';
}
if (m.referenceDropped > 0) {
return 'pushes are being discarded: push smaller blocks, in step with playback, while capture runs';
}
return 'reference is arriving but nothing has aligned: check the rate';
}
AecChannelMetricsOne entry of AecMetrics.channels: the report of the canceller on one delivered channel. It carries the six canceller fields and not the two queue counters, which stay on AecMetrics. Declared as a TypeScript type.
interface AecChannelMetrics {
delaySamples: number | null;
erleDb: number;
doubleTalk: boolean;
referenceStarved: number;
acquisitionParked: number;
referenceReanchors: number;
}
| Field | Type | Meaning |
|---|---|---|
delaySamples |
number | null |
This channel's active alignment between reference and capture. null while its estimator is still searching. |
erleDb |
number |
Smoothed estimate of how much echo this channel's canceller is removing, in dB. |
doubleTalk |
boolean |
Whether this channel's canceller believes the near-end talker is active. Its adaptation is held while true. |
referenceStarved |
number |
Near-end samples on this channel with no far-end sample available while aligned. |
acquisitionParked |
number |
Near-end samples on this channel processed while no alignment was active. |
referenceReanchors |
number |
Times this channel's alignment was rebuilt after a capture discontinuity. |
Each channel's canceller finds its own echo delay, so the entries differ where the channels' acoustic paths differ. erleDb is not a ranking across channels: it rises with echo distance, because a weaker echo is easier to reduce in ratio terms, so a far microphone can report a higher figure than a near one while removing less echo in absolute terms. Compare a channel with its own history, not with its neighbours.
Microphone. The Python binding splits them: push_aec_reference is a plain method on both classes, while aec_metrics is a coroutine on AsyncMicrophone. The two bindings also differ on pushes made while capture is not running: Node adds them to referenceDropped, and Python does not count them. See the Python API.
Node follows its own convention: built-in TypeError and RangeError for argument validation, and a decibri class carrying a code for everything else. The two bindings raise different classes on purpose; catch them in the idiom of the language you are in, not in a shape shared across both.
| Condition | Class | Message |
|---|---|---|
aec is neither a string nor an object |
TypeError |
Invalid aec value: <value>. Expected a model name such as 'tau', or a config object { model, tailMs, suppression, referenceSampleRate, referenceChannels }. |
model is not a string |
TypeError |
Invalid aec model: <value>. Expected a model name string such as 'tau'. |
model names no known model |
DecibriError, code: 'AEC_CONFIG_INVALID' |
echo canceller configuration error: model must be one of: 'tau'; got '<value>' |
tailMs is not a number |
TypeError |
aec tailMs must be a number |
tailMs is out of range |
RangeError |
aec tailMs must be between 16 and 500 |
suppression is not one of the two values |
TypeError |
aec suppression must be 'conservative' or 'off'; got <value> |
referenceSampleRate is not a number |
TypeError |
aec referenceSampleRate must be a number |
referenceSampleRate is out of range |
RangeError |
aec referenceSampleRate must be between 1000 and 384000 |
referenceChannels is not a number |
TypeError |
aec referenceChannels must be a number |
referenceChannels is below 1 |
RangeError |
aec referenceChannels must be at least 1 |
sampleRate outside 8000 to 48000 with aec set |
RangeError |
echo cancellation only supports sample rates 8000 to 48000 |
pushAecReference given something that is not a buffer view, or a typed array of another sample type, such as a Float64Array |
TypeError |
pushAecReference requires a Buffer, TypedArray, or DataView of PCM samples in the configured dtype |
pushAecReference given the typed array of the other dtype, such as a Float32Array on an 'int16' capture |
TypeError |
dtype 'int16' configured but Float32Array samples were pushed; convert to Int16Array or construct Microphone with dtype: 'float32', and the mirror of it on a 'float32' capture |
Every one of these is raised by the Microphone constructor, except the two pushAecReference rows, which the push itself throws. DecibriError is exported from the package and satisfies instanceof; the RangeError and TypeError cases are built-ins and do not.
Each 2 bytes represents one 16-bit signed integer sample, little-endian. Range: -32,768 to 32,767.
mic.on('data', (chunk) => {
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
});
Each 4 bytes represents one 32-bit IEEE 754 float sample, little-endian. Range: approximately -1.0 to 1.0.
const mic = new Microphone({ sampleRate: 16000, channels: 1, dtype: 'float32' });
mic.on('data', (chunk) => {
const samples = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
});
new Speaker(options?)Creates a Node.js Writable stream for speaker playback. Extends Writable from the stream module.
const { Speaker } = require('decibri');
const speaker = new Speaker(options?);
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate |
number |
16000 |
Samples per second (1,000 to 384,000 Hz) |
channels |
number |
1 |
Number of output channels. At least 1, with no fixed maximum: the output device decides. The stream opens at the first write, and a count above the figure the device reports that the device cannot serve fails there with a DecibriError whose code is SPEAKER_CHANNELS_UNSUPPORTED, on the 'error' event or as a writeAsync() rejection; any other failed open stays STREAM_OPEN_FAILED. Below 1 throws RangeError |
dtype |
'int16' | 'float32' |
'int16' |
Encoding of incoming PCM data |
device |
number | string | { id: string } |
system default | Output device index, case-insensitive name substring, or { id } with a stable per-host device id from Speaker.devices() |
highWaterMark |
number |
16384 |
Writable stream buffer size in bytes |
Standard Node.js WritableOptions are also accepted.
speaker.write(chunk)Writes PCM data for playback. Returns boolean. If false, the buffer is full; wait for the 'drain' event before writing more.
speaker.writeAsync(chunk)Returns Promise<void>. Writes PCM audio without blocking the event loop, performing the backpressure wait (when the native playback queue is full) on the native thread pool, and resolves when the samples are queued. This is an opt-in alternative to write(); do not interleave it with write() or pipe() on the same instance, and await calls sequentially to preserve sample order.
speaker.end([chunk])Graceful stop. Plays all remaining buffered audio, then emits 'finish'. Optionally accepts a final chunk to write before ending.
speaker.drainAsync()Returns Promise<void>. Waits for all queued audio to finish playing without blocking the event loop, resolving when the buffer has drained (immediately if nothing was written). Pair with writeAsync() for a fully non-blocking playback path.
speaker.stop()Immediate stop. Discards remaining buffered audio and releases resources. Does not emit 'finish'. Safe to call multiple times.
Speaker.open(options?)Static async factory, symmetric with Microphone.open(). Returns Promise<Speaker>. The speaker loads no model, so the only open work is device resolution; the factory is provided so async callers can use one construction pattern across both classes. A failed open (for example an unknown device) rejects with the matching error.
Speaker.devices()Returns an array of available audio output devices on the system.
const { Speaker } = require('decibri');
const devices = Speaker.devices();
console.log(devices);
// [
// { index: 0, name: 'Speakers', id: '...', maxOutputChannels: 2,
// defaultSampleRate: 48000, isDefault: true },
// ...
// ]
Each device object contains:
| Property | Type | Description |
|---|---|---|
index |
number |
Device index, used as options.device |
name |
string |
Human-readable device name reported by the OS |
id |
string |
Stable per-host device ID, with the same format and fallback rules as the input-device id. Pass via device: { id } |
maxOutputChannels |
number |
Number of output channels the device reports. A larger Speaker count is still offered to the device, which may serve it; one it cannot serve fails with SPEAKER_CHANNELS_UNSUPPORTED, naming this figure |
defaultSampleRate |
number |
Preferred sample rate for this device |
isDefault |
boolean |
Whether this is the system default output device |
Speaker.version()Returns version information. Same as Microphone.version().
speaker.isPlayingboolean (read-only). Returns true while audio is being output to the speaker.
speaker.underrunCountnumber (read-only). Number of samples played as silence because the playback queue ran dry. 0 while the producer keeps the queue fed, or before playback starts. A rising value means the output is filling gaps with silence.
| Event | Payload | Description |
|---|---|---|
'drain' |
(none) | Writable buffer has space, safe to write again. |
'finish' |
(none) | All data flushed after end(). Every buffered sample has been played. |
'error' |
Error |
Speaker errors. |
'close' |
(none) | Stream closed, all resources released. |
Offline audio source. A File runs a recording, or samples you already hold, through the same conditioning chain as Microphone and delivers it as a finite Readable stream that ends at EOF. Because a recording has an end, it can also score the whole thing for speech in one pass with analyze(). The feature page is Audio File Processor (AFP).
File. The web File API defines one. Import decibri's explicitly with const { File } = require('decibri'), or reference it as decibri.File, so the two do not shadow each other.
new File(path, options?)Opens an audio file synchronously: the read, the decode, and the chain construction all happen inline, so this blocks the event loop on disk I/O and throws from the constructor on a bad path. Fine for a script; prefer File.open() in servers. path must be a string; anything else throws TypeError: path must be a string. WAV, AIFF, AIFF-C and FLAC are read, identified from the file's own bytes rather than its extension; see the format table for the encodings each container carries.
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate |
number |
16000 |
Target output rate in Hz (1,000 to 384,000). The source is resampled from its own rate to this one, so a 44.1 kHz recording comes out at 16 kHz unless you set it |
channels |
number |
1 |
Number of channels each chunk delivers, interleaved frame by frame. At least 1. Without a channelMap, 1 delivers the average of every source channel and a count equal to the source's own delivers every channel in source order. Any other unmapped count above 1 throws from the constructor: above the source's own with FILE_CHANNELS_UNSUPPORTED, and above 1 but below it with FILE_CHANNEL_SELECTION_AMBIGUOUS |
channelMap |
number[] |
(none) | 0-based source channel indices, one per delivered channel, with the same rules as the Microphone option: the length must equal channels, and entries may repeat and their order is significant. An entry the source does not have throws from the constructor with FILE_CHANNEL_MAP_OUT_OF_RANGE |
dtype |
'int16' | 'float32' |
'int16' |
Sample encoding of the delivered chunks |
vad |
false | 'energy' | 'silero' | VadOptions |
false |
Voice activity detection, opt-in exactly as on Microphone, including vad.source, which names a delivered channel here too. analyze() requires 'silero'. See Voice activity detection |
modelPath |
string |
(bundled) | Path to a custom Silero VAD ONNX model. Only used when vad is 'silero' |
dcRemoval |
boolean |
off | Remove a constant DC offset. See Audio Capture Engine |
denoise |
'fastenhancer-t' |
off | Neural speech enhancement using the bundled model. Unknown value throws TypeError. See Audio Capture Engine |
highpass |
80 | 100 |
off | Butterworth high-pass cutoff in Hz. Any other value throws RangeError. See Audio Capture Engine |
agc |
number |
off | Automatic gain control target in dBFS, -40 to -3. Out-of-range throws RangeError. See Audio Capture Engine |
limiter |
number |
off | Peak limiter ceiling in dBFS, -3.0 to 0.0. Out-of-range throws RangeError. See Audio Capture Engine |
Omit an option, or pass undefined, to leave that stage off. The live-capture-only options (device, framesPerBuffer, aec) do not apply to an offline source. channels, channelMap and vad.source do, following the Microphone rules in Channels with the source's own channel count standing where the device's stands: the file's header for a path, or inputChannels for File.buffer. That count is known as soon as the source is opened, so every channel refusal throws from the constructor, or rejects File.open(). Standard Node.js ReadableOptions such as highWaterMark are accepted, because FileOptions extends them. The package's own TypeScript declarations ship with it, so FileOptions and FileBufferOptions type-check in an editor without a separate @types package.
File.open(path, options?)Static async factory. Returns Promise<File>. Runs the disk read, the decode, and the chain construction on the native thread pool instead of the event loop, then resolves to a ready instance. Options are identical to the constructor. This is the recommended form, mirroring Microphone.open().
const { File } = require('decibri');
const file = await File.open('clip.wav', { denoise: 'fastenhancer-t', agc: -18 });
for await (const chunk of file) {
handle(chunk); // Buffer of conditioned Int16 LE PCM
}
file.close();
File.buffer(samples, options)Wraps in-memory samples instead of reading a file. samples must be a Float32Array of samples in -1.0 to 1.0, frame-interleaved at options.inputChannels; a raw Buffer of PCM bytes is rejected as ambiguous, with its own message. Raw samples carry no header, so options.inputRate (their native rate, 1,000 to 384,000) is required, and options.inputChannels (1 to 65,535, default 1) is its channel counterpart. The sample count must be a whole number of frames at inputChannels. channels and channelMap then choose what is delivered from those input channels. inputChannels belongs to File.buffer alone; the path constructors refuse it, because a file carries its own count. No I/O is involved, so unlike File.open() this one is synchronous and returns a File directly, not a promise.
const { File } = require('decibri');
// samples: a Float32Array of mono samples in -1.0 to 1.0, at their own rate
const file = File.buffer(samples, { inputRate: 48000, sampleRate: 16000 });
file.on('data', (chunk) => handle(chunk));
// pairs: stereo samples interleaved L, R, L, R, ... Deliver both channels.
const stereo = File.buffer(pairs, { inputRate: 48000, inputChannels: 2, channels: 2 });
This is also the route for any audio the reader rejects, an MP3 or an Opus file for example: decode it yourself, convert to floats in -1.0 to 1.0, interleaved frame by frame, and pass the source rate as inputRate and its channel count as inputChannels. WAV, AIFF, AIFF-C and FLAC do not need it; the path constructors open those directly.
Streaming, analysis and saving are three alternatives, and each reads the source from beginning to end. Use one File per operation.
analyze() is refused from that point on with a DecibriError carrying the code FILE_ENGAGED. resume(), read(), and attaching a 'data' or 'readable' listener all engage it, synchronously with your own call, whether or not any chunk has arrived yet. pause() on its own and prependListener() do not.save() follows the same rule and checks it in the same order: on an engaged stream it rejects with FILE_ENGAGED before any option is interpreted, so a bad format on an engaged File reports the engagement rather than the option.FILE_CONSUMED. That covers calling analyze() twice, saving twice, and streaming a File that has already been analysed or saved.File that has been drained or closed yields nothing rather than erroring. Only a second pass reports the consumed state.analyze() or save() rejected before it touched the source leaves the File streamable: the no-VAD and energy-mode refusals, and a rejected save option, all return before the pass begins, and no 'error' event is emitted.close() is idempotent, and the three properties keep returning their pre-close values afterwards.file.analyze() and file.analyse()Score the whole recording for voice activity in one pass, off the event loop. Returns Promise<VadReport>, a plain object with exactly the keys scores and segments, all times in seconds of file time. Requires vad: 'silero'. Above one delivered channel it scores the frame average of the delivered channels, or the one channel vad.source names. analyse() is the same analysis under the other spelling.
const file = await File.open('clip.wav', { vad: 'silero' });
const report = await file.analyze();
report.scores; // [{ start, end, vadScore, isSpeech }, ...]
report.segments; // [{ start, end }, ...]
scores holds one entry per analysis window. Windows tile the recording from zero, contiguous and non-overlapping, 0.032 seconds each; a trailing remainder shorter than one window is left unscored. segments holds the merged speech regions: consecutive speech windows whose silence gaps fall within the holdoff collapse into one, and a segment ends at its last speech window rather than at the holdoff expiry.
file.save(path, options?)Run the recording once through the conditioning chain, whole, and write it to path, off the event loop. Returns Promise<SaveReport>. The pass consumes the source exactly as analyze() does. Output is 16-bit PCM at sampleRate, in every one of the three containers, at the delivered channel count and interleaved: the same layout the stream emits, so one channel unless channels asks for more. Each container's own channel ceiling applies, and a save above it rejects with AUDIO_FORMAT_UNSUPPORTED, carrying the container layer's own text: FLAC carries at most 8 channels, and a 16-bit WAV at most 32767. Decibri adds no ceiling of its own.
The container comes from the extension: .wav, .aiff, .aif, .aifc or .flac, matched ASCII case-insensitively. .aifc writes a plain AIFF. An unrecognised extension, or a path with no extension, rejects with AUDIO_FORMAT_UNSUPPORTED rather than defaulting. Decibri reads a file by its content and writes one by its name.
SaveOptions:
| Option | Type | Default | Description |
|---|---|---|---|
format |
'wav' | 'aiff' | 'flac' |
(from the extension) | Container to write, overriding the extension. Any other value throws TypeError |
compression |
number |
5 |
FLAC compression level, 0 to 8. Higher levels search harder for a smaller file; every level decodes to identical audio. Applies to FLAC only and is ignored for WAV and AIFF. Out of range throws RangeError |
SaveReport is a plain object with exactly two keys. clippedSamples counts finite samples that fell outside full scale and were clamped to -1.0 to 1.0: AGC without a limiter can push conditioned audio past full scale, and 16-bit PCM cannot hold it, so Decibri clamps and counts rather than clipping silently. nonFiniteSamples counts non-finite samples the save itself repaired on the way out, a NaN written as silence and an infinity as full scale. That repair and its count cover only the direct path, a one-channel source already at the target rate with no channel map and no conditioning. In every other case the conditioning chain runs and has already replaced each non-finite sample with silence at its entry, so the file carries silence there and the count does not include it.
const file = await File.open('noisy.wav', {
denoise: 'fastenhancer-t',
agc: -18,
limiter: -1.0,
});
const report = await file.save('clean.flac', { compression: 8 });
report.clippedSamples; // number
report.nonFiniteSamples; // number
file.close()Releases the source. Idempotent; a closed File reads as ended.
file.sampleRatenumber (read-only). The target output rate every delivered chunk carries.
file.inputRatenumber (read-only). The source's own rate, read from the file's header or taken from the inputRate passed to File.buffer. Differs from sampleRate whenever the recording is resampled.
file.vadScorenumber (read-only). Most recent per-chunk VAD score for the active mode, computed on the signal before conditioning. 0 when VAD is disabled or before the first chunk.
File extends Readable, so the standard stream events apply: 'data' (payload Buffer), 'end' at EOF, 'error', and 'close'. When vad is enabled it additionally emits 'speech' and 'silence', the same pair the Microphone emits, with the holdoff measured in file time rather than wall-clock time.
const file = await File.open('clip.wav', { vad: 'silero' });
file.on('speech', () => console.log('speech starts'));
file.on('silence', () => console.log('speech ends'));
file.on('error', (err) => console.error(err.code, err.message));
file.on('data', (chunk) => {
// file.vadScore holds the score for the chunk just delivered
});
A freshly constructed File has no listeners of its own, so nothing is subscribed for you.
for await loop and a 'data' listener over the same recording deliver the same total number of bytes in a different number of chunks, because Readable coalesces its internal buffer. Treat the total as exact and the chunk count as a property of the consumption style. A full chunk is 1,600 frames at the target rate, each frame carrying one sample per delivered channel: at the default one channel that is 3,200 bytes as 'int16' and 6,400 as 'float32', and it grows with channels.
The same split the rest of the Node API uses. Failures that crossed the native boundary are a DecibriError with a stable code, apart from the argument checks the native layer shares with the wrapper, which arrive as the same built-in the wrapper throws; failures the JavaScript wrapper caught first are built-in RangeError or TypeError with code undefined.
With a code:
| Trigger | Class | Code | Message |
|---|---|---|---|
| Missing file, a directory, or an empty path | DecibriError |
FILE_READ_FAILED |
Failed to read audio file <path>: <os error> |
| Container decibri cannot decode (an MP3, an AVI), a codec or sample width it does not carry, or a file declaring no channels | DecibriError |
AUDIO_FORMAT_UNSUPPORTED |
unsupported audio format: followed by the reader's own text, which names the container, four-CC, tag or width it found |
save() to an extension decibri does not write, or to a path with no extension |
DecibriError |
AUDIO_FORMAT_UNSUPPORTED |
unsupported audio format: followed by the extension found and the accepted set |
save() with more channels than the container carries |
DecibriError |
AUDIO_FORMAT_UNSUPPORTED |
unsupported audio format: followed by the container layer's own text |
No channelMap, and channels above the source's own count |
DecibriError |
FILE_CHANNELS_UNSUPPORTED |
the file does not have <requested> channels to deliver; it has <available> |
No channelMap, and channels above 1 but below the source's own count |
DecibriError |
FILE_CHANNEL_SELECTION_AMBIGUOUS |
delivering <requested> of the file's <available> channels requires a channel map |
A channelMap entry the source does not have |
DecibriError |
FILE_CHANNEL_MAP_OUT_OF_RANGE |
the file channel map names channel <index>; the file has <available> channels |
Structurally wrong file: a RIFF/WAVE with a fmt chunk and no data chunk, a corrupt FLAC frame |
DecibriError |
AUDIO_FILE_MALFORMED |
malformed audio file: followed by the reader's own text, which names the byte offset and what was expected at it |
| File ends before the audio it declares, including a declared data length that is not a whole number of frames | DecibriError |
AUDIO_FILE_TRUNCATED |
truncated audio file: followed by the reader's own text, which names what was needed against what was available |
save() could not write the file (missing directory, permission failure, full disk) |
DecibriError |
FILE_WRITE_FAILED |
Failed to write audio file <path>: <os error> |
modelPath missing |
OrtError |
VAD_MODEL_LOAD_FAILED |
Silero VAD model not found at <path>. Ensure the models/ directory is included in your installation. |
analyze() or save() after the stream was engaged |
DecibriError |
FILE_ENGAGED |
File iteration has begun; construct a new File to analyze the whole recording |
| Second pass | DecibriError |
FILE_CONSUMED |
File already consumed; construct a new File for another pass |
Without a code, raised by the wrapper before the native layer is reached, apart from the last row, which the native layer checks:
| Trigger | Class | Message |
|---|---|---|
| Non-string path | TypeError |
path must be a string |
Called without new |
TypeError |
Class constructor File cannot be invoked without 'new' |
sampleRate out of range |
RangeError |
sample rate must be between 1000 and 384000 |
inputRate out of range |
RangeError |
inputRate must be between 1000 and 384000 |
Bad dtype |
TypeError |
dtype must be 'int16' or 'float32' |
channels below 1 |
RangeError |
channels must be at least 1 |
channelMap is not an array |
TypeError |
Invalid channelMap value: <value>. Expected an array of 0-based source channel indices, such as [0]. |
A channelMap entry that is not an integer |
TypeError |
channelMap entries must be integers |
A channelMap entry outside 0 to 65535 |
RangeError |
channelMap entries must be between 0 and 65535 |
channelMap length differs from channels |
RangeError |
channelMap must have exactly one entry per channel |
vad.source is not an integer |
TypeError |
vad source must be an integer |
vad.source outside 0 to 65535 |
RangeError |
vad source must be between 0 and 65535 |
vad.source not below channels |
RangeError |
the detector source names delivered channel <i>; the delivered channel count is <n> |
agc out of range |
RangeError |
agc target level must be between -40 and -3 |
limiter out of range |
RangeError |
limiter ceiling must be between -3.0 and 0.0 |
highpass not 80 or 100 |
RangeError |
highpass must be one of: 80, 100 |
Bad denoise value |
TypeError |
Invalid denoise value: "rnnoise". Expected 'fastenhancer-t'. |
vad: true |
TypeError |
vad: true is no longer supported. Specify the mode explicitly: vad: 'silero' or vad: 'energy'. |
Bad vad value |
TypeError |
Invalid vad value: "bogus". Expected false, 'silero', 'energy', or a config object { model, threshold, holdoffMs, source }. |
Bad vad.model |
TypeError |
Invalid vad model: "x". Expected 'silero' or 'energy'. |
vad threshold out of range |
RangeError |
vad threshold must be between 0 and 1 |
vad holdoffMs negative |
RangeError |
vad holdoffMs must be non-negative |
| Legacy flat VAD options | TypeError |
vadThreshold and vadHoldoff are no longer supported. Pass them on the vad config object: vad: { model: 'silero', threshold: 0.5, holdoffMs: 300 }. |
analyze() without VAD |
RangeError |
analysis requires VAD; construct the File with a vad configuration |
analyze() with energy VAD |
RangeError |
analyze() requires vad: 'silero'; energy mode does not support whole-file analysis |
File.buffer given a Buffer |
TypeError |
File.buffer requires a Float32Array of samples, not a Buffer of bytes |
File.buffer given the wrong type |
TypeError |
File.buffer requires a Float32Array of samples |
File.buffer without inputRate |
TypeError |
inputRate is required for File.buffer (samples carry no header) |
File.buffer inputChannels that is not an integer |
TypeError |
inputChannels must be an integer |
File.buffer inputChannels outside 1 to 65535 |
RangeError |
inputChannels must be between 1 and 65535 |
inputChannels passed to new File() or File.open() |
TypeError |
inputChannels applies only to File.buffer; a file carries its channel count in its own header |
save() given a bad format value |
TypeError |
Invalid format value: "ogg". Expected 'wav', 'aiff', or 'flac'. |
save() given a non-numeric compression |
TypeError |
compression must be a number |
save() compression out of range |
RangeError |
flac compression level must be between 0 and 8 |
File.buffer samples that are not a whole number of frames at inputChannels |
RangeError |
the requested block size of <samples> samples is not a whole number of <channels>-channel frames |
analyze() rejects its promise, while a read on a consumed File emits 'error' instead, carrying the same DecibriError and code. Attach an 'error' listener before streaming anything you are not consuming with for await, which rethrows at the loop.
A file sink for PCM audio: the Writable to pair with decibri's Readable sources, and with any other stream of PCM bytes such as a TTS engine or a decoded network stream. It collects the whole stream, then writes it as one audio file when the stream finishes. What it writes is what 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. Nothing reaches disk before then, so the whole stream is held in memory: an hour of 16 kHz mono 'int16' arrives as roughly 115 MB of bytes, and the write converts them to 32-bit floats before encoding, so the peak is a multiple of that. File.save() holds its recording the same way; neither route streams to disk.
File is a Readable, so a Writable to pipe into is the idiom a reader already knows; Python has no stream-sink convention for an AudioWriter to match, so the class would arrive as a new concept rather than a familiar one. Python writes with File.save(). This is the one place the two bindings differ in shape rather than only in naming.
new AudioWriter(path, options)path must be a string, and its extension names the container unless format overrides it. options is required, because sampleRate is. Every option is validated in the constructor, so a bad one throws there rather than surfacing as a deferred 'error' event after the audio has already been streamed. AudioWriterOptions extends SaveOptions, so format and compression are accepted here with the same meanings they have on file.save(), and it extends Node's WritableOptions, so highWaterMark is accepted too.
| Option | Type | Default | Description |
|---|---|---|---|
sampleRate |
number |
required | Rate of the incoming samples in Hz, 1,000 to 384,000, written into the file's header. Required because raw audio carries no header to read a rate from. Absent or non-numeric throws TypeError: sampleRate is required for AudioWriter (raw audio carries no header); out of range throws RangeError |
channels |
number |
1 |
The interleave of the incoming bytes: how many channels each frame carries. It goes into the file's header, and the file is written at this channel count. At least 1; 0 throws RangeError: channels must be at least 1. The stream's total sample count must be a whole number of frames, and each container's own channel ceiling applies as it does for file.save(); both are checked when the stream finishes, and a failure destroys the stream with the error |
dtype |
'int16' | 'float32' |
'int16' |
Sample encoding of the incoming bytes, not of the file. 'int16' is what a File or Microphone emits by default; 'float32' is little-endian raw f32. The file is 16-bit PCM either way |
writer.reportSaveReport | null (read-only). The clippedSamples and nonFiniteSamples counts of the completed write, exactly as file.save() resolves them. For a writer, the direct path that nonFiniteSamples covers is a one-channel stream; above one channel the non-finite samples are written as silence and not counted. null until 'finish' has fired, because the file is written when the stream finishes. A failure destroys the stream with the error rather than populating the report.
const { pipeline } = require('node:stream/promises');
const { File, AudioWriter } = require('decibri');
const file = await File.open('noisy.wav', { denoise: 'fastenhancer-t' });
const writer = new AudioWriter('clean.flac', { sampleRate: 16000 });
await pipeline(
file,
writer,
);
writer.report; // { clippedSamples, nonFiniteSamples }, populated after 'finish'
Convenience functions exported directly from the decibri package.
inputDevices()Shortcut for Microphone.devices(). Returns a MicrophoneInfo[].
outputDevices()Shortcut for Speaker.devices(). Returns a SpeakerInfo[].
version()Returns the same VersionInfo ({ decibri, audioBackend, binding }) as Microphone.version().
const { inputDevices, outputDevices, version } = require('decibri');
const inputs = inputDevices();
const outputs = outputDevices();
console.log(version());
Decibri surfaces failures in three ways in Node, depending on when they occur.
Invalid options throw a built-in RangeError or TypeError synchronously from the constructor (and reject the Microphone.open() or Speaker.open() promise), before any device is opened.
| Condition | Error |
|---|---|
sampleRate outside 1,000 to 384,000 |
RangeError |
channels below 1 |
RangeError |
channelMap not an array, or an entry that is not an integer |
TypeError |
channelMap entry outside 0 to 65,535, or a length that differs from channels |
RangeError |
framesPerBuffer outside 64 to 65,536 |
RangeError |
dtype not 'int16' or 'float32' |
TypeError |
vad not false, a valid mode, or a valid config object. vad: true is no longer supported; specify the mode explicitly |
TypeError |
vad threshold outside [0, 1], or holdoffMs negative |
RangeError |
vad.source not an integer |
TypeError |
vad.source outside 0 to 65,535, or not below channels |
RangeError |
denoise not 'fastenhancer-t' |
TypeError |
highpass not 80 or 100 |
RangeError |
agc outside -40 to -3, or limiter outside -3.0 to 0.0 |
RangeError |
device index out of range, or device.id not a string |
RangeError / TypeError |
Failures from the native layer throw DecibriError or one of its subclasses, each carrying a stable string code. These are thrown from the synchronous constructor and rejected from Microphone.open() or Speaker.open().
| Class | Extends | Cause |
|---|---|---|
DecibriError |
Error |
Base class for native device and ONNX Runtime failures. Carries a code string. |
DeviceError |
DecibriError |
Device enumeration or selection failure: an unmatched device name, an ambiguous match, or missing hardware. |
OrtError |
DecibriError |
ONNX Runtime setup or inference failure (the Silero VAD or denoise stages). |
OrtPathError |
OrtError |
A specific ONNX Runtime library path could not be loaded. |
Catch DecibriError to handle any native failure generically, or a subclass for finer control. Argument validation (above) throws built-in RangeError or TypeError, not a DecibriError. If the Silero model file (the bundled one, or the one modelPath names) or the bundled denoise model file cannot be found at construction, an OrtError is thrown, with the code VAD_MODEL_LOAD_FAILED for the Silero model and MODEL_LOAD_FAILED for the denoise model.
Errors after capture has started, such as a mid-stream device disconnect (unplugging the microphone), are surfaced through the 'error' event rather than thrown. When a device fails, isOpen becomes false and the 'error' event fires, rather than capture freezing silently.
The checks that need the opened device arrive the same way. A Microphone reports MICROPHONE_CHANNELS_UNSUPPORTED, CHANNEL_SELECTION_AMBIGUOUS and CHANNEL_MAP_OUT_OF_RANGE on 'error' when you begin consuming it (see Channels). A Speaker opens its stream at the first write and reports SPEAKER_CHANNELS_UNSUPPORTED there, on 'error', or as the rejection of writeAsync(). Each is a DecibriError carrying that code.