Browser API

Decibri includes a browser build, selected through conditional exports. When the decibri npm package is imported in a browser bundle (webpack, vite, and similar), it resolves to a separate JavaScript implementation that uses getUserMedia and AudioWorklet to capture raw PCM audio chunks in real time and to play PCM audio. The browser build carries capture with channel selection, resampling, energy-based voice activity detection, and playback: a subset of the native engine, listed in full under What the browser build carries.

What the browser build carries

The browser build exports Microphone and Speaker, plus the Emitter base class that Microphone extends. Between them they carry:

The rest of the native engine is not in the browser build:

Audio from the browser Microphone therefore carries no Decibri conditioning or echo cancellation. Any processing applied to it is the browser's own capture processing, such as the echo cancellation and noise suppression that the echoCancellation and noiseSuppression options switch on and off. To condition audio, cancel echo, run Silero, or work with audio files, use the native Node.js entry of the same package (see the Node.js API) or the Python API.

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?, source? }. model is 'energy', the only browser detector. threshold is a number in [0, 1] (default 0.01). holdoffMs is the milliseconds of sub-threshold audio before 'silence' is emitted (default 300). source names the delivered channel the detector reads (see Channels). A threshold or holdoffMs that is not a number, or a source that is not an integer, throws TypeError; a threshold outside [0, 1], a negative holdoffMs, or a source outside 0 to 65535 or not below channels throws RangeError. The messages are listed under Constructor validation and, for source, under Channels.

The detector reads one mono signal. Above one delivered channel that signal is the frame average of every delivered channel, unless source names one delivered channel for the detector to read instead. vadScore and the 'speech' and 'silence' events describe that signal; the audio the 'data' event carries is not changed by it.

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). Capture runs at the audio context's rate, which the browser chooses, and the capture worklet converts each delivered channel to this rate by linear interpolation
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 channel count the browser grants is the ceiling. 1 is the average of every granted channel, where earlier releases delivered the first granted channel alone; a one-channel grant is unchanged. Below 1 throws RangeError. See Channels for what a browser typically grants and the counts that work without a channelMap
channelMap number[] (none) 0-based granted channel indices, one per delivered channel: delivered channel j carries granted 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 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?, source? }) to tune the threshold, the holdoff, and the delivered channel the detector reads
echoCancellation boolean true The browser's own echo cancellation, not Decibri's. Set false for music/tuner apps, and for a multichannel capture (see Channels)
noiseSuppression boolean true The browser's own noise suppression, not Decibri's. Set false for raw signal, and for a multichannel capture (see Channels)
workletUrl string (inline Blob URL) URL for AudioWorklet processor file. Override if CSP blocks blob: URLs

Constructor validation

The constructor validates synchronously, before any permission is requested, and throws a built-in RangeError or TypeError for an invalid option. The classes are not uniform: an out-of-range sampleRate is a RangeError, and an out-of-range framesPerBuffer is a TypeError. The removed vad: true shorthand and the flat vadThreshold and vadHoldoff options throw TypeError with a migration message; pass tuning on the vad config object instead. The rows for channels, channelMap, and vad.source are in the Channels table.

Condition Class Message
sampleRate outside 1,000 to 384,000 RangeError sample rate must be between 1000 and 384000
framesPerBuffer outside 64 to 65,536 TypeError frames per buffer must be between 64 and 65536, got <n>
dtype not 'int16' or 'float32' TypeError dtype must be 'int16' or 'float32'
vad not false, 'energy', or a config object TypeError Invalid vad value: <value>. Expected false, 'energy', or a config object { model, threshold, holdoffMs, source }.
A vad config object whose model is not 'energy' TypeError Invalid vad model: <value>. Expected 'energy'.
vad.threshold is not a number TypeError vad threshold must be a number
vad.threshold outside [0, 1] RangeError vad threshold must be between 0 and 1
vad.holdoffMs is not a number TypeError vad holdoffMs must be a number
vad.holdoffMs negative RangeError vad holdoffMs must be non-negative
vad: true TypeError vad: true is no longer supported. Specify the mode explicitly: vad: 'energy'.
vadThreshold or vadHoldoff passed TypeError vadThreshold and vadHoldoff are no longer supported. Pass them on the vad config object: vad: { model: 'energy', threshold: 0.01, holdoffMs: 300 }.

Channels

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 browser, not Decibri, decides how many channels the capture carries. start() asks getUserMedia for 32 channels as an ideal value rather than a requirement, so the browser grants the count it can serve instead of refusing, and Decibri derives the delivered channels from the channels of the granted track.

What the browser grants is the ceiling. A browser typically grants one processed channel while echoCancellation or noiseSuppression is on, and both are on by default, so a multichannel capture generally needs both set to false.

Without a channelMap, two counts can be derived:

