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 |
| 3.11, 3.12, 3.13, 3.14 | Windows ARM64 |
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. The Windows ARM64 wheel carries the same abi3 tag, but the oldest Python it supports is 3.11, the first CPython release published for that platform.
cpython-3.12-windows-aarch64-none.
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,
channel_map=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 |
The number of channels each chunk delivers, interleaved frame by frame. At least 1, with no fixed maximum; without a channel_map, the device's own channel count is the ceiling. 1 is Decibri's own average of every device channel, where earlier releases let the platform collapse the channels first, so a capture from a stereo or multichannel device is not the same signal it was. 0 raises ChannelsOutOfRange. See Channels for the counts that work without a channel_map and the ones that are refused. |
channel_map |
list[int] | None |
None |
0-based device channel indices, one per delivered channel: delivered channel j carries device channel channel_map[j]. The length must equal channels. Entries may repeat and their order is significant. None takes the derivation channels describes. See Channels. |
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, the holdoff, and the delivered channel the detector reads. 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: 1-D at one channel, shaped (frames, channels) above one. Requires pip install decibri[numpy]. |
ort_library_path |
str | Path | None |
resolver | Override path to the ONNX Runtime dynamic library. Consulted when a stage that runs on ONNX Runtime is enabled: vad="silero" or denoise. 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. Above one delivered channel, DC removal, denoise and the high-pass run on each channel with its own state, while AGC and the limiter are linked: one detector measures across all the channels and one gain is applied to all of them, which keeps the balance between the channels.
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.
channels is a delivered count: the number of channels in every chunk you read, interleaved frame by frame, so a chunk holds frames_per_buffer * channels samples. It works the way sample_rate does. The device itself is always opened at its own native channel count, the figure MicrophoneInfo.max_input_channels reports, and Decibri derives the delivered channels from what the device gives it.
Without a channel_map, two counts can be derived:
channels=1, the default, delivers the average of every device channel.Any other count above 1 without a map is refused by start(), because the device's channel count is only read when the stream opens. A count above the device's own raises MicrophoneChannelsUnsupported, because Decibri cannot deliver a channel the device does not have. A count above 1 but below the device's own raises ChannelSelectionAmbiguous, because it does not say which of the device's channels it means. A channel_map lifts both refusals, because it names every delivered channel.
channel_map names device channels. It is a list of 0-based device channel indices with one entry per delivered channel, so its length must equal channels: delivered channel j carries device channel channel_map[j]. Entries may repeat and their order is significant, so a map can select, reorder and duplicate device channels, and it can deliver more channels than the device has. channel_map=[1] with channels=1 delivers the device's second channel alone, and channel_map=[1, 0] with channels=2 swaps a stereo pair. Every entry must name a channel the device has.
Vad(source=...) names a delivered channel. It is a position in the chunk you read, counted after channel_map has been applied, and never a device index. None, the default, feeds the voice activity detector the frame average of every delivered channel; an integer feeds it that one delivered channel. It must be below channels, and it changes only what the detector reads, not the audio you receive.
channel_map counts the device's channels; source counts the channels of the chunk you read. With channel_map=[2, 0], delivered channel 0 is device channel 2, so source=0 runs the detector on device channel 2, and source=2 is refused, because only two channels are delivered.
import decibri
# Needs a device with at least three input channels.
# Deliver device channels 2 and 0, in that order. source=0 names
# delivered channel 0, which carries device channel 2.
with decibri.Microphone(
channels=2,
channel_map=[2, 0],
vad=decibri.Vad(model="silero", source=0),
) as mic:
for chunk in mic:
process(chunk) # interleaved frames: device channel 2, then device channel 0
The constructor checks everything that does not need the device. start() checks the rest against the resolved device, and a with block calls start() on entry, so the three device refusals surface there. AsyncMicrophone raises the same errors, from its constructor and from await mic.start().
| Condition | Class | Message |
|---|---|---|
channels below 1 (constructor) |
ChannelsOutOfRange |
channels must be at least 1 |
A channel_map entry that is not an integer, bool included (constructor) |
TypeError |
channel_map entries must be integers; got <value> |
A channel_map entry outside 0 to 65535 (constructor) |
ValueError |
channel_map entries must be in [0, 65535]; got <value> |
channel_map length differs from channels (constructor) |
ChannelMapLengthMismatch |
the channel map has <entries> entries; it must have exactly one entry per delivered channel (<channels>) |
Vad(source=...) not below channels (constructor) |
DetectorSourceOutOfRange |
the detector source names delivered channel <index>; the delivered channel count is <channels> |
No channel_map, and channels above the device's own count (start()) |
MicrophoneChannelsUnsupported |
the input device does not support <requested> delivered channels; it reports <available> |
No channel_map, and channels above 1 but below the device's own count (start()) |
ChannelSelectionAmbiguous |
a channel map is required to deliver <requested> of the device's <available> input channels |
A channel_map entry the device does not have (start()) |
ChannelMapOutOfRange |
the channel map names device channel <index>; the device reports <available> input channels |
The six named classes are DecibriError subclasses; TypeError and ValueError are Python built-ins. The type and range of source itself are checked when the Vad is built; see Voice activity detection. There is no MultichannelNotSupported class: nothing raises it, and importing it fails.
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. This is where channels and channel_map are checked against the resolved device; see Channels.
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. Above one channel the chunk holds interleaved frames: bytes carry the channels frame by frame, and an ndarray is shaped (frames, channels). 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. Above one delivered channel both modes score the frame average of the delivered channels, or the one channel Vad(source=...) names. Always 0.0 when vad=False.
mic.overrun_countint (read-only). Number of capture buffers dropped because the consumer could not keep pace. 0 while the consumer keeps up, before capture starts, and after stop(), which releases the stream the counter lives on, so read it before stopping to see a session's total. A rising value means audio is being dropped to bound memory. The sync Microphone carries it; AsyncMicrophone does not.
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. At least 1, with no fixed maximum: the output device decides. Multi-channel samples are interleaved on the wire. A count above the figure the device reports that the device cannot serve raises SpeakerChannelsUnsupported from start(), naming that figure; any other failed open stays StreamOpenFailed. 0 raises ChannelsOutOfRange at construction. |
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.
spk.underrun_countint (read-only). Number of samples played as silence because the playback queue ran dry. 0 while the producer keeps the queue fed, or before playback starts. A rising value means the output is filling gaps with silence. The sync Speaker carries it; AsyncSpeaker does not.
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,
channels=1,
channel_map=None,
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. The file's channel count comes from its header. |
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. |
channels |
int |
1 |
The number of channels each chunk delivers, interleaved frame by frame. At least 1. Without a channel_map, 1 delivers the average of every source channel and a count equal to the source's own delivers every channel in source order. Any other unmapped count above 1 is refused at construction: above the source's own raises FileChannelsUnsupported, and above 1 but below it raises FileChannelSelectionAmbiguous. |
channel_map |
list[int] | None |
None |
0-based source channel indices, one per delivered channel, with the same rules as the Microphone parameter: the length must equal channels, and entries may repeat and their order is significant. An entry the source does not have raises FileChannelMapOutOfRange at construction. |
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, including Vad(source=...), which names a delivered channel here too. 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.
channels, channel_map and Vad(source=...) follow the Microphone rules in Channels, with the source's own channel count standing where the device's stands: the file's header for a path, or input_channels for File.buffer. That count is known as soon as the source is opened, so every channel refusal is raised by the constructor, with its own class.
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 frames at the target rate, each frame carrying one sample per delivered channel: at the default one channel that is 3,200 bytes as int16 and 6,400 bytes as float32, and it grows with channels. 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, input_channels=1, **kwargs)Wraps in-memory samples instead of reading a file. samples is a list of floats or a numpy.ndarray with a floating dtype, in the range -1.0 to 1.0, frame-interleaved at input_channels. Raw samples carry no header, so input_rate (their native rate, 1,000 to 384,000) is a mandatory keyword-only argument, and input_channels (default 1, mono) is its channel counterpart. At one input channel an ndarray must hold one channel: a redundant axis, (N, 1) or (1, N), is accepted, and an array with two axes longer than 1 raises ValueError. Above one input channel an ndarray is either 1-D and interleaved or 2-D (frames, input_channels), and the sample count must be a whole number of frames, or BlockSizeNotFrameAligned is raised. Every other keyword matches the constructor, so channels and channel_map choose what is delivered from those input channels.
file = decibri.File.buffer(samples, input_rate=48000, sample_rate=16000)
# pairs: a float32 ndarray shaped (frames, 2). Deliver both channels.
stereo = decibri.File.buffer(pairs, input_rate=48000, input_channels=2, channels=2)
This is also the route for any audio the reader rejects, an MP3 or an Opus file for example: decode it yourself, convert to floats in -1.0 to 1.0, and pass the source rate as input_rate and its channel count as input_channels. 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. Above one delivered channel it scores the frame average of the delivered channels, or the one channel Vad(source=...) names. 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 at sample_rate, in every one of the three containers, at the delivered channel count and interleaved: the same layout iteration delivers, so one channel unless channels asks for more. Each container's own channel ceiling applies, and a save above it raises AudioFormatUnsupported carrying the container layer's own text: FLAC carries at most 8 channels, and a 16-bit WAV at most 32767. Decibri adds no ceiling of its own. 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' |
channels below 1, or File.buffer's input_channels below 1 |
ChannelsOutOfRange |
channels must be at least 1 |
A channel_map entry that is not an integer, bool included |
TypeError |
channel_map entries must be integers; got <value> |
A channel_map entry outside 0 to 65535 |
ValueError |
channel_map entries must be in [0, 65535]; got <value> |
channel_map length differs from channels |
ChannelMapLengthMismatch |
the channel map has <entries> entries; it must have exactly one entry per delivered channel (<channels>) |
No channel_map, and channels above the source's own count |
FileChannelsUnsupported |
the file does not have <requested> channels to deliver; it has <available> |
No channel_map, and channels above 1 but below the source's own count |
FileChannelSelectionAmbiguous |
delivering <requested> of the file's <available> channels requires a channel map |
A channel_map entry the source does not have |
FileChannelMapOutOfRange |
the file channel map names channel <index>; the file has <available> channels |
Vad(source=...) not below channels |
DetectorSourceOutOfRange |
the detector source names delivered channel <index>; the delivered channel count is <channels> |
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() with more channels than the container carries |
AudioFormatUnsupported |
unsupported audio format: followed by the container layer's own text |
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 at one input channel given an array with two axes longer than 1 |
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 above one input channel given an array that is neither 1-D nor (frames, input_channels) |
ValueError |
samples must be 1-D interleaved or 2-D (frames, input_channels); got shape <shape> with input_channels=<n> |
File.buffer given a sample count that is not a whole number of frames at input_channels |
BlockSizeNotFrameAligned |
the requested block size of <samples> samples is not a whole number of <channels>-channel frames |
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=...), Vad(holdoff_ms=...), and the type and range of Vad(source=...) validate inside the Vad constructor, so those errors are raised before File is reached. Only the check of source against channels belongs to File, as DetectorSourceOutOfRange. 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 at sample_rate and at the delivered channel count, each container's own channel ceiling applies, 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.
channels=1 writes the average of every device channel, and a count equal to the device's own writes every device channel, interleaved. The helper takes no channel_map, so a strict subset of the device's channels needs a Microphone with a channel_map; any other count above 1 is refused exactly as Microphone.start() refuses it. See Channels.
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, interleaved at the delivered channel count. Shape matches the as_ndarray constructor flag: an ndarray is 1-D at one channel and (frames, channels) above one. |
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 the save itself replaced: a NaN with silence, an infinity with full scale, the same on every format. That replacement and this count cover only the direct path, a one-channel source already at the target rate with no channel map and no conditioning. In every other case the conditioning chain runs and has already replaced each non-finite sample with silence at its entry, so the file carries silence there and the count does not include it. |
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: the count the device opens at, which channels and channel_map are checked against at start(). |
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 |
Number of output channels the device reports. A larger Speaker count is still offered to the device, which may serve it; one it cannot serve raises SpeakerChannelsUnsupported, naming this figure. |
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 |
Both detectors read one mono signal. Above one delivered channel that signal is the frame average of every delivered channel, unless Vad(source=...) names one delivered channel for the detector to read instead. vad_score and is_speaking describe that signal; the audio you receive is not changed by it.
onnxruntime system dependency are required for vad="silero".
decibri.Vad objectTo tune the threshold or holdoff, or to choose the delivered channel the detector reads, 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. |
source |
int | None |
None |
The delivered channel the detector reads: a position in the chunk, counted after any channel_map, never a device index. None reads the frame average of every delivered channel. A value that is not an integer, bool included, raises TypeError (source must be an integer; got <value>), and one outside 0 to 65535 raises ValueError (source must be in [0, 65535]; got <value>), both from Vad(...). A value not below channels raises DetectorSourceOutOfRange from the Microphone or File constructor. See Channels. |
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 v6.2 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,
reference_channels=1,
),
)
Echo cancellation requires sample_rate between 8000 and 48000, narrower than the range sample_rate otherwise accepts. It runs on every delivered channel: one canceller per delivered channel, each fed the same pushed reference and each finding its own channel's echo delay, so its processing cost grows with channels. It is available on native capture only and is not a File parameter. The browser build does not carry 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
reference_channels: int = 1 # at least 1, the interleave of what you push
| 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. |
reference_channels |
int |
1 |
The channel count of the audio you push, interleaved frame by frame. At least 1; below 1 raises AecConfigInvalid. Above 1, Decibri averages each frame to one mono sample before the canceller reads it. Set it whenever you push more than one channel. |
The declared reference_channels must match what you push. A mismatch is not detected and raises no error: the frames are misread, nothing is cancelled, and aec_metrics().delay_samples stays None. The canceller reads one mono reference, so with playback through more than one loudspeaker whose echo paths differ, some echo remains that adaptation does not remove.
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, interleaved at reference_channels (mono by default). bytes and 1-D or 2-D ndarrays are accepted, the input shapes Speaker.write accepts. One push serves every delivered channel.
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 and is not counted in reference_dropped. 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
channels: tuple[AecChannelMetrics, ...]
| 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. |
channels |
tuple[AecChannelMetrics, ...] |
One entry per delivered channel, in delivered order. See AecChannelMetrics below. |
The first six fields report the canceller on the first delivered channel. reference_dropped and reference_silence describe the one reference queue that every channel's canceller shares. On a one-channel capture, channels holds a single entry that agrees with the top-level fields.
The diagnostics section of the feature page reads these fields as a procedure rather than a field list.
AecChannelMetricsFrozen dataclass, one entry of AecMetrics.channels: the report of the canceller on one delivered channel. It carries the six canceller fields and not the two queue counters, which stay on AecMetrics. Importable from decibri.
@dataclass(frozen=True, slots=True)
class AecChannelMetrics:
delay_samples: int | None
erle_db: float
double_talk: bool
reference_starved: int
acquisition_parked: int
reference_reanchors: int
| Field | Type | Meaning |
|---|---|---|
delay_samples |
int | None |
This channel's active alignment between reference and capture. None while its estimator is still searching. |
erle_db |
float |
Smoothed estimate of how much echo this channel's canceller is removing, in dB. |
double_talk |
bool |
Whether this channel's canceller believes the near-end talker is active. Its adaptation is held while true. |
reference_starved |
int |
Near-end samples on this channel with no far-end sample available while aligned. |
acquisition_parked |
int |
Near-end samples on this channel processed while no alignment was active. |
reference_reanchors |
int |
Times this channel's alignment was rebuilt after a capture discontinuity. |
Each channel's canceller finds its own echo delay, so the entries differ where the channels' acoustic paths differ. erle_db is not a ranking across channels: it rises with echo distance, because a weaker echo is easier to reduce in ratio terms, so a far microphone can report a higher figure than a near one while removing less echo in absolute terms. Compare a channel with its own history, not with its neighbours.
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> |
reference_channels is below 1 |
AecConfigInvalid |
reference_channels must be at least 1; 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 or 2-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, reference_sample_rate and reference_channels 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). Its shape follows channels: 1-D (frames,) at one channel, and 2-D (frames, channels) above one, one row per frame.
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 below 1, or File.buffer's input_channels below 1. |
ChannelMapLengthMismatch |
DecibriError |
A channel_map whose length differs from channels. Raised at construction. See Channels. |
ChannelMapOutOfRange |
DecibriError |
A Microphone channel_map entry names a device channel the device does not have. Raised from start(). |
MicrophoneChannelsUnsupported |
DecibriError |
A Microphone channels count, with no channel_map, above the device's own. Raised from start(). |
ChannelSelectionAmbiguous |
DecibriError |
A Microphone channels count, with no channel_map, above 1 but below the device's own. Raised from start(). |
DetectorSourceOutOfRange |
DecibriError |
A Vad(source=...) that is not below the delivered channel count. Raised by the Microphone or File constructor. |
FileChannelsUnsupported |
DecibriError |
A File channels count, with no channel_map, above the source's own. Raised at construction. |
FileChannelSelectionAmbiguous |
DecibriError |
A File channels count, with no channel_map, above 1 but below the source's own. Raised at construction. |
FileChannelMapOutOfRange |
DecibriError |
A File channel_map entry names a channel the source does not have. Raised at construction. |
BlockSizeNotFrameAligned |
DecibriError |
File.buffer samples that are not a whole number of frames at input_channels. |
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, and for a save with more channels than the container carries. |
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. |
SpeakerChannelsUnsupported |
DecibriError |
An output device cannot serve the requested channels, a count above the figure it reports. Raised from Speaker.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.
Two stages run on ONNX Runtime: the Silero detector, vad="silero", and the denoise conditioning stage. When either is enabled, 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.
When vad="silero" or denoise is set, on Microphone, AsyncMicrophone, File and AsyncFile alike. Energy mode and vad=False, with denoise unset, never touch ORT, so the resolver and bundled-dylib lookup are skipped entirely. On a Microphone, the Silero model loads in the constructor and the denoise model when capture starts. On a File, the denoise model loads in the constructor and the Silero model at the first read, or when analyze() runs.
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: later loads, by either stage and from any class, reuse the same loaded ORT.
The first load of ORT in a process, by either stage and from any class, determines the dylib for the whole process. Later loads inherit that initialisation regardless of their own ort_library_path argument. To switch dylibs, restart the process.
ONNX Runtime carries its own telemetry, separate from anything Decibri does. Decibri disables it when it initialises the runtime. Set DECIBRI_ORT_TELEMETRY=1 in the environment before the first load to leave it enabled; any other value, an empty value, and an absent variable leave it disabled.
Two limits apply on Windows, and Decibri can close neither, so it does not claim that no telemetry is emitted. ONNX Runtime logs one process-information event while its environment is being created, before the setting is applied, once per process, so that event is emitted whichever way the setting is left. The runtime also takes its telemetry state from the Windows tracing session through an ETW callback, so the platform can re-enable telemetry after Decibri has disabled it. On other platforms ONNX Runtime's telemetry provider does nothing.