headroom/crates
atagen ab02df23fe filter rate matching C: live rebuild when real-sink rate changes
Closes the cold-boot + hot-swap gap A+B left open. When the real
sink's Format-param listener fires with a rate that doesn't match
the filter's currently-running rate, the daemon now rebuilds the
filter atomically and rebinds the slow AGC controller to the new
measurement ring + FilterControl.

What triggers a rebuild
  - Cold-boot against an ALSA sink. `audio.rate` isn't in the
    props dict, so the registry-capture path falls back to 48 kHz
    and creates the filter at that rate. Tens of ms later the
    Format listener fires with the real rate (say 96 kHz). If
    different from the filter's current rate, post
    `PwCommand::RebuildFilter`.
  - Hot-swap. User runs `wpctl set-default <other-sink>` and the
    new sink has a different native rate. `adopt_new_real_sink`
    swaps the Format listener; the next param event from the new
    node's negotiated Format triggers the same rebuild path.

What the rebuild does
  - Snapshots `FilterInit` from the active profile under the
    daemon lock, then drops the lock before touching PipeWire.
  - Drops the old `Filter` (RAII tears down the two pw_streams
    + their listeners), then calls `Filter::create` at the new
    rate. ~50–100 ms audio gap on the processed path during the
    swap.
  - Updates `daemon.filter_control` + `daemon.filter_sample_rate`
    under the lock.
  - `AgcController::rebind(new_consumer, new_control, new_rate)`
    swaps the AGC's view atomically and rebuilds its `ebur128`
    instance at the new rate.
  - Runs `reevaluate_all` so any explicit links anchored at the
    old filter's now-gone ports get re-pinned to the new
    processed-sink ports on the next drain tick.

Plumbing
  - New `PwCommand::RebuildFilter { sample_rate }`.
  - `RoutingState` gains `bus_filter: Option<Filter>` (filter
    ownership moves from `runtime::run`'s local into routing
    state so the registry thread can swap it) and
    `agc_controller: Option<Rc<RefCell<AgcController>>>` so the
    rebuild can call `rebind` on the slow loop.
  - `RoutingState::install_filter_rebuild_handles` is called once
    from `runtime` after `start_routing` + `AgcController::new`.
  - `PwContext::routing_state()` accessor exposes the
    `Rc<RefCell<RoutingState>>` so runtime can install the
    handles without threading them through `start_routing`'s
    signature.
  - The Format listener computes `need_rebuild = filter_sample_rate
    != Some(new_rate)` under the daemon lock, then sends the
    `RebuildFilter` command on `daemon.pw_command_tx` if needed.

What doesn't change
  - Steady-state: when the daemon boots and the rate hasn't
    moved, no rebuild fires. The no-rebuild path is the common
    case for users whose hardware is 48 kHz native; nothing about
    their setup gets touched.
  - Layer A taps: orthogonal to the bus path. The rebuild doesn't
    touch `managed_streams`; existing taps keep their links.

Verified

  - 191 tests still pass; clippy clean.
  - Cold-boot against the dev Mbox (48 kHz native): filter
    creates at 48 k, Format listener fires ~22 ms later
    detecting 48 k → `need_rebuild = false` → no rebuild posted.
    Status reports `processed.sample_rate = 48000`. The
    no-rebuild path is the one most users will hit.
  - Live rebuild against a non-48 kHz sink: not exercised in
    this commit (I can't reliably fabricate a non-48 kHz null
    sink via `pw-cli load-module` in the shell — same limitation
    8d hit). The user's 96 kHz motherboard, once they activate
    its card profile and set it as default, is the next test
    target.
2026-05-21 20:51:11 +10:00
..
headroom-cli 8e: playback callback timing instrumentation + spike investigation 2026-05-21 16:42:46 +10:00
headroom-client stage 2 2026-05-19 16:33:09 +10:00
headroom-core filter rate matching C: live rebuild when real-sink rate changes 2026-05-21 20:51:11 +10:00
headroom-dsp filter rate matching A+B: runtime-parameterised rate at boot 2026-05-21 20:43:55 +10:00
headroom-ipc filter rate matching A+B: runtime-parameterised rate at boot 2026-05-21 20:43:55 +10:00