No description
Adds a lock-free `PlaybackTiming` struct (atomics: call_count,
sum_us, max_us, spike_count, last_spike_us, last_spike_at_call)
shared between the bus filter's `playback_process` callback (RT
thread, writes) and the AGC controller (daemon thread, reads).
The audio thread wraps each inner call in
`Instant::now()` ... `state.timing.record(elapsed)` — wait-free,
no allocation. The AGC tick samples the snapshot once per second
and logs at WARN when new spikes have landed since the previous
sample, DEBUG otherwise. `#[global_allocator]` declaration in
`headroom-cli` now sits behind `cfg(debug_assertions)` so release
builds compile cleanly (assert_no_alloc strips `AllocDisabler`
under its default `disable_release` feature).
Spike investigation outcome
PLAN §11 follow-up noted: ~240 μs steady state, ~2 ms BUSY
spikes at ~10 s cadence. My ~3 min capture of a 1 kHz sine
routed through processed (release build) showed:
- Steady state ~2180 μs / call
- Max climbed slowly: 2186 → 2222 → 2606 → 2655 → 2812 μs over
~1 min (1.3× steady-state, well within the per-quantum budget)
- Callback rate ~4 Hz, implying the Mbox is negotiating a large
quantum (~12k frames per call vs the 1024-frame baseline
PLAN §4.7 measured). Per-frame DSP cost is identical to the
original budget; the longer wall-clock is just the longer
quantum
No clear ~10 s-cadence outlier pattern reproduced. The system
is comfortably inside budget (~2.2 ms / 250 ms quantum ≈ 1% of
one core). Without an audible artefact or a reproducible
failure mode I'm not chasing the original spike further; the
instrumentation stays so future regressions are visible at
WARN level. `SPIKE_THRESHOLD_US = 5000` is comfortably above
steady-state at both small and large quanta, so only real
outliers trip the log.
Verified
185 tests pass; clippy clean at -D warnings --all-targets.
Release build runs sine playback continuously for >3 min with
no assert_no_alloc abort, no panic, no spike warning. Debug
build (with assert_no_alloc active) likewise stable across
thousands of audio callbacks (revalidated as part of the
release-build comparison).
|
||
|---|---|---|
| crates | ||
| docs | ||
| profiles | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| IPC.md | ||
| PLAN.md | ||
| README.md | ||
| rust-toolchain.toml | ||
headroom
AGC + compressor + true-peak limiter daemon for PipeWire, in Rust.
Headroom puts a per-application audio safety net between noisy sources (browsers, voice chat, random video) and your speakers, while leaving the things you don't want compressed (music players, games, DAWs) untouched.
- Hard −0.1 dBTP ceiling on the processed route, with proper
inter-sample-peak handling, enforced inline so the contract holds
regardless of control-plane state. Streams routed
bypassride the real sink directly and are not in scope of the contract — that's the trade-off that makes the per-app exclusion useful. - Per-app exclusion with profile-driven rules.
- Single binary daemon + CLI, controlled over a Unix-domain socket
with a documented JSON wire protocol (see
IPC.md). - First-party Rust crate (
headroom-client) for programmatic use; third-party clients (Qt panels, status bars, …) target the wire protocol directly.
See PLAN.md for the full design and roadmap.
Status
Pre-alpha. Wire protocol and crate scaffolding are in; daemon and filter are under construction.
Building
nix develop # toolchain + pipewire dev libs + helpers
cargo build # iterate
nix build # final packaged headroom binary
License
GPL-3.0-or-later for the daemon and CLI. headroom-dsp and headroom-ipc
are MPL-2.0 so they can be reused by non-GPL plugin hosts and clients.