Browser API

Decibri includes browser support via conditional exports. When imported in a browser bundle (webpack, vite, etc.), it uses getUserMedia and AudioWorklet to capture raw PCM audio chunks in real-time. Same API as the Node.js version, same npm package.

Quickstart

Environment Backend Entry point
Node.js Rust native addon (cpal) Resolved via conditional export
Browser Web Audio API Resolved via conditional export

Install

$ npm install decibri

Quick start

Capture microphone audio and log the chunk size:

import { Microphone } from 'decibri';

const mic = new Microphone({ sampleRate: 16000 });

mic.on('data', (chunk) => {
  console.log(`Received ${chunk.length} samples`);
});

await mic.start();

start() must be called from a user gesture (click/tap) in Safari. Each chunk contains 100 ms of audio by default (1,600 frames at 16 kHz).

Permission handling

Microphone access requires HTTPS (or localhost). The browser will show a permission prompt when start() is called.

Stopping and cleanup

mic.stop();

stop() releases all resources: MediaStream tracks are stopped, AudioContext is closed, all nodes are disconnected, references are nulled. Safe for React/Vue component unmount cycles. Safe to call multiple times or before start().

To restart after stopping:

await mic.start(); // creates a fresh audio pipeline

Device selection

// Start first to trigger permission, then enumerate with labels
await mic.start();
const devices = await Microphone.devices();
console.log(devices);
// [{ deviceId: 'abc123', label: 'Built-in Microphone', groupId: 'g1' }, ...]

// Use a specific device
const usbMic = new Microphone({ device: devices[1].deviceId });
await usbMic.start();

Output formats

// Int16 PCM (default) - ready for most STT engines
const mic = new Microphone({ dtype: 'int16' });
mic.on('data', (chunk) => {
  // chunk is an Int16Array
});

// Float32 - native browser format, no conversion
const mic2 = new Microphone({ dtype: 'float32' });
mic2.on('data', (chunk) => {
  // chunk is a Float32Array
});

Voice activity detection

Enable the built-in VAD to receive 'speech' and 'silence' events based on RMS energy thresholding:

const mic = new Microphone({
  sampleRate: 16000,
  vad: { model: 'energy', threshold: 0.01, holdoffMs: 300 },
});

mic.on('speech', () => console.log('Speaking...'));
mic.on('silence', () => console.log('Silence'));

await mic.start();

The browser build runs energy-mode VAD only. Pass vad: 'energy' for defaults, or a VadOptions object to tune detection: { model: 'energy', threshold?, holdoffMs? }. model is 'energy', the only browser detector. threshold is a number in [0, 1] (default 0.01; a value outside the range throws TypeError). holdoffMs is the milliseconds of sub-threshold audio before 'silence' is emitted (default 300; a negative value throws TypeError).

WebSocket streaming

const ws = new WebSocket('wss://your-server.com/audio');
const mic = new Microphone({ sampleRate: 16000, dtype: 'int16' });

mic.on('data', (chunk) => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(chunk.buffer);
  }
});

document.getElementById('start').onclick = () => mic.start();
document.getElementById('stop').onclick = () => mic.stop();

Browser support

Requires HTTPS (or localhost) for microphone access.

Browser Minimum Version
Chrome 66+
Firefox 76+
Safari 14.1+ (requires user gesture)
Edge 79+
iOS Safari 14.5+
Android Chrome 66+

CSP-restricted environments

By default, decibri loads its AudioWorklet processor via an inline Blob URL. If your Content Security Policy blocks blob: URLs:

const mic = new Microphone({
  workletUrl: '/static/decibri-worklet.js',
});

Copy the worklet file from node_modules/decibri/src/browser/worklet-processor.js to your static assets directory and pass its URL as workletUrl. A self-hosted worklet file must register the processor under the exact name decibri-processor (the name requested when the AudioWorkletNode is created); the file shipped in the package already does.

API reference

Constructor

new Microphone(options?)

Creates a new capture instance. Does not start capture. Call start() to begin.