Any other count above 1 without a map is refused when capture starts, because the granted count is only known once the browser has answered. A count above the granted count is refused because Decibri cannot deliver a channel the browser did not grant. A count above 1 but below the granted count is refused because it does not say which of the granted channels it means. A channelMap lifts both refusals, because it names every delivered channel.

channelMap names granted channels. It is an array of 0-based indices into the channels of the granted track, with one entry per delivered channel, so its length must equal channels: delivered channel j carries granted channel channelMap[j]. Entries may repeat and their order is significant, so a map can select, reorder and duplicate granted channels, and it can deliver more channels than the browser granted. channelMap: [1] with channels: 1 delivers the second granted channel alone, and channelMap: [1, 0] with channels: 2 swaps a stereo pair. An entry at or above the granted count is refused when capture starts.

vad.source names a delivered channel. It is a position in the 'data' chunk, counted after channelMap has been applied, and never an index into the granted channels. 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 'data' event carries.

Note: The two indices count different things. channelMap counts the granted channels; vad.source counts the channels of the chunk you receive. With channelMap: [2, 0], delivered channel 0 is granted channel 2, so source: 0 runs the detector on granted channel 2, and source: 2 throws a RangeError, because only two channels are delivered.
import { Microphone } from 'decibri';

// Needs a grant of at least three channels, which generally means
// turning the browser's own processing off.
// Deliver granted channels 2 and 0, in that order. source: 0 names
// delivered channel 0, which carries granted channel 2.
const mic = new Microphone({
  channels: 2,
  channelMap: [2, 0],
  vad: { model: 'energy', source: 0 },
  echoCancellation: false,
  noiseSuppression: false,
});

mic.on('error', (err) => console.error(err.message));
mic.on('data', (chunk) => {
  // Int16Array, interleaved: granted channel 2, then granted channel 0
});

await mic.start();

The constructor checks everything that does not need the grant. start() checks the rest against the channel count the browser reports for the granted track, before any audio is delivered, and rejects with a plain Error that the 'error' event also carries. Where the browser does not report the granted count, start() resolves, and the same Error arrives on 'error' once the audio graph reports the true count; capture then stops, emitting 'end' and 'close'. Attach an 'error' listener before calling start(). At channels: 1 with no map there is nothing to check, because the average serves any grant.

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 granted count (start() or 'error') Error the input device does not support <requested> delivered channels; it reports <granted>
No channelMap, and channels above 1 but below the granted count (start() or 'error') Error a channel map is required to deliver <requested> of the device's <granted> input channels
A channelMap entry at or above the granted count (start() or 'error') Error the channel map names device channel <index>; the device reports <granted> input channels

The constructor rows use the same classes and messages as the Node.js Microphone. The three grant refusals are plain Error objects with no code, since the browser build has no DecibriError; their messages are the native ones, so they say "device" where this page says "granted". A 'data' handler that reads each chunk as a run of mono samples needs channels: 1; any higher count that the grant serves delivers interleaved frames.

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. When channels is above 1 or a channelMap is set, this is also where they are checked against the channel count the browser grants; see Channels.

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]. Above one delivered channel it is the RMS of each frame's average across the delivered channels, or of the one channel vad.source names. 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. The browser build has no native core, so decibri reports the installed package version.

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

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(), such as Microphone permission denied, No microphone found, Microphone access failed: ..., Failed to load audio worklet: ..., or a channel refusal (see Channels). Where the browser does not report the granted channel count, a channel refusal is emitted here after start() has resolved, and capture then stops.
'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
  source?: number,     // delivered channel; default the frame average
}

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 interleaved sample layout
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: the browser's own processing, not Decibri's aec
noiseSuppression N/A boolean (default true) Browser-only option: the browser's own processing, not Decibri's denoise
Conditioning chain, Silero, aec, File, AudioWriter Available Not in the browser build See What the browser build carries
'backpressure' event Available Not available No browser equivalent
pipe() / streams Full Readable stream Not available Browser has no Node streams
channels At least 1; without a channelMap, the device's own channel count is the ceiling At least 1; without a channelMap, the granted channel count is the ceiling Same rules. A browser typically grants one channel while its own processing is on
channelMap, vad.source Map entries are device channel indices; source is a delivered position Map entries are granted channel indices; source is a delivered position Same values, same constructor classes and messages
Channel refusals when capture starts DecibriError with a code, on 'error' Plain Error, rejecting start() or on 'error' Same messages; no code in the browser
sampleRate 1,000 to 384,000. The device opens at its native rate and Decibri's own polyphase resampler converts 1,000 to 384,000. Capture runs at the audio context's rate and the capture worklet converts by linear interpolation Same target rate, different resamplers
framesPerBuffer out of range RangeError TypeError Different classes for the same condition
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 Constructing resolves the device, and the stream opens at the first write; Speaker.open() factory start(), which must run in a user gesture
channels At least 1; the output device decides. The samples you write are interleaved at channels 1 to 32. The samples you write are one mono stream, played on every channel
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.