Node.js API

Complete reference for the decibri Node.js API. For installation and basic usage, see Getting started.

Constructor

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 Mono only. The only accepted value is 1; any other value throws RangeError (multichannel capture is not supported). The option is kept for forward compatibility
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 v5 ONNX model for ML-based detection. Pass a VadOptions object to tune the threshold and holdoff
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.

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.

Methods

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
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: '', audioBackend: 'cpal ', binding: '' }

Properties

mic.isOpen

boolean (read-only). Returns true while the microphone is actively capturing audio.

mic.vadScore

number (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. 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.overrunCount

number (read-only). Number of capture buffers dropped because the consumer could not keep pace. 0 while the consumer keeps up, or before capture starts. A rising value means audio is being dropped to bound memory.

Events

Stream events

Microphone extends Node.js Readable, so all standard stream events are available:

When 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');
});

Voice activity detection

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 v5 ONNX model, run through ONNX Runtime. 0.5 0.0 to 1.0

Tuning with a VadOptions object

To tune the threshold or holdoff, 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
const mic = new Microphone({ vad: { model: 'silero', threshold: 0.6, holdoffMs: 200 } });

Speaking state: the 'speech' and 'silence' events

Node 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.

Bundled Silero model

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'.

Acoustic Echo Cancellation (AEC)

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.

The 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,
  },
});

Echo cancellation requires sampleRate between 8000 and 48000, narrower than the range sampleRate otherwise accepts. Capture is mono, as it is for every decibri capture. The option is available on native capture only; the browser build does not take it, because browser capture carries the platform's own echoCancellation constraint, which is on by default. See the Browser API.

AecOptions

interface 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
}
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

Methods

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, mono.

The push method never blocks and never raises on a full queue. Samples that do not fit are discarded and counted in referenceDropped. A push while capture is not running, or with the option unset, does nothing. A bad input type raises 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.

AecMetrics

interface AecMetrics {
  delaySamples: number | null;
  erleDb: number;
  doubleTalk: boolean;
  referenceStarved: number;
  acquisitionParked: number;
  referenceReanchors: number;
  referenceDropped: number;
  referenceSilence: number;
}
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 because a push exceeded the queue, at the reference rate.
referenceSilence number Far-end samples supplied as silence because none were pushed, at the capture rate.

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';
  }
  return 'reference is arriving but nothing has aligned: check the rate';
}
Note: Node has no separate async microphone class, so both members are synchronous on Microphone. The Python binding splits them: push_aec_reference is a plain method on both classes, while aec_metrics is a coroutine on AsyncMicrophone. See the Python API.

AEC errors

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 }.
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
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 TypeError pushAecReference requires a Buffer, TypedArray, or DataView of PCM samples in the configured dtype

Every one of these is raised by the Microphone constructor, except the last. DecibriError is exported from the package and satisfies instanceof; the RangeError and TypeError cases are built-ins and do not.

Audio format

Int16 (default)

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);
});

Float32

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);
});

Speaker

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 (1 to 32)
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.

Methods

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.

Static methods

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 Maximum number of output channels supported
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().

Properties

speaker.isPlaying

boolean (read-only). Returns true while audio is being output to the speaker.

Events

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.

File

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).

Node.js also has a global 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
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. 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, channels, framesPerBuffer) do not apply to an offline source. 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 mono samples in -1.0 to 1.0; 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. 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));

This is also the route for any audio the reader rejects, an MP3 or an Opus file for example: decode it yourself, convert to mono floats in -1.0 to 1.0, and pass the source rate as inputRate. WAV, AIFF, AIFF-C and FLAC do not need it; the path constructors open those directly.

Single-pass lifecycle

Streaming, analysis and saving are three alternatives, and each reads the source from beginning to end. Use one File per operation.

Methods

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'. 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, mono, at sampleRate, in every one of the three containers; no option changes that.

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 repaired on the way out, a NaN written as silence and an infinity as full scale.

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.

Properties

file.sampleRate

number (read-only). The target output rate every delivered chunk carries.

file.inputRate

number (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.vadScore

number (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.

Events

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.

Note: Chunk framing depends on how you consume the stream. A 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 samples at the target rate: 3,200 bytes as 'int16', 6,400 as 'float32'.

File errors

The same split the rest of the Node API uses. Failures that crossed the native boundary are a DecibriError with a stable code; 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
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:

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'
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 }.
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)
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
Note: The same lifecycle failure arrives by two routes. 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.

AudioWriter

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.

Node.js only, by design. Python has no equivalent and is not getting one. Node's 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 1 1 Audio is written mono, so 1 is the only accepted value. Anything else throws RangeError: multichannel write is not supported; channels must be 1 (mono). There is no reason to pass it
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.report

SaveReport | null (read-only). The clippedSamples and nonFiniteSamples counts of the completed write, exactly as file.save() resolves them. 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'

Module-level helpers

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());

Errors

Decibri surfaces failures in three ways in Node, depending on when they occur.

Constructor argument validation

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 other than 1 (multichannel capture is not supported) 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
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

Native failures

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 bundled Silero or denoise model file cannot be located at construction, a plain Error is thrown.

Runtime stream errors

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.