Decibri ships a native Python package with synchronous Microphone and Speaker classes plus matching AsyncMicrophone and AsyncSpeaker classes for asyncio. Written in Rust (via PyO3 / abi3) with pre-built wheels for Python 3.10 and newer. For installation and first capture, see Getting started.
Three ways to get audio into and out of Python, in order of increasing control.
Recommended with uv:
Or with pip:
For NumPy ndarray support (see Audio format):
import decibri
decibri.record_to_file("output.wav", duration_seconds=10)
Captures 10 seconds of microphone audio to a 16-bit PCM WAV file at 16 kHz mono. No async, no streaming, no setup.
with blockimport decibri
with decibri.Microphone(sample_rate=16000) as mic:
for chunk in mic:
print(f"Got {len(chunk)} bytes")
break
Open the system microphone, iterate raw 16-bit PCM chunks, break after the first. Replace break with your processing pipeline.
import asyncio
import decibri
async def main():
async with await decibri.AsyncMicrophone.open(sample_rate=16000) as mic:
async for chunk in mic:
print(f"Got {len(chunk)} bytes")
break
asyncio.run(main())
Same loop, but on the event loop. Use this in voice agents or websocket pipelines.
| Python versions | Platforms |
|---|---|
| 3.10, 3.11, 3.12, 3.13, 3.14 | Linux x64, Linux ARM64, macOS Apple Silicon, Windows x64 |
Pre-built wheels are published for every supported platform. pip install decibri fetches a binary wheel; no Rust toolchain, no C compiler, and no system audio headers are required at install time.
Wheels are built against the CPython stable ABI (abi3) with a 3.10 floor, so a single wheel per platform serves every supported interpreter version. New CPython releases work without a new decibri release as long as the stable ABI is preserved.
Primary capture surface for synchronous code. Construct an instance, enter a with block (or call start() manually), then iterate or call read() for chunks.
decibri.Microphone(
sample_rate=16000,
channels=1,
frames_per_buffer=1600,
dtype="int16",
device=None,
vad=False,
model_path=None,
as_ndarray=False,
ort_library_path=None,
denoise=None,
highpass=None,
agc=None,
limiter=None,
dc_removal=False,
aec=None,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
sample_rate |
int |
16000 |
Samples per second (1,000 to 384,000 Hz). 16,000 matches Silero VAD and most cloud STT providers; OpenAI Realtime requires 24,000. |
channels |
int |
1 |
Capture is mono only; the only accepted value is 1. A value greater than 1 raises MultichannelNotSupported; a zero or otherwise out-of-range value raises ChannelsOutOfRange. |
frames_per_buffer |
int |
1600 |
Frames per audio callback. 1,600 at 16 kHz is 100 ms chunks (64 to 65,536). |
dtype |
"int16" | "float32" |
"int16" |
Sample encoding format. |
device |
int | str | None |
system default | Device index from Microphone.devices() or case-insensitive name substring. |
vad |
False | "silero" | "energy" | Vad |
False |
Voice activity detection. False disables it; "silero" or "energy" selects a mode with default tuning; pass a decibri.Vad(...) object to tune the threshold and holdoff. See Voice activity detection. |
model_path |
str | Path | None |
bundled | Override path to a Silero VAD ONNX model. Only used when vad="silero"; defaults to the model bundled with the wheel. |
as_ndarray |
bool |
False |
When True, read() returns a numpy.ndarray instead of bytes. Requires pip install decibri[numpy]. |
ort_library_path |
str | Path | None |
resolver | Override path to the ONNX Runtime dynamic library. Only used when vad="silero". See ONNX Runtime resolution for the four-arm priority order. |
dc_removal |
bool |
False |
Remove a constant DC offset from the captured audio. See Audio Capture Engine. |
denoise |
"fastenhancer-t" | None |
None |
Neural speech enhancement using the bundled model. An unknown value raises ValueError. See Audio Capture Engine. |
highpass |
80 | 100 | None |
None |
Butterworth high-pass cutoff in Hz. Any other value raises ValueError. See Audio Capture Engine. |
agc |
int | None |
None |
Automatic gain control target in dBFS, -40 to -3. Out of range raises AgcTargetOutOfRange with the message agc must be in [-40, -3]; got -41. See Audio Capture Engine. |
limiter |
float | None |
None |
Peak limiter ceiling in dBFS, -3.0 to 0.0. Out of range raises LimiterCeilingOutOfRange with the message limiter must be in [-3.0, 0.0]; got -3.1. See Audio Capture Engine. |
aec |
str | Aec | None |
None |
Acoustic echo cancellation. None leaves it off; "tau" selects the model with default tuning; pass a decibri.Aec(...) object to tune it. See Acoustic Echo Cancellation (AEC). |
The five conditioning options (dc_removal, 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 parameter, 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.
The canonical Python pattern. Entering the with block opens the stream and starts capture; exiting stops the stream and resets VAD state, even if an exception propagates out.
import decibri
with decibri.Microphone(sample_rate=16000, channels=1, frames_per_buffer=1600) as mic:
for chunk in mic:
process(chunk)
if done():
break
Calling start() manually is also supported when the context manager does not fit. Pair it with stop() in a try / finally.
mic.start()Open and start the capture stream. Calling start() after stop() or close() is supported and reconstructs the stream cleanly; VAD state resets on each new start(). Calling start() on an already-running instance raises AlreadyRunning.
mic.stop()Stop the capture stream and reset VAD state and the sequence counter. Idempotent; safe to call multiple times.
mic.close()Alias for stop(). Provided for ergonomic parity with the asyncio / aiohttp / httpx convention. The two methods are currently equivalent and are intended to remain interchangeable.
mic.read(timeout_ms=None)Read one chunk. Returns the chunk, or None if the stream closed. Return type is bytes by default, or numpy.ndarray when the Microphone was constructed with as_ndarray=True. Advances VAD state as a side effect when VAD is enabled.
mic.read_with_metadata(timeout_ms=None)Read one chunk and return it as a frozen Chunk with .data, .timestamp, .sequence, .is_speaking, and .vad_score attributes. Returns None on clean stream close. See Value types.
mic.iter_with_metadata()Generator yielding Chunk objects until the stream closes cleanly. Use this in place of for chunk in mic when you want metadata alongside the audio data.
with decibri.Microphone(vad="silero") as mic:
for chunk in mic.iter_with_metadata():
if chunk.is_speaking:
send_to_stt(chunk.data)
iter(mic) and next(mic)The Microphone is itself an iterator. for chunk in mic: yields the raw data shape (bytes or numpy.ndarray) and raises StopIteration when the stream closes.
mic.is_openbool (read-only). Returns True while the capture stream is currently running.
mic.is_speakingbool (read-only). Returns True while VAD considers the user to be speaking, including the holdoff grace period. Always False when vad=False. Holdoff expiry is checked on every property access, so consumers who pause iteration still observe correct state when they next read.
mic.vad_scorefloat in [0, 1], mode-agnostic. In vad="silero" mode this is the raw Silero probability for the most recent chunk; in vad="energy" mode it is the normalised RMS energy. Always 0.0 when vad=False.
Microphone.devices()Returns a list of MicrophoneInfo objects describing every input device recognised by the operating system.
for d in decibri.Microphone.devices():
print(d.index, d.name, d.default_sample_rate)
Microphone.version()Returns a VersionInfo object with the Rust core version, the audio backend version, and the binding wheel version.
All Microphone instances support repr() for debugging; the output includes sample rate, channels, dtype, frames per buffer, device, VAD mode, and open state.
Audio output surface. Construct, enter a with block, write samples, and drain.
decibri.Speaker(
sample_rate=16000,
channels=1,
dtype="int16",
device=None,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
sample_rate |
int |
16000 |
Output sample rate in Hz (1,000 to 384,000). Use 24,000 for OpenAI Realtime playback. |
channels |
int |
1 |
Number of output channels (1 to 32). Multi-channel samples are interleaved on the wire. |
dtype |
"int16" | "float32" |
"int16" |
Sample dtype. Must match the data passed to write(); mismatch raises TypeError. |
device |
int | str | None |
system default | Device index from Speaker.devices() or case-insensitive name substring. |
import decibri
with decibri.Speaker(sample_rate=16000, channels=1) as spk:
spk.write(audio_bytes)
spk.drain()
spk.start()Open and start the output stream. Re-entry after stop() or close() is supported.
spk.stop()Stop the output stream.
spk.close()Alias for stop(). See Microphone.close() for the equivalence note.
spk.write(samples)Write a chunk to the output stream. Accepts bytes or a numpy.ndarray with dtype matching the configured dtype. Multi-channel ndarrays use shape (N, channels). Output streams duck-type the input on each call rather than committing at construction time; mixing bytes and ndarrays across calls is supported. Raises TypeError on dtype mismatch or unsupported input.
spk.drain()Block until all queued samples have been played. Useful at the end of a playback sequence so the program does not exit before the speaker buffer empties.
spk.is_playingbool (read-only). Returns True while the output stream is currently running.
Speaker.devices()Returns a list of SpeakerInfo objects describing every output device recognised by the operating system.
Offline audio source. A File runs a recording, or samples you already hold, through the same conditioning chain as Microphone, and delivers the same chunk shape. 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).
decibri.File(
path,
sample_rate=16000,
dtype="int16",
vad=False,
model_path=None,
as_ndarray=False,
ort_library_path=None,
denoise=None,
highpass=None,
agc=None,
limiter=None,
dc_removal=False,
)
Every parameter after path is keyword-only. An unknown keyword raises TypeError.
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str | Path |
required | Path to an audio file. 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. Multi-channel files are downmixed to mono. |
sample_rate |
int |
16000 |
Target output rate in Hz (1,000 to 384,000). The source is resampled from its own rate to this one. |
dtype |
"int16" | "float32" |
"int16" |
Sample encoding of the delivered chunks. |
vad |
False | "silero" | "energy" | Vad |
False |
Voice activity detection, opt-in exactly as on Microphone. analyze() requires "silero". See Voice activity detection. |
model_path |
str | Path | None |
bundled | Override path to a Silero VAD ONNX model. Only used when vad="silero". |
as_ndarray |
bool |
False |
When True, reads return a numpy.ndarray instead of bytes. Requires pip install decibri[numpy]; without it the constructor raises ImportError. |
ort_library_path |
str | Path | None |
resolver | Override path to the ONNX Runtime dynamic library. See ONNX Runtime resolution. |
dc_removal |
bool |
False |
Remove a constant DC offset. See Audio Capture Engine. |
denoise |
"fastenhancer-t" | None |
None |
Neural speech enhancement using the bundled model. An unknown value raises ValueError. See Audio Capture Engine. |
highpass |
80 | 100 | None |
None |
Butterworth high-pass cutoff in Hz. Any other value raises ValueError. See Audio Capture Engine. |
agc |
int | None |
None |
Automatic gain control target in dBFS, -40 to -3. Out of range raises AgcTargetOutOfRange. See Audio Capture Engine. |
limiter |
float | None |
None |
Peak limiter ceiling in dBFS, -3.0 to 0.0. Out of range raises LimiterCeilingOutOfRange. See Audio Capture Engine. |
The five conditioning options behave exactly as they do on Microphone, in the same fixed chain, and are documented in full on the Audio Capture Engine page.
File is a context manager. Entering the block hands back the same instance; exiting closes the source.
import decibri
with decibri.File("clip.wav", denoise="fastenhancer-t", agc=-18) as file:
for chunk in file:
handle(chunk) # 3,200 bytes per full chunk at dtype="int16"
A full chunk is 1,600 samples at the target rate: 3,200 bytes as int16, 6,400 bytes as float32. The final chunk is shorter whenever the total does not divide evenly.
File.open(path, **kwargs)Classmethod, identical to File(path, **kwargs) and accepting the same keywords. It is an alternate spelling of the constructor, not an async variant; for asyncio use AsyncFile.
File.buffer(samples, *, input_rate, **kwargs)Wraps in-memory samples instead of reading a file. samples is a list of floats or a one-channel numpy.ndarray with a floating dtype, mono, in the range -1.0 to 1.0. Raw samples carry no header, so input_rate (their native rate, 1,000 to 384,000) is a mandatory keyword-only argument. Every other keyword matches the constructor.
file = decibri.File.buffer(samples, input_rate=48000, sample_rate=16000)
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 input_rate. The conditioning chain then behaves identically to the path constructor. WAV, AIFF, AIFF-C and FLAC do not need it; the path constructor opens those directly.
Iteration, analysis and saving are three alternatives, and each reads the source from beginning to end. Use one File per operation.
analyze() after iteration has begun raises FileEngaged. Any call that advances the cursor marks the File engaged, including one that returns nothing: on an empty recording the first read() returns None and a later analyze() still raises.save() follows the same rule and checks it in the same order: after iteration has begun it raises FileEngaged, before any argument is interpreted, so a bad format on an engaged File reports the engagement rather than the argument.FileConsumed. That covers calling analyze() twice, saving twice, and iterating a File that has already been analysed or saved.File that has been drained or closed yields nothing rather than raising. Only analysis and saving report the consumed state.analyze() that never touched the source leaves the File iterable: both VadNotConfigured and the energy-mode refusal return before the pass begins.close() is idempotent, and the four accessors keep returning their pre-close values afterwards.file.read()Read one conditioned chunk. Returns the chunk, or None at the end of the source. Return type is bytes by default, or numpy.ndarray when the File was constructed with as_ndarray=True. Advances VAD state as a side effect when VAD is enabled.
file.read_with_metadata()Read one chunk and return it as a frozen Chunk, or None at the end. On a File, Chunk.timestamp is the chunk's position in seconds of file time from the start of the recording, not a time.monotonic() snapshot, and Chunk.is_speaking applies the holdoff in file time. See Value types.
file.iter_with_metadata()Generator yielding Chunk objects until the end of the source.
with decibri.File("clip.wav", vad="silero") as file:
for chunk in file.iter_with_metadata():
if chunk.is_speaking:
send_to_stt(chunk.data)
iter(file) and next(file)The File is itself an iterator. for chunk in file: yields the raw data shape (bytes or numpy.ndarray) and raises StopIteration at the end of the source.
file.analyze() and file.analyse()Score the whole recording for voice activity in one pass and return a VadReport. Requires vad="silero": a File built without a VAD configuration raises VadNotConfigured, and energy mode raises ValueError because it has no whole-recording analysis. On the sync class the two spellings are one object: File.analyse is File.analyze is True.
report = decibri.File("clip.wav", vad="silero").analyze()
report.scores # list[VadWindow]: start, end, vad_score, is_speech
report.segments # list[Segment]: start, end
file.save(path, *, format=None, compression=None)Run the recording once through the conditioning chain, whole, and write it to path. Returns a SaveReport. Both keyword arguments are keyword-only, and the pass consumes the source exactly as analyze() does.
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str | Path |
required | Destination. Its extension names the container: .wav, .aiff, .aif, .aifc or .flac, matched ASCII case-insensitively. .aifc writes a plain AIFF. An unrecognised extension, or a path with no extension, raises AudioFormatUnsupported rather than defaulting. |
format |
"wav" | "aiff" | "flac" | None |
None |
Container to write, overriding the extension. None takes it from the extension. Any other value raises ValueError. |
compression |
int | None |
None (level 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 raises FlacCompressionOutOfRange. |
Output is 16-bit PCM, mono, at sample_rate, in every one of the three containers. No parameter changes that. Decibri reads a file by its content and writes one by its name, which is why a save needs an extension or an explicit format and a read needs neither.
file = decibri.File("noisy.wav", denoise="fastenhancer-t", agc=-18, limiter=-1.0)
report = file.save("clean.flac", compression=8)
report.clipped_samples # int
report.non_finite_samples # int
file.close()Release the source. Idempotent; a closed File reads as ended.
file.sample_rateint (read-only). The target output rate every delivered chunk carries.
file.input_rateint (read-only). The source's own rate, read from the file's header or taken from the input_rate passed to File.buffer. Differs from sample_rate whenever the recording is resampled.
file.is_speakingbool (read-only). Whether per-chunk VAD currently considers speech present. The holdoff is measured in file time (sample positions), not wall-clock time, so processing speed never changes the reported state. Always False when vad=False.
file.vad_scorefloat in [0, 1] (read-only). The most recent per-chunk score, computed on the signal before conditioning. Always 0.0 when vad=False.
duration * sample_rate will come up short. Only the identity case (source rate equal to target rate, denoise off) yields exactly the input sample count.
Every class below except the plain ValueError, TypeError, and ImportError rows is a DecibriError subclass, so a single except decibri.DecibriError catches the typed failures.
| Trigger | Class | Message |
|---|---|---|
| Missing file | FileReadFailed |
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 | AudioFormatUnsupported |
unsupported audio format: followed by the reader's own text, which names the container, four-CC, tag or width it found |
Structurally wrong file: a RIFF/WAVE with a fmt chunk and no data chunk, a corrupt FLAC frame |
AudioFileMalformed |
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 | AudioFileTruncated |
truncated audio file: followed by the reader's own text, which names what was needed against what was available |
sample_rate or input_rate out of range |
SampleRateOutOfRange |
sample rate must be between 1000 and 384000 |
Bad dtype |
InvalidFormat |
dtype must be 'int16' or 'float32'; got 'int32' |
agc out of range |
AgcTargetOutOfRange |
agc must be in [-40, -3]; got -41 |
limiter out of range |
LimiterCeilingOutOfRange |
limiter must be in [-3.0, 0.0]; got -3.1 |
highpass not 80 or 100 |
ValueError |
highpass must be one of: 80, 100; got 60 |
Bad denoise value |
ValueError |
Invalid denoise value: 'rnnoise'. Expected 'fastenhancer-t'. |
vad=True |
ValueError |
vad=True is no longer supported. Specify the mode explicitly: vad='silero' or vad='energy'. |
Bad vad value |
ValueError |
Invalid vad value: 'bogus'. Expected False, 'silero', 'energy', or a Vad config object. |
analyze() without VAD |
VadNotConfigured |
analysis requires VAD; construct the File with a vad configuration |
analyze() with energy VAD |
ValueError |
analyze() requires vad='silero'; energy mode does not support whole-file analysis |
analyze() or save() after iterating |
FileEngaged |
File iteration has begun; construct a new File to analyze the whole recording |
| Second pass | FileConsumed |
File already consumed; construct a new File for another pass |
save() to an extension decibri does not write, or to a path with no extension |
AudioFormatUnsupported |
unsupported audio format: followed by the extension found and the accepted set |
save() could not write the file (missing directory, permission failure, full disk) |
FileWriteFailed |
Failed to write audio file <path>: <os error> |
save() compression out of range |
FlacCompressionOutOfRange |
compression must be in [0, 8]; got 9 |
save() given a bad format value |
ValueError |
Invalid format value: 'ogg'. Expected 'wav', 'aiff', or 'flac'. |
model_path missing |
VadModelLoadFailed |
Silero VAD model not found at <path>. Ensure model_path points to an existing ONNX model file. |
File.buffer given an integer dtype |
ValueError |
samples must have a floating dtype; got int16. Convert with samples.astype(numpy.float32), scaling integer PCM to [-1.0, 1.0]. |
File.buffer given a 2-D array |
ValueError |
samples must be one channel; got an array of shape (100, 2). Select a single channel or mix down to mono before passing it. |
File.buffer given the wrong type |
TypeError |
samples must be a list of floats or a numpy ndarray |
File.buffer without input_rate |
TypeError |
File.buffer() missing 1 required keyword-only argument: 'input_rate' |
as_ndarray=True with NumPy absent |
ImportError |
numpy is not installed. Install with: pip install decibri[numpy] |
Vad(threshold=...) and Vad(holdoff_ms=...) validate inside the Vad constructor, so those errors are raised before File is reached. See Voice activity detection.
as_ndarray=True fails at construction rather than at the first read, so an ordinary try around the constructor is enough; nothing needs to be wrapped around the read loop. The failure is a builtin ImportError, which except Exception catches.
Reach for AsyncMicrophone when capture lives on an event loop: a voice agent that streams chunks to a websocket, a coroutine-based pipeline, or anywhere a sibling task may need to cancel the read in flight. The async classes serialise concurrent calls via a Rust-side Tokio mutex, so sibling-task cancellation is safe in a way that the sync Microphone does not provide.
Parameters match Microphone exactly. See the Microphone section for the full parameter table.
mic = decibri.AsyncMicrophone(sample_rate=16000, vad="silero")
AsyncMicrophone.version() is synchronous (no await) because it returns compile-time constants. Every other method on the class is a coroutine. Double-awaiting version() raises a confusing TypeError; call it without await.
await AsyncMicrophone.open(...)Async factory classmethod. The synchronous constructor blocks for roughly 100 to 500 milliseconds when vad="silero" because it loads the Silero ONNX model inline. open() dispatches that load to loop.run_in_executor(None, ...) so the event loop keeps spinning while ORT initialises.
mic = await decibri.AsyncMicrophone.open(vad="silero")
async with mic:
async for chunk in mic:
await process(chunk)
async with await AsyncMicrophone.open(...) as mic:. The open() factory is itself a coroutine, so it must be awaited before async with takes the resulting instance.
async with and async forThe async context manager opens the stream on entry and stops it on exit. Iteration via async for yields the same data shape as the sync Microphone (bytes by default, numpy.ndarray when as_ndarray=True).
async with await decibri.AsyncMicrophone.open(sample_rate=16000) as mic:
async for chunk in mic:
await websocket.send(chunk)
await mic.start()Open and start the capture stream. Re-entry after stop() or close() is supported and resets VAD state.
await mic.stop()Stop the capture stream and reset VAD state and the sequence counter.
await mic.close()Alias for stop(). See Microphone.close() for the equivalence note.
await mic.read(timeout_ms=None)Read one chunk. Returns the chunk, or None if the stream closed. Same return-type rules as the sync read().
await mic.read_with_metadata(timeout_ms=None)Async parallel of Microphone.read_with_metadata(). Returns a frozen Chunk with metadata, or None on clean close.
mic.aiter_with_metadata()Async-generator function yielding Chunk objects until the stream closes cleanly. Stops when the bridge returns None.
async with await decibri.AsyncMicrophone.open(vad="silero") as mic:
async for chunk in mic.aiter_with_metadata():
if chunk.is_speaking:
await stt.send(chunk.data)
aiter_with_metadata() is an async-generator function. The correct iteration pattern is async for chunk in mic.aiter_with_metadata():; do not await the call itself.
Properties on AsyncMicrophone are synchronous attribute access (no await). They are backed by lock-free atomic mirrors on the Rust bridge, so they report current truth even when the Rust side closes the stream itself (for example, device disconnect).
mic.is_openbool (read-only). True while the capture stream is running.
mic.is_speakingbool (read-only). Same semantics as the sync property: above-threshold detection plus holdoff. Always False when vad=False.
mic.vad_scorefloat in [0, 1]. Same semantics as the sync property.
await AsyncMicrophone.devices()Async parallel of Microphone.devices(). Returns a list of MicrophoneInfo.
AsyncMicrophone.version()Synchronous (see the callout above). Returns a VersionInfo for the Rust core, the audio backend, and the binding wheel.
Cancelling an awaited AsyncMicrophone call (via asyncio.CancelledError, asyncio.wait_for, or explicit task.cancel()) raises CancelledError immediately on the Python side. The Rust-side spawn_blocking thread completes on its own schedule and its result is dropped. The bridge state stays consistent for subsequent reads, so sibling-task cancellation while a read() is in flight is safe.
Asyncio mirror of Speaker. Same parameter set, same lifecycle, all methods coroutines.
Parameters match Speaker exactly. See the Speaker section for the parameter table.
spk = decibri.AsyncSpeaker(sample_rate=24000, channels=1)
await AsyncSpeaker.open(...)Async factory classmethod, symmetric with AsyncMicrophone.open. Speaker does not load ORT, so the event-loop blocking risk is smaller, but the factory is provided for API parity.
AsyncMicrophone, the synchronous AsyncSpeaker(...) constructor does no heavy work, so calling it directly inside an async function is fine. open() is provided for symmetry; reach for it if you prefer the consistent factory pattern across both classes.
async with usageasync with decibri.AsyncSpeaker(sample_rate=24000) as spk:
await spk.write(audio_bytes)
await spk.drain()
await spk.start()Open and start the output stream.
await spk.stop()Stop the output stream.
await spk.close()Alias for stop(). See Microphone.close() for the equivalence note.
await spk.write(samples)Async parallel of Speaker.write. Accepts bytes or a numpy.ndarray with matching dtype.
await spk.drain()Block until all queued samples have been played. Cancelling this await raises CancelledError immediately, but the audio continues to play until the output buffer empties on the callback's own schedule. For production code, complete drains before initiating new writes.
spk.is_playingbool (read-only). Synchronous property backed by a lock-free atomic mirror on the bridge.
await AsyncSpeaker.devices()Async parallel of Speaker.devices().
Asyncio mirror of File. Same parameters, same single-pass lifecycle, same errors; the file read, the conditioning pass, and detection each run in the default ThreadPoolExecutor so the event loop keeps spinning.
AsyncFile(path, **kwargs) takes exactly the parameters File takes, and reads the file inline. Prefer the factory inside a running event loop:
await AsyncFile.open(path, **kwargs): the async factory. Identical result to the bare constructor, with the read dispatched off the event loop.await AsyncFile.buffer(samples, *, input_rate, **kwargs): the async parallel of File.buffer. Unlike the sync class, this one is a coroutine.import asyncio
import decibri
async def main():
file = await decibri.AsyncFile.open("clip.wav", denoise="fastenhancer-t")
async with file:
async for chunk in file:
await handle(chunk)
asyncio.run(main())
await file.read()Read one chunk. Returns the chunk, or None at the end of the source. Same return-type rules as the sync read().
await file.read_with_metadata()Async parallel of File.read_with_metadata(). Returns a frozen Chunk whose timestamp is file time in seconds, or None at the end.
file.aiter_with_metadata()Async-generator function yielding Chunk objects until the end of the source. Iterate it with async for chunk in file.aiter_with_metadata():; do not await the call itself.
await file.analyze() and await file.analyse()Score the whole recording and return a VadReport, with the single pass running off the event loop. The same requirements as the sync method: vad="silero", and a source that iteration has not yet touched. Unlike the sync class, the two spellings are two callables here (AsyncFile.analyse is AsyncFile.analyze is False); they return equal reports.
file = await decibri.AsyncFile.open("clip.wav", vad="silero")
report = await file.analyze()
for segment in report.segments:
print(segment.start, segment.end)
await file.save(path, *, format=None, compression=None)Async parallel of File.save(), with the single conditioning and encode pass running off the event loop. Same signature, same keyword-only arguments, same SaveReport, and the same contract: the container comes from the extension or from format, compression sets the FLAC level, output is 16-bit PCM mono at sample_rate, and the source is consumed. Requires an AsyncFile whose iteration has not begun; after that it raises FileEngaged.
file = await decibri.AsyncFile.open("noisy.wav", denoise="fastenhancer-t")
report = await file.save("clean.wav")
print(report.clipped_samples, report.non_finite_samples)
await file.close()Release the source. Idempotent. AsyncFile is an async context manager, so async with file: closes it on exit.
The four accessors are synchronous attribute access, with the same semantics as the sync class: sample_rate, input_rate, is_speaking, and vad_score.
Convenience entry points exposed directly on the decibri module.
decibri.input_devices()Module-level shortcut for Microphone.devices(). Returns a list of MicrophoneInfo.
for d in decibri.input_devices():
print(d.index, d.name)
decibri.output_devices()Module-level shortcut for Speaker.devices(). Returns a list of SpeakerInfo.
decibri.version()Returns a VersionInfo for the Rust core, the audio backend, and the binding wheel.
v = decibri.version()
print(v.decibri, v.audio_backend, v.binding)
decibri.record_to_file(path, duration_seconds, sample_rate=16000, channels=1, device=None)Synchronous one-shot recorder. Captures duration_seconds of microphone audio to a 16-bit PCM WAV file. Wraps Microphone plus the standard library wave module. Frame-count termination guarantees an accurate duration even on platforms where the buffer hint is ignored by the OS audio subsystem.
decibri.record_to_file("clip.wav", duration_seconds=5.0)
await decibri.async_record_to_file(path, duration_seconds, sample_rate=16000, channels=1, device=None)Async parallel of record_to_file. Same parameters, same semantics; await it from an asyncio context.
await decibri.async_record_to_file("clip.wav", duration_seconds=5.0)
Small typed return shapes used across the API.
ChunkFrozen dataclass returned by read_with_metadata() and iter_with_metadata() on Microphone, AsyncMicrophone, File, and AsyncFile.
| Property | Type | Description |
|---|---|---|
data |
bytes or numpy.ndarray |
Audio chunk. Shape matches the as_ndarray constructor flag. |
timestamp |
float |
On a capture stream, a time.monotonic() snapshot at the chunk boundary, in seconds, useful for relative timing within a session. On a File, the chunk's position in seconds of file time from the start of the recording. |
sequence |
int |
Chunk counter starting at 0. On a capture stream it resets on each new start(); on a File it counts from the start of the source. |
is_speaking |
bool |
VAD state snapshot at the chunk boundary. Always False when VAD is disabled. |
vad_score |
float |
VAD score snapshot in [0, 1]. Always 0.0 when VAD is disabled. |
VadReportFrozen dataclass returned by File.analyze() and await AsyncFile.analyze(). It has exactly two attributes.
| Property | Type | Description |
|---|---|---|
scores |
list[VadWindow] |
Per-window speech scores across the whole recording, in file order. |
segments |
list[Segment] |
Merged speech regions across the whole recording, in file order. |
SaveReportFrozen dataclass returned by File.save() and await AsyncFile.save(). It records what the write did to the samples on their way into the file.
| Property | Type | Description |
|---|---|---|
clipped_samples |
int |
Finite samples that fell outside full scale and were clamped to [-1.0, 1.0]. AGC without a limiter can push conditioned audio past full scale, and 16-bit PCM cannot hold it; rather than clip silently, decibri clamps and counts. |
non_finite_samples |
int |
Non-finite samples replaced before writing: a NaN with silence, an infinity with full scale. The same replacement on every format. |
VadWindowOne scored analysis window, carried in VadReport.scores. Windows tile the recording from zero, contiguous and non-overlapping, 0.032 seconds each; a trailing remainder shorter than one window is left unscored.
| Property | Type | Description |
|---|---|---|
start |
float |
Window start, in seconds of file time. |
end |
float |
Window end, in seconds of file time. |
vad_score |
float |
Speech probability for this window in [0, 1]. |
is_speech |
bool |
Whether vad_score met the configured threshold. The raw per-window test, not the debounced is_speaking. |
SegmentOne merged speech region, carried in VadReport.segments. Consecutive speech windows whose silence gaps fall within the configured holdoff collapse into one segment, and a segment ends at its last speech window rather than at the holdoff expiry.
| Property | Type | Description |
|---|---|---|
start |
float |
Region start, in seconds of file time. |
end |
float |
Region end, in seconds of file time. |
MicrophoneInfoReturned by Microphone.devices(), decibri.input_devices(), and await AsyncMicrophone.devices().
| Property | Type | Description |
|---|---|---|
index |
int |
Device index, usable as the device constructor argument. |
name |
str |
Human-readable device name reported by the operating system. |
id |
str |
Stable platform-specific device identifier. |
max_input_channels |
int |
Maximum number of input channels the device supports. |
default_sample_rate |
int |
The device's native or preferred sample rate in Hz. |
is_default |
bool |
Whether this is the current system default input device. |
SpeakerInfoReturned by Speaker.devices(), decibri.output_devices(), and await AsyncSpeaker.devices().
| Property | Type | Description |
|---|---|---|
index |
int |
Device index, usable as the device constructor argument. |
name |
str |
Human-readable device name reported by the operating system. |
id |
str |
Stable platform-specific device identifier. |
max_output_channels |
int |
Maximum number of output channels the device supports. |
default_sample_rate |
int |
The device's native or preferred sample rate in Hz. |
is_default |
bool |
Whether this is the current system default output device. |
VersionInfoReturned by Microphone.version(), AsyncMicrophone.version(), and decibri.version().
| Property | Type | Description |
|---|---|---|
decibri |
str |
Semver of the underlying Rust core. |
audio_backend |
str |
Audio backend name and version, in the shape "cpal <version>". |
binding |
str |
Semver of the Python binding wheel. |
Decibri ships two VAD modes plus a disabled default. All three are selected with the vad constructor parameter on Microphone and AsyncMicrophone.
| Mode | Description | Default threshold | Threshold range |
|---|---|---|---|
False |
VAD disabled. is_speaking always False, vad_score always 0.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 ONNX model, run through ONNX Runtime. | 0.5 | 0.0 to 1.0 |
onnxruntime system dependency are required for vad="silero".
decibri.Vad objectTo tune the threshold or holdoff, pass a decibri.Vad object as the vad argument instead of a bare mode string. Vad is a frozen dataclass, so vad="silero" is shorthand for vad=decibri.Vad(model="silero") with default tuning.
| Field | Type | Default | Description |
|---|---|---|---|
model |
"silero" | "energy" |
"silero" |
Which detector to run. |
threshold |
float | None |
mode default | Threshold in [0, 1]. None uses the mode default (0.5 for "silero", 0.01 for "energy"). A value outside [0, 1] raises ValueError. |
holdoff_ms |
int |
300 |
Milliseconds of sub-threshold audio before is_speaking flips back to False. A negative value raises ValueError. |
mic = decibri.Microphone(vad=decibri.Vad(model="silero", threshold=0.6, holdoff_ms=200))
is_speaking state machineDecibri runs a pure-Python state machine on top of the raw VAD probability. Above-threshold chunks set the speaking state and cancel any pending silence timer. Below-threshold chunks while already speaking start a silence timer; the timer expires after the Vad object's holdoff_ms (default 300) of elapsed real time, at which point is_speaking flips back to False. Timer expiry is checked on every property access via time.monotonic(), so consumers who pause iteration still observe correct state on the next read.
is_speaking vs vad_scoreis_speaking is the debounced state machine output: above the threshold plus the holdoff grace. vad_score is the raw per-chunk view, identical to the Silero probability in Silero mode and the normalised RMS in energy mode. Use is_speaking for gating downstream work; use vad_score when you need the underlying signal (for example, to threshold differently per chunk or to log probability distributions).
model_pathThe bundled Silero model is the published Silero v5 checkpoint. To use a different Silero ONNX variant, pass an absolute path on the model_path constructor parameter. The path is only consulted when vad="silero"; energy mode and vad=False ignore it.
mic = decibri.Microphone(vad="silero", model_path="/opt/models/silero_vad_v4.onnx")
Echo cancellation removes the sound of your own loudspeaker from the microphone capture. It is opt-in through the aec parameter on Microphone and AsyncMicrophone. 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 Python surface.
aec parameterSignature: aec: str | Aec | None = None.
| Parameter | Type | Default | Description |
|---|---|---|---|
aec |
str | Aec | None |
None (off) |
Echo cancellation. None disables it; "tau" selects the model with its defaults; pass a decibri.Aec(...) object to tune it. |
from decibri import Aec, Microphone
# Short form: the model with its defaults.
a = Microphone(sample_rate=16000, aec="tau")
# Object form: the same model, every option set to its default.
b = Microphone(
sample_rate=16000,
aec=Aec(
model="tau",
tail_ms=200,
suppression="conservative",
reference_sample_rate=16000,
),
)
Echo cancellation requires sample_rate between 8000 and 48000, narrower than the range sample_rate 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. See the Browser API.
decibri.AecFrozen dataclass carrying the tuning. Assigning to a field after construction raises dataclasses.FrozenInstanceError. Build a new instance instead.
@dataclass(frozen=True, slots=True)
class Aec:
model: str = "tau"
tail_ms: int | None = None # 16 to 500, None takes 200
suppression: str | None = None # "conservative" or "off", None takes "conservative"
reference_sample_rate: int | None = None # 1000 to 384000, None takes the capture rate
| Field | Type | Default | Description |
|---|---|---|---|
model |
str |
"tau" |
The model to run. 'tau' is the model available today; it carries no model file and needs no download. An unrecognised name raises from the Microphone constructor. |
tail_ms |
int | None |
None takes 200 |
How much echo delay spread the canceller can account for, in milliseconds. 16 to 500. |
suppression |
str | None |
None takes "conservative" |
"conservative" attenuates the residue the linear stage leaves behind. "off" delivers the linear output as it stands. |
reference_sample_rate |
int | None |
None takes 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. |
mic.push_aec_reference(samples)Signature push_aec_reference(samples: SampleData) -> None, identical on Microphone and AsyncMicrophone. 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 reference_dropped. A push while capture is not running, or with the option unset, does nothing. A bad input type raises regardless of capture state.
mic.aec_metrics()Signature aec_metrics() -> AecMetrics | None. Returns None 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 None.
AecMetricsFrozen dataclass returned by aec_metrics().
@dataclass(frozen=True, slots=True)
class AecMetrics:
delay_samples: int | None
erle_db: float
double_talk: bool
reference_starved: int
acquisition_parked: int
reference_reanchors: int
reference_dropped: int
reference_silence: int
| Field | Type | Meaning |
|---|---|---|
delay_samples |
int | None |
Active alignment between reference and capture. None while searching. |
erle_db |
float |
Smoothed estimate of how much echo is being removed, in dB. |
double_talk |
bool |
Whether the near-end talker is believed active. Adaptation is held while true. |
reference_starved |
int |
Near-end samples with no far-end sample available while aligned. |
acquisition_parked |
int |
Near-end samples processed while no alignment was active. |
reference_reanchors |
int |
Times the alignment was rebuilt after a capture discontinuity. |
reference_dropped |
int |
Far-end samples discarded because a push exceeded the queue, at the reference rate. |
reference_silence |
int |
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.
On AsyncMicrophone, the two members do not match, and the difference is deliberate.
push_aec_reference is a plain method. You call it, you do not await it. The push never blocks, so making it a coroutine would add an await to a call that has nothing to wait for, and would stop you calling it from a synchronous render callback. It has the same signature on Microphone and AsyncMicrophone.
aec_metrics is a coroutine and must be awaited. Reading the metrics serialises against block processing on the capture chain, so it can wait; awaiting keeps that wait off the event loop.
mic.push_aec_reference(block) # no await
metrics = await mic.aec_metrics() # await
| Class | Signature |
|---|---|
Microphone |
push_aec_reference(samples: SampleData) -> None |
Microphone |
aec_metrics() -> AecMetrics | None |
AsyncMicrophone |
push_aec_reference(samples: SampleData) -> None |
AsyncMicrophone |
async aec_metrics() -> AecMetrics | None |
import asyncio
from decibri import AsyncFile, AsyncMicrophone, AsyncSpeaker
RATE = 16000
async def main():
async with AsyncFile("speech.wav", sample_rate=RATE) as f:
audio = b"".join([chunk async for chunk in f])
block_bytes = RATE // 10 * 2
async with (
AsyncMicrophone(sample_rate=RATE, aec="tau") as mic,
AsyncSpeaker(sample_rate=RATE) as speaker,
):
for i in range(0, len(audio), block_bytes):
block = audio[i : i + block_bytes]
await speaker.write(block)
mic.push_aec_reference(block) # not awaited
near = await mic.read(timeout_ms=100)
metrics = await mic.aec_metrics() # awaited
asyncio.run(main())
Python has typed exceptions for its own failure modes and uses them. Catch them in the idiom of the language you are in, not in a shape shared across both bindings; the Node.js API raises different classes on purpose.
| Condition | Class | Message |
|---|---|---|
aec is neither a string nor an Aec |
ValueError |
Invalid aec value: <value>. Expected None, a model name such as 'tau', or an Aec config object. |
model is not a string |
ValueError |
Invalid aec model: <value>. Expected a model name such as 'tau'. |
model names no known model |
AecConfigInvalid |
echo canceller configuration error: model must be one of: 'tau'; got '<value>' |
tail_ms is out of range |
AecConfigInvalid |
tail_ms must be in [16, 500]; got <value> |
suppression is not one of the two values |
ValueError |
Invalid aec suppression: <value>. Expected 'conservative' or 'off'. |
reference_sample_rate is out of range |
SampleRateOutOfRange |
reference_sample_rate must be in [1000, 384000]; got <value> |
sample_rate outside 8000 to 48000 with aec set |
AecSampleRateUnsupported |
echo cancellation only supports sample rates 8000 to 48000 |
push_aec_reference given a wrong type |
TypeError |
samples must be bytes or numpy.ndarray with dtype matching the configured format (int16 or float32); the reference is mono |
push_aec_reference given an array of the wrong dtype |
TypeError |
format='<configured>' configured but 1-D ndarray dtype is <actual>; convert with arr.astype(np.<configured>) or construct Microphone with format='<actual>' |
Where the check happens differs. Aec(...) validates tail_ms, suppression and reference_sample_rate at construction, before any microphone exists, so those raise from the config object. The model name is not checked there; an unknown name raises from the Microphone constructor, as does the capture rate constraint.
AecConfigInvalid, AecSampleRateUnsupported and SampleRateOutOfRange all subclass DecibriError, so except DecibriError catches all three. They are importable from decibri and from decibri.exceptions.
from decibri import Aec, Microphone
from decibri.exceptions import AecConfigInvalid, AecSampleRateUnsupported
try:
mic = Microphone(sample_rate=64000, aec="tau")
except AecSampleRateUnsupported:
mic = Microphone(sample_rate=48000, aec="tau")
Decibri supports two on-the-wire sample formats, selected with the dtype constructor parameter. Both apply to Microphone, Speaker, and their async variants.
int16 (default)Each two bytes represents one 16-bit signed integer sample, little-endian. Range: -32,768 to 32,767. Two bytes per sample. This is the format expected by most cloud STT providers and the wire format used by the record_to_file helpers.
with decibri.Microphone(dtype="int16") as mic:
chunk = mic.read() # bytes; len(chunk) == frames * channels * 2
float32Each four bytes represents one 32-bit IEEE 754 float sample, little-endian. Range: approximately -1.0 to 1.0. Four bytes per sample. Use this when your downstream pipeline expects normalised floats and you would otherwise convert from int16.
with decibri.Microphone(dtype="float32") as mic:
chunk = mic.read() # bytes; len(chunk) == frames * channels * 4
frames_per_buffer hint and delivers chunks sized to the OS device period instead. Frame-count loops still observe accurate total duration (see record_to_file); chunk-count loops can record more or less than requested. Prefer frame-count termination when an exact duration matters.
Set as_ndarray=True on the Microphone constructor to receive numpy.ndarray instead of bytes from read(). The array's dtype matches the configured dtype (np.int16 or np.float32); the shape is 1-D (N,), since capture is mono only.
import decibri
import numpy as np
with decibri.Microphone(sample_rate=16000, dtype="float32", as_ndarray=True) as mic:
chunk = mic.read()
assert isinstance(chunk, np.ndarray)
assert chunk.dtype == np.float32
Speaker.write() duck-types on each call: pass bytes or pass an ndarray with matching dtype. Mixing both within a single output session is supported.
The NumPy extra is opt-in to keep the default install lightweight:
as_ndarray=True requires the NumPy extra. Reading from a Microphone constructed with as_ndarray=True on an install without the extra raises ImportError with the message numpy is not installed. Install with: pip install decibri[numpy].
py.typedThe decibri wheel ships a py.typed marker file per PEP 561, with hand-written .pyi stubs covering the internal Rust extension module and the full exception hierarchy. The package is mypy strict-clean. IDEs and type checkers will autocomplete every public name and narrow return types correctly.
import decibri
import numpy as np
mic = decibri.Microphone(as_ndarray=True)
chunk = mic.read()
# Type checker narrows `chunk` to numpy.ndarray | None when as_ndarray=True
# and to bytes | None otherwise.
The package depends on typing-extensions at runtime to support the 3.10 abi3 floor (typing.Self is only available in 3.11 and newer).
Decibri raises typed exceptions instead of generic RuntimeError or Exception. The root of the hierarchy is DecibriError. Three intermediate parents (DeviceError, OrtError, OrtPathError) group related instance classes so callers can catch by category instead of by individual class. Every exception remains catchable as DecibriError.
| Exception class | Parent | Common cause |
|---|---|---|
SampleRateOutOfRange |
DecibriError |
Constructor sample_rate outside the supported range. |
ChannelsOutOfRange |
DecibriError |
Constructor channels outside the supported range. |
MultichannelNotSupported |
DecibriError |
Microphone capture is mono only; a channels value greater than 1 was requested. |
FramesPerBufferOutOfRange |
DecibriError |
Constructor frames_per_buffer outside the supported range. |
InvalidFormat |
DecibriError |
Constructor dtype not "int16" or "float32". |
AgcTargetOutOfRange |
DecibriError |
An AGC target level outside the valid range -40 to -3 dBFS. |
LimiterCeilingOutOfRange |
DecibriError |
A limiter ceiling outside the valid range -3.0 to 0.0 dBFS. |
AlreadyRunning |
DecibriError |
start() called on an instance that is already capturing. |
FileReadFailed |
DecibriError |
A File source could not be read from disk (missing path, a directory, a permission failure). |
AudioFormatUnsupported |
DecibriError |
An offline audio file is in a format decibri cannot decode: an unrecognised container, a codec or sample width the reader does not carry, or a channel layout it cannot decode. Also raised by File.save() for an extension decibri does not write. |
AudioFileMalformed |
DecibriError |
An offline audio file is structurally wrong: the container parsed, then the bytes were not what the format requires. |
AudioFileTruncated |
DecibriError |
An offline audio file ends before the audio it declares, including one whose declared payload length is not a whole number of frames. |
FileWriteFailed |
DecibriError |
A File.save() destination could not be written (missing directory, permission failure, full disk). The audio was encoded before the failure. |
FlacCompressionOutOfRange |
DecibriError |
A File.save() compression level outside 0 to 8. |
VadNotConfigured |
DecibriError |
File.analyze() called on a source built without a vad configuration. |
FileEngaged |
DecibriError |
File.analyze() called after iteration has begun. Also raised by File.save() under the same rule. See File. |
FileConsumed |
DecibriError |
A second pass was attempted over a File that has already been read. |
StreamOpenFailed |
DecibriError |
The audio stream failed to open. |
StreamStartFailed |
DecibriError |
The audio stream opened but failed to start. |
PermissionDenied |
DecibriError |
The operating system denied microphone access. Message includes platform-specific guidance. |
MicrophoneStreamClosed |
DecibriError |
Read attempted on a closed capture stream (often a mid-stream device disconnect). |
SpeakerStreamClosed |
DecibriError |
Write attempted on a closed output stream. |
DeviceFailed |
DecibriError |
A running microphone or speaker stream failed at the device or driver level (device unplugged, driver reset, exclusive-mode preemption). |
VadSampleRateUnsupported |
DecibriError |
VAD enabled with a sample rate the VAD model cannot accept. |
VadThresholdOutOfRange |
DecibriError |
A VAD threshold outside the valid range [0, 1]. |
AecSampleRateUnsupported |
DecibriError |
Echo cancellation enabled with a sample rate outside 8000 to 48000 Hz. See AEC errors. |
AecConfigInvalid |
DecibriError |
The echo canceller rejected its configuration, carrying the canceller's own message. See AEC errors. |
ResampleConfigInvalid |
DecibriError |
A sample rate conversion the resampler does not support. Defensive: every rate pair the configuration validator accepts is inside the resampler's range, so no Python call reaches it. |
ResampleAfterFlush |
DecibriError |
Audio fed to a resample chain that has already been flushed. Defensive: the capture and File paths stop feeding a chain once it is flushed, so no Python call reaches it. |
ResampleFailed |
DecibriError |
The resampler reported an error decibri does not recognise, forwarding the resampler's own text. Defensive: every error the pinned resampler release defines maps to its own class, so no Python call reaches it. |
ForkAfterOrtInit |
DecibriError |
Linux only. The current process inherited an ORT session from its parent across fork(). See Multiprocessing and asyncio caveats. |
OnnxBackendFailed |
DecibriError |
An ONNX inference backend reported an error. A catch-all for ONNX backend failures that are not the specific ORT failures grouped under OrtError. |
MicrophoneNotFound |
DeviceError |
The named input device does not match any device on the system. |
SpeakerNotFound |
DeviceError |
The named output device does not match any device on the system. |
MultipleDevicesMatch |
DeviceError |
The device name substring matches more than one device; use a more specific substring or the integer index. |
DeviceIndexOutOfRange |
DeviceError |
The integer device index is out of range for the host audio API. |
NoMicrophoneFound |
DeviceError |
The system reports zero input devices. |
NoSpeakerFound |
DeviceError |
The system reports zero output devices. |
NotAnInputDevice |
DeviceError |
The matched device exists but is not capable of input. |
DeviceEnumerationFailed |
DeviceError |
The audio backend failed to enumerate devices. |
OrtInitFailed |
OrtError |
ONNX Runtime initialisation itself failed (no specific path was supplied). |
OrtSessionBuildFailed |
OrtError |
Building an ORT inference session failed. |
OrtThreadsConfigFailed |
OrtError |
Configuring ORT thread pools failed. |
VadModelLoadFailed |
OrtError |
Loading the Silero VAD ONNX model failed. Has a .path attribute. |
ModelLoadFailed |
OrtError |
Loading a bundled capture model (the denoise stage) failed. Has a .path attribute. |
OrtInferenceFailed |
OrtError |
ORT inference produced an error at runtime. |
OrtTensorCreateFailed |
OrtError |
Creating an ORT input tensor failed. |
OrtTensorExtractFailed |
OrtError |
Extracting values from an ORT output tensor failed. |
OrtLoadFailed |
OrtPathError |
The supplied ORT dylib path passed the filesystem pre-check but ORT rejected it. Has a .path attribute. |
OrtPathInvalid |
OrtPathError |
The supplied ORT dylib path failed the pre-check before ORT saw it. Has .path and .reason attributes. |
Four classes carry additional attributes beyond the standard exception message:
VadModelLoadFailed.path: the model path that failed to load.ModelLoadFailed.path: the capture model path (the denoise stage) that failed to load.OrtLoadFailed.path: the ORT dylib path that failed to load.OrtPathInvalid.path, OrtPathInvalid.reason: the rejected path and a short reason string.Catch any decibri error:
try:
with decibri.Microphone(sample_rate=16000) as mic:
chunk = mic.read()
except decibri.DecibriError as e:
print(f"Decibri error: {e}")
Catch device-selection failures specifically, then fall back to the system default:
try:
mic = decibri.Microphone(device="USB Audio")
except decibri.DeviceError as e:
print(f"Device problem: {e}")
mic = decibri.Microphone()
Python's default fork start method on Linux duplicates the parent's memory into the child, but ONNX Runtime's internal state is not safe to share across forked processes. A Silero-enabled Microphone initialised in the parent and then used in a forked child either produces incorrect inference results or segfaults; decibri detects the PID mismatch at the start of every Silero inference call and raises ForkAfterOrtInit instead.
The fix is to either set the spawn start method before constructing any worker, or to construct the Microphone inside each child process after the fork.
import multiprocessing
if __name__ == "__main__":
multiprocessing.set_start_method("spawn")
# ... rest of program
vad="silero" with multiprocessing, call multiprocessing.set_start_method("spawn") before spawning workers. The default fork start method shares ORT state across processes unsafely; decibri detects the mismatch and raises ForkAfterOrtInit. macOS already defaults to spawn; Windows always uses spawn. The setting only matters on Linux.
The synchronous AsyncMicrophone(...) constructor blocks for roughly 100 to 500 milliseconds when vad="silero" because it loads the Silero ONNX model inline. In an async context this blocks the event loop for the duration of the load, which can cause dropped websocket frames, late timer callbacks, and UI jitter in voice agents. Use await AsyncMicrophone.open(...) instead; the factory dispatches the synchronous construction to loop.run_in_executor(None, ...) so the event loop keeps running.
For more multiprocessing recipes, see the Python multiprocessing guide in the repository.
When vad="silero" is requested, decibri needs to load the ONNX Runtime dynamic library. The path is resolved in this order, first match wins:
ort_library_path constructor argument, if supplied.DECIBRI_ORT_DYLIB_PATH environment variable, for per-deployment overrides without code changes.ORT_DYLIB_PATH environment variable, the upstream ort crate's standard convention, respected so existing bare-ort deployments keep working.decibri/_ort/. This is the default pip install decibri experience.If none of the above resolve to a real file, ORT's default loader runs. That loader itself respects ORT_DYLIB_PATH if set after decibri import, so a late environment change still works as a last-resort fallback.
Only when vad="silero". The other VAD modes ("energy", False) never touch ORT, so the resolver and bundled-dylib lookup are skipped entirely.
The first Microphone constructed with vad="silero" incurs roughly 100 to 500 milliseconds of cold load on most platforms. The cost is amortised across the rest of the process: subsequent Microphones (sync or async) reuse the same loaded ORT.
The first Microphone that loads Silero determines the dylib for the whole process. Subsequent Microphone constructions inherit that initialisation regardless of their own ort_library_path argument. To switch dylibs, restart the process.