headroom/crates
atagen e0c23ec459 F1: make bypass on a real kill switch
Codex flagged that `bypass.set` only flipped `bypass_global` in
profile state and never touched the graph: `try_route_stream`
returned Skip but the daemon kept re-asserting
`default.audio.sink = headroom-processed`, so apps following
default still landed in the processor, and already-managed streams
kept their explicit links to the processed sink. The "kill switch"
killed nothing.

What the bypass now actually does

  Three coupled effects, applied atomically by a single
  `PwCommand::ReevaluateAll` post from the IPC handler:

  1. **Routing decision flips.** `routing::evaluate` learned to
     short-circuit to `Route(Bypass)` for every routable playback
     stream when `bypass_global=true`. Surround's pre-existing
     `>2ch -> Bypass` rule still applies; both share the same
     output and pick up the same explicit-link machinery from 4k.

  2. **Existing managed streams get re-routed.** A new
     `known_streams: HashMap<u32, PwNodeInfo>` cache in
     `RoutingState` (populated on `try_route_stream`, cleared in
     `on_global_remove`) lets `reevaluate_all` iterate every
     stream we've ever seen and re-run the decision. The
     extracted `apply_bus_route` runs the same enqueue / unmanage
     logic the registry callback uses, so the live-arrival path
     and the bypass-toggle path stay in lockstep.

  3. **`default.audio.sink` flips to the real sink.** Inside
     `reevaluate_all`, the daemon writes default to the real sink
     name under bypass, and back to `headroom-processed` when
     bypass clears. The `reassert_default_processed` rate-limiter
     is gated on bypass so we don't keep fighting WP for a sink
     we no longer want as default. Apps that route to "default"
     (which is most legacy code paths and a lot of GTK/Qt
     widgets) now actually skip the processor under bypass.

Adjacent cleanups that fell out

  - `try_route_stream` no longer carries the bypass branch
    inline. The split — registry callback inserts cache + calls
    `apply_bus_route` + maybe spawns Layer A — keeps the
    re-evaluation path free of the `&GlobalObject` it doesn't
    have. Layer A spawning stays at first-see time as before;
    streams that arrived before the daemon doesn't get a
    retroactive tap, which is fine since Layer A is orthogonal
    to bus routing and tap creation requires the registry global.
  - `RoutingDecision::Skip` now properly tears down any prior
    bus state (`unmanage()` drops the Link proxies and removes
    the IPC-visible `state.streams` entry).
  - `PwCommand::ReevaluateAll` is a generic re-evaluation
    trigger; F2 will reuse it for profile / rule changes.

Tests

  - `routing::evaluate` signature picked up a `bypass_global:
    bool` arg; 11 unit tests updated to pass `false`.
  - ops::tests' `let PwCommand::RouteStream { .. } = cmd;` is
    now `let ... else { panic!(..) }` (the enum is no longer
    single-variant). 188 tests pass; clippy clean.

Live verification

  A/B/A against a 1 kHz sine `--target headroom-processed`:
  - bypass off (baseline): pw-cat → headroom-processed:playback;
    default.audio.sink = headroom-processed.
  - bypass on: pw-cat → Mbox:playback (the explicit link to
    processed is gone, a new explicit link to the real sink is
    in place); default.audio.sink = the Mbox.
  - bypass off (back): pw-cat → headroom-processed:playback;
    default.audio.sink = headroom-processed.
  - Layer A tap link stays attached through both transitions —
    orthogonal as designed.
2026-05-21 18:32:43 +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 F1: make bypass on a real kill switch 2026-05-21 18:32:43 +10:00
headroom-dsp F6: honour compressor.enabled in the DSP 2026-05-21 18:19:32 +10:00
headroom-ipc 5: monitor TUI + wire fill-ins 2026-05-21 13:35:27 +10:00