npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

decibri

v5.4.0

Published

Cross-platform audio capture, playback, and processing for Node.js and browsers

Readme

decibri

Cross-platform audio capture, playback, and voice activity detection for Node.js and browsers.

Installation

npm install decibri

One package for Node.js and browsers. Node.js gets a prebuilt native addon. Browsers get a JavaScript AudioWorklet implementation. Platform-specific binaries are installed automatically.

Requires Node.js >= 18. TypeScript definitions are bundled.

The package uses named exports:

const { Microphone, Speaker, inputDevices, outputDevices, version } = require('decibri');

Quick Start

Capture audio

const { Microphone } = require('decibri');

const mic = new Microphone({ sampleRate: 16000, channels: 1 });
mic.on('data', (chunk) => { /* Buffer of Int16 PCM samples */ });
setTimeout(() => mic.stop(), 5000);

Condition and analyze a file

const { File } = require('decibri');

// The same conditioning chain as the live microphone, over a WAV file.
const file = await File.open('clip.wav', { denoise: 'fastenhancer-t', highpass: 80 });
file.on('data', (chunk) => { /* Buffer of conditioned Int16 PCM */ });
file.on('end', () => console.log('done'));

// Whole-file speech analysis (a live stream cannot do this).
const f = await File.open('clip.wav', { vad: 'silero' });
const report = await f.analyze();
for (const s of report.segments) console.log(s.start, s.end); // seconds

Play audio

const { Speaker } = require('decibri');

const speaker = new Speaker({ sampleRate: 16000, channels: 1 });
speaker.write(pcmBuffer);
speaker.end();

Browser capture

import { Microphone } from 'decibri'; // browser entry via conditional export

const mic = new Microphone({ sampleRate: 16000 });
mic.on('data', (chunk) => { /* Int16Array of PCM samples */ });
await mic.start(); // requires user gesture in Safari

Browser playback

import { Speaker } from 'decibri'; // browser entry via conditional export

const speaker = new Speaker({ sampleRate: 16000 });
playButton.onclick = async () => {
  await speaker.write(int16Chunk); // Int16Array of PCM samples
  await speaker.drain();           // resolves when playback finishes
  speaker.stop();
};

Pipe capture to playback (echo)

const { Microphone, Speaker } = require('decibri');

const mic = new Microphone({ sampleRate: 16000, channels: 1 });
const speaker = new Speaker({ sampleRate: 16000, channels: 1 });
mic.pipe(speaker);

API: Microphone (Capture)

new Microphone(options?)

Creates a Readable stream that captures from the microphone.

| Option | Type | Default | Description | | --- | --- | --- | --- | | sampleRate | number | 16000 | Samples per second (1000 to 384000) | | channels | number | 1 | Mono only: the only accepted value is 1; a value greater than 1 throws a RangeError | | framesPerBuffer | number | 1600 | Frames per chunk (64 to 65536). At 16kHz mono, 1600 = 100ms = 3200 bytes | | device | number, string, or { id: string } | system default | Device index, case-insensitive name substring, or stable per-host ID | | dtype | 'int16' | 'float32' | 'int16' | Sample encoding | | vad | false | 'silero' | 'energy' | VadOptions | false | Voice activity detection: disabled, the Silero ML model, an RMS energy threshold, or a config object { model, threshold, holdoffMs } to tune the policy | | modelPath | string | bundled model | Path to the Silero model. Only used when vad is 'silero' | | dcRemoval | boolean | off | Remove a constant (DC) offset with a one-pole DC-blocking high-pass. Runs first in the chain; same-length, no added latency | | denoise | 'fastenhancer-t' | off | Single-channel speech-enhancement (denoise) model. The model ships in the package; no path or download needed | | highpass | 80 | 100 | off | High-pass cutoff in Hz (second-order Butterworth) that removes low-frequency rumble. Runs after denoise. Out-of-set values throw a RangeError | | agc | number | off | AGC target level in dBFS, an integer in -40 to -3 (typical -18). Runs after the high-pass. Out-of-range throws a RangeError | | limiter | number | off | Peak limiter ceiling in dBFS, a number in -3.0 to 0.0 (typical -1.0). Runs last. Out-of-range throws a RangeError |

Standard ReadableOptions (e.g. highWaterMark) are also accepted.

