docs: adopt fan-out (Option C) after feasibility spike

Ran the direct-link spike on the live graph (PipeWire 1.6.8,
WirePlumber 0.5.15). Fan-out carries full-level audio for paplay, mpv
and VLC while the application keeps its existing speaker link;
WirePlumber does not reap foreign links across default-sink switch,
suspend/resume or 100s steady state; and non-lingering links are
destroyed automatically when their owning connection is SIGKILLed.

The decisive result is that destroying the capture sink mid-share left
the application playing to its speakers undisturbed, so capture-side
failure degrades to "not captured" rather than breaking the user's
audio. That is the property the move-based design had to work hard to
approximate.

Records what the spike does not prove: fidelity beyond signal presence,
daemon restart, quantum perturbation, and exclusive/passthrough streams.
The capture null sink is still pactl-owned, so Stop Share continues to
leak a module every time and the graceful-stop work is still owed.

Eligibility becomes a broad guarded selector rather than a narrow
allowlist, since copying no longer risks disturbing the source.

Option A and its attendant cleanup, restore and output-switch machinery
are retained for the record but are no longer the plan. A v3 rewrite is
owed once the AEC playback-leg identity is settled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 05:04:25 -04:00
co-authored by Claude Opus 4.8
parent 88ad5a0807
commit 10203e1edb
+59 -3
View File
@@ -215,7 +215,33 @@ saved-default restoration plus idempotent crash repair.
B's genuine advantages, recorded honestly: new apps land in the mix automatically, no B's genuine advantages, recorded honestly: new apps land in the mix automatically, no
registry-add race, and no sweeping up of arbitrary module playback legs. registry-add race, and no sweeping up of arbitrary module playback legs.
## 5.1 Option C — copy instead of move (Codex's proposal; now the leading candidate) ## 5.1 Option C — copy instead of move — ✅ **DECIDED: this supersedes Option A**
> **STATUS 2026-07-20: feasibility spike RUN, Option C ADOPTED by both reviewers.**
> Measurements: `~/Documents/handoff-docs/Claude/peerspeak/fanout-spike-results-2026-07-20.md`.
> Codex ratification: `…/Codex/peerspeak/review-2026-07-20-fanout-spike-round4-ratification.md`.
> Everything below in §5 (Option A/B), §6, §7 and §8.28.3 is **retained for the record
> but is no longer the plan** — most of it describes problems Option C does not have.
> **A v3 rewrite is owed once the AEC gate (§10, OPEN-Q 3) is settled.**
>
> Measured on this machine (PipeWire 1.6.8 / WirePlumber 0.5.15):
> fan-out carries full-level audio (24.1 dB, matching the speaker monitor) for
> **paplay, mpv and VLC**; the app keeps its speaker link; WirePlumber does **not** reap
> foreign links across default-sink switch, switch-back, suspend, resume, or 100 s
> steady state; and non-lingering links are destroyed automatically when their owning
> connection is **SIGKILLed**.
> **Strongest result — destroying the capture sink mid-share left the application
> playing to its speakers undisturbed.** Capture-side failure degrades to
> "not captured," never to "the user's audio is broken."
>
> **Not yet proven** (do not overclaim these): fidelity beyond signal presence — equal
> `mean_volume` on a sine cannot see xruns, drift or dropouts; daemon restart;
> quantum/latency perturbation; sample-rate mismatch, surround, `port.exclusive`,
> IEC958 passthrough; two concurrent capture sinks.
> **Also still true:** the capture null sink is `pactl`-loaded and pipewire-pulse-owned
> (`audio.rs:69`), so **Stop Share still leaks one null-sink module every time**.
> Option C fixes stranded *desktop audio*, not the sink leak — §6's graceful stop is
> still owed, and the capture sink should become connection-owned too.
Codex's round-2 review made the strongest architectural point either review has Codex's round-2 review made the strongest architectural point either review has
produced: **the design is overbuilt because it *moves* audio that pixelpass only needs produced: **the design is overbuilt because it *moves* audio that pixelpass only needs
@@ -256,8 +282,38 @@ open question is whether **WirePlumber tears down foreign links** it did not cre
part of its own policy management. That is the one thing that would kill Option C, and part of its own policy management. That is the one thing that would kill Option C, and
it is cheap to test. it is cheap to test.
**Recommendation: a direct-link feasibility spike before any further work on Option A.** ### 5.2 Eligibility under Option C — a broad guarded selector
If fan-out holds on this graph, Option C supersedes Option A outright.
Because copying is cheap and non-destructive, the narrow allowlist of §4.2 is no longer
justified — but "fan out everything and let mis-selection be harmless" is **also wrong**:
selecting peerspeak-owned playback recreates self-echo, selecting the AEC leg recreates
call echo, and a second link still participates in format/buffer negotiation, so it can
perturb the source even though the original link survives.
Agreed replacement:
- Start from **all** `Stream/Output/Audio` nodes.
- Exclude `peerspeak.owned` (§4.1).
- Exclude the proven AEC identity (**OPEN-Q 3 — the remaining gate**).
- Exclude pixelpass-owned objects and any graph path with capture ancestry (cycles).
- Skip `port.exclusive`, encoded/passthrough, and already-owned links.
- Create **per-port** links, **retain the Link proxies** for the session, and count a
stream captured only once every required link reaches `ACTIVE`. In the Rust
`pipewire` crate, dropping a proxy destroys the object — that is the ownership
mechanism, and it is also what makes SIGKILL safe.
- On failure: leave the original route untouched, report the stream as unsupported.
`node.dont-move` drops out of the predicate entirely — fan-out is not a metadata move.
### 5.3 Next gate
**Load `module-echo-cancel` on the live graph and establish a stable, pixelpass-
observable identity for its actual playback `Stream/Output/Audio` leg** — not merely the
virtual sink — that survives unload/reload without PID guesswork. Then fan out an
ordinary tone and demonstrate that remote-call audio is absent from the capture monitor.
If no stable tag or module/group relationship exists, the AEC path must be redesigned
before this feature proceeds. Both reviewers agree nothing belongs ahead of this.
## 6. Graceful stop is a prerequisite, not a test case ## 6. Graceful stop is a prerequisite, not a test case