import { Microphone } from 'decibri';
const mic = new Microphone(options?);
Option Type Default Description
sampleRate number 16000 Target sample rate in Hz (1,000 to 384,000)
channels number 1 Number of channels (1 to 32; browsers reliably support 1)
framesPerBuffer number 1600 Frames per chunk. 1,600 at 16 kHz = 100 ms chunks (64 to 65,536)
device string system default deviceId string from Microphone.devices()
dtype 'int16' | 'float32' 'int16' Sample encoding format
vad false | 'energy' | VadOptions false Voice activity detection. false disables it; 'energy' enables it with default tuning; pass a VadOptions object ({ model: 'energy', threshold?, holdoffMs? }) to tune the threshold and holdoff
echoCancellation boolean true Browser echo cancellation. Set false for music/tuner apps
noiseSuppression boolean true Browser noise suppression. Set false for raw signal
workletUrl string (inline Blob URL) URL for AudioWorklet processor file. Override if CSP blocks blob: URLs

The constructor validates synchronously and throws TypeError for invalid options: a sampleRate, channels, or framesPerBuffer outside its range, a dtype other than 'int16' or 'float32', or an invalid vad value. For backward compatibility, the removed vad: true shorthand and the flat vadThreshold and vadHoldoff options also throw TypeError with a migration message; pass tuning on the vad config object instead.

Methods

mic.start()

Returns Promise<void>. Requests microphone permission and begins capture. Must be called from a user gesture in Safari. No-op if already started, and a call made while a start is still in progress returns that same pending promise rather than starting a second pipeline. Rejects with a clear error on permission denial.

mic.stop()

Stops capture and releases all resources (tracks, context, nodes). Safe to call anytime, including before start() or multiple times. When it stops an active capture it emits 'end' then 'close'; calling it before start() or after it has already stopped is a no-op and emits nothing.

Properties

mic.isOpen

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

mic.vadScore

number (read-only). Most recent VAD score: the normalized RMS energy of the last chunk, in [0, 1]. The browser runs energy VAD only, so this is always the energy score (there is no Silero in the browser build). 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.

Static methods

Microphone.devices()

Returns Promise<MicrophoneInfo[]>. Lists available audio input devices. Labels may be empty before microphone permission is granted.

const devices = await Microphone.devices();
console.log(devices);
// [
//   { deviceId: 'abc123', label: 'Built-in Microphone', groupId: 'g1' },
//   ...
// ]

Microphone.version()

Returns version information for decibri.

Microphone.version();
// { decibri: '5.0.0' }

Events

Event Payload Description
'data' Int16Array or Float32Array Audio chunk. Format depends on dtype option. Emitted ~10 times/sec at default settings.
'error' Error Emitted with the same Error that rejects start(): Microphone permission denied, No microphone found, or Failed to load audio worklet: ....
'end' (none) Emitted after stop().
'close' (none) Emitted after stop(), after 'end'.
'speech' (none) VAD: RMS energy crossed threshold. Requires vad to be enabled.
'silence' (none) VAD: emitted after a 'speech' period when audio stays sub-threshold for the configured holdoffMs. Fires only after a preceding 'speech'; sub-threshold audio with no prior speech does not emit it. Requires vad to be enabled.

Listeners use an EventEmitter-style API: on(event, listener) and once(event, listener) to subscribe, and off(event, listener) or removeAllListeners(event?) to unsubscribe (useful when tearing down a component).

Types

// MicrophoneInfo
{
  deviceId: string,
  label: string,
  groupId: string,
}

// VersionInfo
{
  decibri: string,
}

// VadOptions
{
  model: 'energy',
  threshold?: number,  // [0, 1], default 0.01
  holdoffMs?: number,  // default 300
}

Differences from the Node.js API

Feature Node.js decibri decibri (browser) Notes
Class name Microphone Microphone Identical
Constructor Sync, capture starts on read Sync, requires await start() Browser needs async permission
'data' payload Buffer Int16Array / Float32Array Different types, same PCM data
devices() Sync Async (returns Promise) Browser API is async
device option Number index, name substring, or { id } object String deviceId only Browser uses opaque device IDs
version() { decibri, audioBackend, binding } { decibri } Different runtime info
echoCancellation N/A boolean (default true) Browser-only option
noiseSuppression N/A boolean (default true) Browser-only option
'backpressure' event Available Not available No browser equivalent
pipe() / streams Full Readable stream Not available Browser has no Node streams
channels Mono only (other values throw) 1 to 32 The browser accepts a channel count from 1 to 32; Node captures mono only
sampleRate Any (cpal resamples) Any (AudioWorklet resamples) Same behavior
dtype 'int16' or 'float32' 'int16' or 'float32' Identical
VAD (speech/silence) Energy (RMS) and Silero (ONNX) Energy (RMS) only Browser offers the energy detector only; Silero is native-only (bundled ONNX) and not available in the browser build