vad: true is not accepted; pass the mode explicitly as vad: 'silero' or vad: 'energy'. The string shorthand uses the default threshold (0.5 for 'silero', 0.01 for 'energy') and a 300 ms holdoff; to tune them pass a config object: vad: { model: 'silero', threshold: 0.6, holdoffMs: 200 }. The threshold (01) and holdoffMs fields are optional and fall back to those defaults.

Methods

| Method | Description | | --- | --- | | mic.stop() | Stop capture and end stream. Safe to call multiple times | | Microphone.open(options?) | Construct without blocking the event loop. Returns a Promise<Microphone>. See Non-blocking API | | Microphone.devices() | List available input devices | | Microphone.version() | Version info: { decibri, audioBackend, binding } |

The module-level inputDevices() and version() free functions are equivalent to the static methods.

Properties

| Property | Type | Description | | --- | --- | --- | | mic.isOpen | boolean | true while capturing | | mic.vadScore | number | Latest VAD score for the active mode (Silero probability or normalized RMS); 0 when disabled |

Events

| Event | Payload | Description | | --- | --- | --- | | 'data' | Buffer | Audio chunk (Int16 LE or Float32 LE) | | 'backpressure' | - | Internal buffer full, consumer too slow | | 'speech' | - | VAD: audio crosses threshold | | 'silence' | - | VAD: audio below threshold for the holdoff period | | 'end' | - | Stream ended | | 'error' | Error | An error occurred |

API: Speaker (Playback)

new Speaker(options?)

Creates a Writable stream for speaker playback.

| Option | Type | Default | Description | | --- | --- | --- | --- | | sampleRate | number | 16000 | Playback sample rate (1000 to 384000) | | channels | number | 1 | Output channels (1 to 32) | | dtype | 'int16' | 'float32' | 'int16' | Sample encoding of incoming data | | device | number, string, or { id: string } | system default | Output device index, case-insensitive name substring, or stable per-host ID |

Standard WritableOptions (e.g. highWaterMark) are also accepted.

| Method / Property | Description | | --- | --- | | speaker.write(chunk) | Write PCM data for playback | | speaker.writeAsync(chunk) | Write without blocking the event loop. Returns a Promise. See Non-blocking API | | speaker.end() | Signal end. Drains remaining audio, then emits 'finish' | | speaker.drainAsync() | Wait for queued audio to finish without blocking the event loop. Returns a Promise | | speaker.stop() | Immediate stop. Discards remaining audio | | speaker.isPlaying | true while audio is being output | | Speaker.open(options?) | Construct without blocking the event loop. Returns a Promise<Speaker> | | Speaker.devices() | List available output devices | | Speaker.version() | Same as Microphone.version() |

The module-level outputDevices() free function is equivalent to Speaker.devices().

Non-blocking API

The synchronous constructors and write / drain do their work on the event loop, which is fine for most apps. For event-loop-sensitive code (servers, real-time voice pipelines), 4.1.0 adds async variants that perform the blocking work without stalling the event loop. They are additive: the synchronous API is unchanged, and you opt in only where you need it.

Non-blocking construction

Microphone.open(options?) and Speaker.open(options?) are async factories that return a Promise of a ready instance. They take the same options as the constructors. For a microphone with Silero VAD, the model load that the constructor does inline runs without blocking the event loop.

const { Microphone, Speaker } = require('decibri');

const mic = await Microphone.open({ sampleRate: 16000, vad: 'silero' });
const speaker = await Speaker.open({ sampleRate: 16000, channels: 1 });

The synchronous new Microphone(...) and new Speaker(...) still work unchanged. A failed open rejects the Promise with the same typed error a failed constructor throws.

Non-blocking playback

speaker.writeAsync(chunk) resolves once the audio is queued, performing the backpressure wait (when the playback buffer is full) without blocking the event loop. speaker.drainAsync() resolves when all queued audio has finished playing, again without blocking.

const speaker = await Speaker.open({ sampleRate: 16000, channels: 1 });

await speaker.writeAsync(pcmBuffer);
await speaker.drainAsync(); // resolves when playback finishes

These are a direct alternative to the synchronous write() / pipe() / end() stream interface, which is unchanged. Use one path per instance (the stream methods or the async methods, not both at once), and await calls in sequence to keep samples in order.

Errors

Construction errors come as typed classes you can catch:

const { Microphone, DecibriError, DeviceError } = require('decibri');

try {
  new Microphone({ device: 'no such device' });
} catch (err) {
  if (err instanceof DeviceError) {
    console.log(err.code); // e.g. 'MICROPHONE_NOT_FOUND'
  }
}

