Two follow-ups from 4k's commit body, both surfaced by the same
smoke-test setup.
filter.playback through 4k
`try_capture_filter_playback` and the bypass-retarget pass in
`adopt_new_real_sink` now call `enqueue_route` on top of the
existing `write_stream_target`. Without that, WirePlumber was
fanning the filter's output port to *both* the real sink (the
intended target) and `headroom-processed:playback` — a feedback
loop where the filter's output flowed back into the processed
sink, then through monitor → filter capture → DSP → filter
playback again.
Plumbing 4k for the filter required two small tweaks elsewhere:
- `enforce_link_for_managed_stream` and `apply_pending_routes`
were destroying every non-target outbound link from a managed
source. That included Layer A passive tap links, which sent
Layer A's own retry loop into a create/destroy fight with this
code. Both paths now skip links whose destination isn't a
known Audio/Sink, so only WP-created sink links get torn down.
- The processed sink is now also recorded in `sinks_by_name`
(previously skipped because it's "tracked elsewhere" in
`processed_sink_id`). `apply_pending_routes` resolves the
target by name, so it needed processed visible here to handle
Route::Processed.
Sticky default.audio.sink
`adopt_new_real_sink` previously short-circuited via
`apply_real_sink_change` when the real sink name hadn't changed
— which meant the *first* time WP rewrote `default.audio.sink`
away from `headroom-processed` we'd re-assert, but on every
subsequent rewrite to the same Mbox value we'd skip out before
reaching the re-assert call at the bottom of the function. WP
won 1-0 after the first round.
Fixed by hoisting the re-assertion into a dedicated method
(`reassert_default_processed`) with a per-second attempt cap (10
per second), called both from the idempotent early-exit path
and from the end of the full retarget path. The cap is what
keeps a hostile WP policy from pulling us into a hot loop — at
10 Hz we tolerate a brief metadata storm, then back off for the
remainder of the window.
Verified
185 tests still pass; clippy clean at -D warnings --all-targets.
Live smoke against a running PipeWire/WP:
- `pw-metadata` confirms `default.audio.sink` settles on
`headroom-processed` after daemon startup (daemon wrote 3
times in ~30 ms, WP yielded; metadata then stayed put).
- `pw-link` confirms `headroom-filter.playback:output_{FL,FR}`
has exactly one outbound link each — to the Mbox playback
ports — with no link back to processed:playback.
- Sine-into-processed regression still passes: 59/59
meter ticks above the floor, momentary_lufs around -28, true
peak around -21 dBTP — bus DSP chain still processing
end-to-end after the filter's link surface was tightened.