Speaker

The browser Speaker plays PCM audio out through the Web Audio API and an output AudioWorklet. It is the inverse of the Microphone: the microphone captures input and emits chunks; the speaker accepts chunks and plays them. The API is promise-based and has no events and no static methods: progress is observed by awaiting write() and drain(), and failures surface as a rejected promise (or a TypeError thrown synchronously from the constructor). The first start() or write() must run from a user gesture so the browser allows audio.

Playback quickstart

Write PCM samples from a click handler, wait for playback to finish, then release resources:

import { Speaker } from 'decibri';

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

document.getElementById('play').onclick = async () => {
  await speaker.write(int16Chunk); // Int16Array of PCM samples
  await speaker.drain();           // wait for playback to finish
  speaker.stop();
};

Constructor

new Speaker(options?)

Creates a playback instance. Does not open the audio output; the pipeline is created on the first start() or write().

import { Speaker } from 'decibri';
const speaker = new Speaker(options?);
Option Type Default Description
sampleRate number 16000 Sample rate in Hz of the audio you write (1,000 to 384,000). Samples are resampled to the audio context's native rate before playback
channels number 1 Number of output channels (1 to 32). A mono stream is played on every channel
dtype 'int16' | 'float32' 'int16' Sample encoding of the audio you write
workletUrl string (inline Blob URL) URL for the output AudioWorklet processor file. Override if CSP blocks blob: URLs

The constructor validates synchronously and throws TypeError for invalid options: a sampleRate outside 1,000 to 384,000, a channels outside 1 to 32, or a dtype other than 'int16' or 'float32'. There is no device option; the browser Speaker plays to the default output device.

Methods

speaker.start()

Returns Promise<void>. Creates and resumes the AudioContext and loads the output worklet. Must be called from a user gesture so the browser allows audio. No-op if already started, and a call made while a start is still in progress returns that same pending promise. Optional: write() starts the pipeline on its own, but calling start() from a click handler is the reliable way to unlock audio before samples are ready.

speaker.write(chunk)

Returns Promise<void>. Plays PCM audio. chunk is an Int16Array, Float32Array, or ArrayBuffer of samples in the configured dtype. The samples are converted to the context rate and queued; the returned promise resolves when they are accepted, applying backpressure when the internal buffer (a fixed window of about 2 seconds) is full, so awaiting write() paces the caller to playback. Await calls sequentially to preserve sample order. An empty chunk resolves immediately. Pass data matching the configured dtype: a typed array of the other element type is byte-reinterpreted, not rejected.

speaker.drain()

Returns Promise<void>. Resolves when all queued audio has finished playing. Resolves immediately if nothing is queued.

speaker.stop()

Stops playback immediately, discards any queued audio, and releases all resources (worklet node, audio context). Safe to call anytime, including before start() or multiple times. After stop(), a later write() or start() begins a fresh session.

Properties

speaker.isPlaying

boolean (read-only). Returns true while the speaker is started and has queued audio that has not finished playing. It becomes false once the buffer drains, even if stop() has not been called.

Differences from the Node.js Speaker

The browser Speaker is a minimal promise-based playback class, not the Node.js Writable stream.

Feature Node.js Speaker Speaker (browser)
Model Writable stream with 'drain', 'finish', 'error', 'close' events Promise-based, no events
write() Returns boolean (plus writeAsync()) Returns Promise<void> with awaitable backpressure
Drain drainAsync() and the 'drain' event drain() returns Promise<void>
Graceful end end([chunk]) then 'finish' None (await drain(), then stop())
Start Opens on construct or first write; Speaker.open() factory start(), which must run in a user gesture
Output device device option and Speaker.devices() None (default output device only)
version() Speaker.version() None
Buffering highWaterMark Fixed internal buffer (about 2 seconds)
workletUrl N/A Supported (CSP override)

CSP-restricted environments (Speaker)

Like the Microphone, the Speaker loads its output AudioWorklet via an inline Blob URL by default. If your Content Security Policy blocks blob: URLs, copy node_modules/decibri/src/browser/output-worklet-processor.js to your static assets directory and pass its URL as workletUrl. A self-hosted file must register the processor under the exact name decibri-output-processor (the name requested when the output AudioWorkletNode is created); the file shipped in the package already does.