DeviceError, OrtError, and OrtPathError extend DecibriError, which extends Error. Each carries a stable code string. Argument validation (bad sampleRate, channels, dtype, or vad) throws a built-in RangeError or TypeError.

API: Browser

The browser API uses getUserMedia and AudioWorklet. It differs from the Node.js API because browser audio is fundamentally async.

new Microphone(options?) (browser)

Same options as Node.js, plus:

| Option | Type | Default | Description | | --- | --- | --- | --- | | device | string | system default | Device ID from Microphone.devices() (not index) | | echoCancellation | boolean | true | Browser echo cancellation | | noiseSuppression | boolean | true | Browser noise suppression | | workletUrl | string | inline blob | Custom worklet URL for strict CSP |

The browser runs energy-mode VAD only, so its vad option accepts false or 'energy'. The browser version() returns { decibri } only: the browser build has no native core, so decibri reports the installed package version. In Node, version().decibri reports the native core version.

Key differences from Node.js

| Aspect | Node.js | Browser | | --- | --- | --- | | Start | Automatic on first read | await mic.start() | | Base class | Readable stream | Custom Emitter | | Data type | Buffer | Int16Array / Float32Array | | devices() | Sync, returns array | Async, returns Promise | | Sample rate | Native device rate | Resampled from native rate | | VAD | 'silero' or 'energy' | 'energy' only |

new Speaker(options?) (browser)

Browser audio playback through the Web Audio API. Playback is async (Promise based) and must be started from a user gesture so the browser allows audio.

import { Speaker } from 'decibri'; // browser entry via conditional export

const speaker = new Speaker({ sampleRate: 16000 });

playButton.onclick = async () => {
  await speaker.write(int16Chunk); // Int16Array of PCM samples
  await speaker.drain();           // resolves when playback finishes
  speaker.stop();
};

| Option | Type | Default | Description | | --- | --- | --- | --- | | sampleRate | number | 16000 | Sample rate of the audio you write (resampled to the output rate) | | channels | number | 1 | Output channels (a mono stream plays on every channel) | | dtype | 'int16' | 'float32' | 'int16' | Encoding of the samples you write | | workletUrl | string | inline blob | Custom worklet URL for strict CSP |

  • start() creates and resumes the audio output. Optional: write() starts it on the first call. Either must run in a user gesture (a click or tap); a context blocked by the autoplay policy surfaces a clear error.
  • write(chunk) resolves when the samples are queued. It waits when the buffer is full, so awaiting it paces playback. Await calls sequentially to preserve order.
  • drain() resolves when the queued audio has finished playing, immediately if nothing is queued.
  • stop() halts immediately and discards anything queued.
  • isPlaying reports whether audio is currently queued and playing.

To verify playback in real browsers, open examples/browser-speaker-test.html (see examples/README.md).

Voice Activity Detection

Energy mode

Lightweight RMS energy threshold. No model required.

const mic = new Microphone({ vad: { model: 'energy', threshold: 0.01 } });
mic.on('speech', () => console.log('speaking'));
mic.on('silence', () => console.log('silent'));

Silero mode

ML-based detection using the Silero VAD v5 model. More accurate than energy mode, especially in noisy environments.

const mic = new Microphone({ vad: { model: 'silero', threshold: 0.5 } });
mic.on('speech', () => console.log('speaking'));
mic.on('silence', () => console.log('silent'));

The Silero model (~2MB) ships inside the npm package. No downloads or API keys required. Silero mode is Node.js only.

decibri ACE (audio conditioning)

decibri ACE (Audio Capture Engine) is decibri's opt-in audio front-end for speech. It is a conditioning chain that runs on the captured audio before it reaches your 'data' handler. Every stage is off by default, runs on-device, and needs no API key. With nothing enabled the capture path is byte-identical to plain capture.

The stages run in a fixed order. Enable any subset:

| Stage | Option | Range | | --- | --- | --- | | DC removal | dcRemoval: true | boolean | | Denoise | denoise: 'fastenhancer-t' | the one bundled model | | High-pass | highpass: 80 or 100 | Hz | | AGC | agc: -18 | dBFS, -40 to -3 | | Limiter | limiter: -1.0 | dBFS, -3.0 to 0.0 |

const { Microphone } = require('decibri');

const mic = new Microphone({
  sampleRate: 16000,
  denoise: 'fastenhancer-t',   // bundled speech-enhancement model
  highpass: 80,                // remove low-frequency rumble
  agc: -18,                    // target level in dBFS
  limiter: -1.0,               // peak ceiling in dBFS
  vad: { model: 'silero', threshold: 0.5 },
});

