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
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
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
it is cheap to test.
**Recommendation: a direct-link feasibility spike before any further work on Option A.**
If fan-out holds on this graph, Option C supersedes Option A outright.
### 5.2 Eligibility under Option C — a broad guarded selector
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