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.
| Environment | Backend | Entry point |
|---|---|---|
| Node.js | Rust native addon (cpal) | Resolved via conditional export |
| Browser | Web Audio API | Resolved via conditional export |
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).
Microphone access requires HTTPS (or localhost). The browser will show a permission prompt when start() is called.
start() rejects with Error('Microphone permission denied')start() rejects with Error('No microphone found')'error' event also fires with the same errorMicrophone.devices() may be empty before permission is grantedmic.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
// 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();
// 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
});
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).
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();
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+ |
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.
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.
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.
mic.isOpenboolean (read-only). Returns true while actively capturing audio.
mic.vadScorenumber (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.
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' }
| 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).
// MicrophoneInfo
{
deviceId: string,
label: string,
groupId: string,
}
// VersionInfo
{
decibri: string,
}
// VadOptions
{
model: 'energy',
threshold?: number, // [0, 1], default 0.01
holdoffMs?: number, // default 300
}
| 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 |
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.
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();
};
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.
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.
speaker.isPlayingboolean (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.
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) |
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.