mic.on('data', (chunk) => { /* Buffer of conditioned Int16 PCM */ });
mic.on('speech', () => console.log('speech'));
mic.on('silence', () => console.log('silence'));
setTimeout(() => mic.stop(), 5000);

VAD reads the signal before the chain, so vadScore and the 'speech' / 'silence' events are unaffected by which conditioning stages you enable. The conditioning chain runs in the native Node.js capture path; the browser build does not include it.

API: File (offline source)

Everything a Microphone does to live audio, File does to audio you already have: the same conditioning options, the same Readable stream of conditioned chunks (finite: it ends at EOF), and the same opt-in vad. Because a File is a complete recording, it can also analyze the whole recording for speech.

  • await File.open(path, options?): read a file off the event loop (recommended, like Microphone.open). Reads WAV, AIFF, AIFF-C and FLAC, identified from the file's own bytes rather than its extension.
  • new File(path, options?): the same result, synchronous (blocks on disk I/O; fine for scripts).
  • File.buffer(samples, options): wrap a Float32Array of samples you already hold. options.inputRate is required (raw samples carry no header); a raw Buffer of bytes is rejected as ambiguous.
  • await file.analyze() (also spelled analyse()): consume the source and resolve to a VadReport of per-window scores ({ start, end, vadScore, isSpeech }) and merged speech segments ({ start, end }), in seconds of file time. Requires vad: 'silero'; a File opened without vad rejects with analysis requires VAD.
  • file.vadScore, 'speech' / 'silence' events: per-chunk VAD alongside the stream, with the holdoff measured in FILE time (sample positions), never wall-clock time, so processing speed does not change the reported events.

Options mirror Microphone (sampleRate, dtype, vad, dcRemoval, denoise, highpass, agc, limiter); the live-capture options (device, channels, framesPerBuffer) do not apply. Iteration and analysis are separate single passes: construct one File per operation. Note: Node also has a global File (the web File API); import decibri's explicitly to avoid shadowing surprises.

Device Selection

const { Microphone } = require('decibri');

// System default
const mic = new Microphone();

// By name (case-insensitive substring match)
const mic = new Microphone({ device: 'USB' });

// By index
const devices = Microphone.devices();
const mic = new Microphone({ device: devices[1].index });

// By stable per-host ID (survives across enumerations)
const mic = new Microphone({ device: { id: devices[1].id } });

Microphone.devices();
// [
//   { index: 0, name: 'Microphone', id: '{0.0.1.00000000}.{...}', maxInputChannels: 2, defaultSampleRate: 48000, isDefault: true },
//   { index: 1, name: 'USB Headset', id: '{0.0.1.00000000}.{...}', maxInputChannels: 1, defaultSampleRate: 44100, isDefault: false }
// ]

Examples

Runnable examples ship with the package. After npm install decibri, find them under node_modules/decibri/examples/. From a clone of the repo they live at npm/decibri/examples/.

# Capture to WAV file (no extra dependencies)
node node_modules/decibri/examples/wav-capture.js

# Stream to WebSocket (requires: npm install ws)
node node_modules/decibri/examples/websocket-server.js   # terminal 1
node node_modules/decibri/examples/websocket-stream.js   # terminal 2

For the browser, examples/browser-speaker-test.html is a page for manually verifying audio playback in each browser. See examples/README.md for how to serve it on desktop and mobile.

Migrating from 3.x

decibri 4.0.0 renames the API to a microphone and speaker vocabulary and switches to named exports. See MIGRATION.md for a complete before-and-after guide.

Platform Support

| Platform | Architecture | Audio Backend | | --- | --- | --- | | Windows | x64 | WASAPI | | macOS | arm64 | CoreAudio | | Linux | x64 | ALSA | | Linux | arm64 | ALSA | | Browser | - | Web Audio API (AudioWorklet) |

How It Works

decibri compiles a Rust audio core to a Node.js native addon and ships prebuilt binaries for each platform, so there is no build step on install. Browser support uses a JavaScript AudioWorklet implementation with the same event-driven API.

On Node.js, audio flows from the OS audio device through frame-exact buffering (which guarantees consistent chunk sizes) and into a standard Readable stream. In the browser, audio is captured and resampled in an AudioWorklet and delivered through the same 'data' event interface.

Documentation

License

Apache-2.0. See LICENSE for details.

Copyright (c) 2026 Decibri.