Metadata-Version: 2.4
Name: decibri
Version: 0.13.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
Requires-Dist: numpy>=1.20 ; extra == 'numpy'
Provides-Extra: numpy
License-File: LICENSE
Summary: Cross-platform audio capture, conditioning, and playback for Python, with voice activity detection on live and recorded audio
Keywords: audio,microphone,capture,sound,voice,vad
Author-email: Decibri <hello@decibri.com>
License: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/decibri/decibri/blob/main/bindings/python/CHANGELOG.md
Project-URL: Homepage, https://github.com/decibri/decibri
Project-URL: Issues, https://github.com/decibri/decibri/issues
Project-URL: Repository, https://github.com/decibri/decibri

# Decibri

Cross-platform audio capture, conditioning, and playback for Python, with voice activity detection on live and recorded audio.

[![PyPI version](https://img.shields.io/pypi/v/decibri.svg)](https://pypi.org/project/decibri/)
[![Python versions](https://img.shields.io/pypi/pyversions/decibri.svg)](https://pypi.org/project/decibri/)
[![License](https://img.shields.io/pypi/l/decibri.svg)](https://github.com/decibri/decibri/blob/main/LICENSE)

Decibri is a native Python package that delivers microphone capture, speaker output, local voice activity detection, device enumeration, and sample format conversion. It is written in Rust (via PyO3 / abi3) and ships pre-built wheels for Linux, macOS Apple Silicon, and Windows.

## Install

Recommended with [uv](https://docs.astral.sh/uv/):

```bash
uv pip install decibri
```

Or with pip:

```bash
pip install decibri
```

## Quickstart

### Capture audio

```python
import decibri

with decibri.Microphone(sample_rate=16000, channels=1) as mic:
    for chunk in mic:
        print(f"Got {len(chunk)} bytes")
        break  # exit after first chunk for demo
```

### Record one second to a WAV file

```python
import decibri

decibri.record_to_file("output.wav", duration_seconds=1.0, sample_rate=16000)
```

### Condition and analyze a recording

```python
import decibri

# The same conditioning chain as the live microphone, over a WAV file.
with decibri.File("clip.wav", denoise="fastenhancer-t", highpass=80) as file:
    for chunk in file:
        handle(chunk)          # conditioned int16 PCM bytes

# Whole-file speech analysis (a live stream cannot do this).
report = decibri.File("clip.wav", vad="silero").analyze()
for segment in report.segments:
    print(segment.start, segment.end)   # seconds of file time
```

### Capture with Silero VAD

```python
import decibri

with decibri.Microphone(sample_rate=16000, vad="silero") as mic:
    for chunk in mic:
        print(f"Got {len(chunk)} bytes; VAD score {mic.vad_score}; speaking={mic.is_speaking}")
        break  # exit after first chunk for demo
```

### Async capture

```python
import asyncio
import decibri

async def main():
    async with await decibri.AsyncMicrophone.open(sample_rate=16000, vad="silero") as mic:
        async for chunk in mic:
            print(f"Got {len(chunk)} bytes; VAD score {mic.vad_score}")
            break  # exit after first chunk for demo

asyncio.run(main())
```

### Speaker output

```python
import decibri

with decibri.Speaker(sample_rate=24000, channels=1) as spk:
    audio_bytes = b"\x00\x00" * 24000  # 1 second of silence at 24kHz int16
    spk.write(audio_bytes)  # int16 PCM
    spk.drain()
```

## Public API

### Core classes

- `Microphone`: synchronous audio capture
- `Speaker`: synchronous audio output
- `File`: offline source; conditions a recording or in-memory samples, analyzes it for speech, or writes it back out
- `AsyncMicrophone`: async-await audio capture
- `AsyncSpeaker`: async-await audio output
- `AsyncFile`: async-await offline source

### Module-level functions

- `decibri.input_devices()`: enumerate available input devices
- `decibri.output_devices()`: enumerate available output devices
- `decibri.version()`: version + audio backend info
- `decibri.record_to_file(path, duration_seconds, ...)`: record N seconds to a WAV file
- `decibri.async_record_to_file(path, duration_seconds, ...)`: async equivalent

### Value types

`MicrophoneInfo`, `SpeakerInfo`, `VersionInfo`, `Chunk`, `VadReport`, `VadWindow`, `Segment`, `AecMetrics`, `AecChannelMetrics`, `SaveReport`, the config objects `Vad` and `Aec`, and the device selector `Device`.

### Exceptions

The full hierarchy lives at `decibri.exceptions`. Top-level catch-targets surfaced at the package root:

- `DecibriError`: base of the hierarchy
- `DeviceError`: input / output device problems
- `OrtError`: ONNX Runtime issues
- `OrtPathError`: ORT dylib path resolution issues
- `ForkAfterOrtInit`: Linux fork-after-ORT-init detection

## Device selection

```python
import decibri

# System default
mic = decibri.Microphone()

# By name (case-insensitive substring match)
mic = decibri.Microphone(device="USB")

# By index
devices = decibri.input_devices()
mic = decibri.Microphone(device=devices[1].index)

# By stable per-host identifier (exact match; survives across enumerations)
mic = decibri.Microphone(device=decibri.Device(id=devices[1].id))
```

`Speaker`, `AsyncMicrophone`, `AsyncSpeaker`, `record_to_file` and `async_record_to_file` take the same `device` forms. The device is resolved when the stream starts: a name that matches more than one device raises `MultipleDevicesMatch`, and a selector that matches none raises `MicrophoneNotFound` or `SpeakerNotFound`.

## Voice Activity Detection

Decibri ships with two VAD modes: a lightweight RMS energy threshold (opt-in via `vad="energy"`) and a Silero ONNX model (~2.3 MB, bundled in the wheel; no API keys required). Pass the mode as the `vad="silero"` / `vad="energy"` shorthand (which uses the default threshold and a 300 ms holdoff) or as a `decibri.Vad(model=, threshold=, holdoff_ms=)` config object to tune them.

```python
# Energy mode (lightweight, no model)
mic = decibri.Microphone(vad=decibri.Vad(model="energy", threshold=0.01))

# Silero mode (ML-based, more accurate in noisy environments)
mic = decibri.Microphone(vad=decibri.Vad(model="silero", threshold=0.5))
```

Use `mic.vad_score` (a value in `[0, 1]`) to gate downstream processing. `mic.is_speaking` returns the boolean above-threshold view.

## Decibri ACE (audio conditioning)

Decibri ACE (Audio Conditioning Engine) is decibri's opt-in audio front-end for speech: a conditioning chain applied to the captured audio before it is delivered. Every stage is off by default, runs on-device, and needs no API key. Leave the options unset and the capture path is unchanged.

The stages run in this order. Pass any subset on the `Microphone` constructor:

| Stage | Keyword | Value |
| --- | --- | --- |
| DC removal | `dc_removal=True` | bool, default `False` |
| Denoise | `denoise="fastenhancer-t"` | the one bundled model |
| High-pass | `highpass=80` or `100` | Hz, second-order Butterworth |
| AGC | `agc=-18` | int dBFS, -40 to -3 |
| Limiter | `limiter=-1.0` | float dBFS, -3.0 to 0.0 |

```python
import decibri

with decibri.Microphone(
    sample_rate=16000,
    denoise="fastenhancer-t",  # bundled speech-enhancement model
    highpass=80,               # remove low-frequency rumble
    agc=-18,                   # target level in dBFS
    limiter=-1.0,              # peak ceiling in dBFS
    vad=decibri.Vad(model="silero", threshold=0.5),
) as mic:
    for chunk in mic:
        if mic.is_speaking:
            handle(chunk)      # conditioned int16 PCM bytes
```

The denoise model is bundled in the wheel (the same way the Silero VAD model is), so `denoise="fastenhancer-t"` needs no download. VAD reads the signal before the chain, so `mic.vad_score` and `mic.is_speaking` are unaffected by which conditioning stages you enable. The same options are available on `AsyncMicrophone`.

## Echo cancellation

Acoustic echo cancellation removes the echo of far-end audio (what your application is playing out) from the captured audio. It is opt-in: pass `aec="tau"` on the `Microphone` constructor and push the far-end audio through `push_aec_reference()` as it is played, in played order. Leave `aec` unset and the capture path is unchanged; with no reference pushed the captured audio passes through unchanged. The canceller runs before voice activity detection, so `mic.vad_score` and `mic.is_speaking` read the echo-removed signal and playback stops triggering detection. It requires `sample_rate` in 8000 to 48000; a rate outside that raises `AecSampleRateUnsupported` at construction.

`aec="tau"` selects the model with its defaults; an `Aec` config object tunes it:

| Field | Default | Value |
| --- | --- | --- |
| `model` | `"tau"` | `"tau"`, the one model |
| `tail_ms` | `None` (the canceller's 200) | int ms, 16 to 500 |
| `suppression` | `None` (the canceller's `"conservative"`) | `"conservative"` or `"off"` |
| `reference_sample_rate` | `None` (the capture `sample_rate`) | int Hz, 1000 to 384000 |
| `reference_channels` | `1` | int, at least 1 |

```python
import decibri

with decibri.Microphone(sample_rate=16000, aec="tau") as mic:
    # Push the far-end audio as it is played, in played order: the same
    # input shapes Speaker.write accepts, in the microphone's dtype.
    mic.push_aec_reference(far_end_pcm)
    for chunk in mic:
        handle(chunk)                  # echo-cancelled int16 PCM bytes
        metrics = mic.aec_metrics()    # AecMetrics while capture runs
```

`push_aec_reference()` never blocks and never raises on a full queue: samples that do not fit are discarded and counted by `aec_metrics().reference_dropped`. Silence between played audio need not be pushed. The canceller reads one mono reference: a reference at a `reference_sample_rate` other than the capture rate is converted before the canceller sees it, and with `reference_channels` above 1 each frame is averaged to one mono sample. With `channels` above 1, one canceller runs per delivered channel, each fed the same pushed reference, so one push serves every channel.

`aec_metrics()` returns an `AecMetrics` dataclass, the canceller's report merged with the reference queue's counters (`delay_samples`, `erle_db`, `double_talk`, `reference_starved`, `acquisition_parked`, `reference_reanchors`, `reference_dropped`, `reference_silence`), or `None` when `aec` is unset or capture is not running. Its `channels` tuple holds one `AecChannelMetrics` per delivered channel, in delivered order; the top-level fields report the first delivered channel's. `erle_db` is not a quality ranking across channels: it rises with echo distance, so a far microphone reports a higher figure than a near one while removing less echo in absolute terms.

The same `aec` parameter is available on `AsyncMicrophone`. `push_aec_reference()` is a plain method there too, so a renderer callback calls it without awaiting; `aec_metrics()` is awaited.

## Files

`File` runs the same conditioning chain as `Microphone` on audio you already have: the same conditioning options (`dc_removal`, `denoise`, `highpass`, `agc`, `limiter`), the same iteration, the same conditioned chunks out, and the same opt-in `vad=`. Echo cancellation is a live-capture option and is not offered on `File`. A `File` reads WAV, AIFF, AIFF-C and FLAC (`File("clip.wav")`, or the identical `File.open("clip.wav")`; the container is identified from the file's own bytes rather than its extension) or wraps in-memory samples (`File.buffer(samples, input_rate=48000)`; raw samples carry no header, so their native rate is explicit). `sample_rate` stays the target output rate, the same meaning it has on `Microphone`, so a 44.1 kHz recording comes out at 16 kHz unless you set it.

Because a `File` is a complete recording, it can analyze the whole recording for speech:

```python
report = decibri.File("clip.wav", vad="silero").analyze()   # or .analyse()
for w in report.scores:
    print(w.start, w.end, w.vad_score, w.is_speech)   # per 32 ms window
for segment in report.segments:
    print(segment.start, segment.end)                 # merged speech regions
```

`analyze()` requires VAD: a `File` built without `vad=` raises `VadNotConfigured` rather than constructing a detector silently. With `vad=` set, metadata iteration (`iter_with_metadata()`) carries per-chunk `vad_score` and `is_speaking` exactly as the microphone does, with one deliberate difference: on a `File`, the speaking holdoff and the `Chunk.timestamp` are measured in FILE time (sample positions in seconds), never wall-clock time, so a file processed faster than real time still reports correct speech timing.

A `File` can also be written back out. `save(path, format=None, compression=None)` runs the recording once through the same conditioning pass iteration delivers and writes it as 16-bit PCM at `sample_rate`, at the delivered channel count, in WAV, AIFF or FLAC:

```python
report = decibri.File("noisy.wav", denoise="fastenhancer-t").save("clean.flac")
print(report.clipped_samples, report.non_finite_samples)
```

The container comes from the path's extension (`.wav`, `.aiff`, `.aif`, `.aifc` or `.flac`) or from `format`; an extension it does not recognise raises `AudioFormatUnsupported` rather than defaulting. `compression` sets the FLAC compression level, 0 to 8 with 5 the default. The returned `SaveReport` says what the write did to the samples: `clipped_samples` counts finite samples outside full scale clamped to `[-1.0, 1.0]`, and `non_finite_samples` counts the non-finite samples the save replaces before they reach the file (NaN as silence, an infinity as full scale). When the conditioning chain runs it has already replaced every non-finite sample with silence at its entry, so the count covers the direct path only, a mono source already at `sample_rate` with no conditioning enabled. Saving consumes the source and raises `FileEngaged` once iteration has begun, exactly as `analyze()` does.

Iteration, analysis and saving are separate single passes; construct one `File` per operation. `AsyncFile` mirrors the whole surface with `async` semantics, `await file.save(...)` included.

## Compatibility

| Python                       | Platforms                                                               |
| ---------------------------- | ----------------------------------------------------------------------- |
| 3.10, 3.11, 3.12, 3.13, 3.14 | Linux x64, Linux ARM64, macOS Apple Silicon, Windows x64, Windows ARM64 |

On Windows ARM64 the oldest supported Python is 3.11, the first CPython release published for that platform.

## Bundled assets

The wheel includes:

- **Silero VAD ONNX model** (~2.3 MB): no downloads or API keys required for `vad="silero"`.
- **ONNX Runtime dylib** (~15-20 MB platform-specific): no system dependency on `pip install onnxruntime`.

First ORT load on `vad="silero"` initialization is ~100 to 500 ms (amortized across subsequent calls).

## ONNX Runtime telemetry

`vad="silero"` and the ACE `denoise` stage run on ONNX Runtime, which carries its own telemetry, separate from anything decibri does. Decibri disables it on the environment it commits when it initializes the runtime. Set `DECIBRI_ORT_TELEMETRY=1` in the environment before first use to leave it enabled; every other value, an empty value, and an absent variable leave it disabled.

Two limits apply on Windows and decibri can close neither, so decibri does not claim that no telemetry is emitted. ONNX Runtime logs one process-information event while the environment is being created, before the setting is applied, and logs it once per process, so that event is emitted whichever way the setting is left. The runtime also assigns 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.

## Async usage

For Silero VAD in async code, use the `open()` factory to dispatch the synchronous ORT init off the event loop:

```python
import asyncio
import decibri

async def main():
    async with await decibri.AsyncMicrophone.open(vad="silero") as mic:
        async for chunk in mic:
            ...

asyncio.run(main())
```

Synchronous constructors (`AsyncMicrophone(...)`) remain supported and unchanged; `open()` is the recommended pattern when ORT load cost matters.

## Multiprocessing

Linux multiprocessing with Silero VAD requires `set_start_method('spawn')`. Calling `fork()` after ORT initialization raises `ForkAfterOrtInit`:

```python
import multiprocessing as mp

mp.set_start_method('spawn')  # required on Linux for Silero
```

See the ecosystem guides under `bindings/python/docs/ecosystem/` (jupyter, docker, multiprocessing) for environment-specific details.

## Documentation

- **Source code & issues**: [github.com/decibri/decibri](https://github.com/decibri/decibri)
- **CHANGELOG**: [CHANGELOG.md](https://github.com/decibri/decibri/blob/main/CHANGELOG.md)
- **Ecosystem guides** (Jupyter, Docker, multiprocessing): under `bindings/python/docs/ecosystem/` in the repo

## License

Apache-2.0. See [LICENSE](https://github.com/decibri/decibri/blob/main/LICENSE) for details.

Copyright (c) 2026 Decibri.

