Soak testing surfaced a continuous per-quantum tremolo on the
processed audio path. `pw-cli info` on the filter streams showed
`clock.quantum-limit = "8192"` (frames) — exactly matching the
ring's 16 384-sample capacity. With max buffer == ring capacity
there is zero headroom: each callback the capture pushed a full
buffer's worth into a half-empty ring (dropping the overflow) and
the very next playback callback found the ring under-filled
(zero-filling the deficit). At steady state ~32 k samples/sec
were each dropped on capture and zero-filled on playback —
audible as ~23 Hz amplitude modulation on whatever was playing.
Confirmed by plumbing `samples_starved` / `samples_dropped` (which
existed in `PlaybackState` / `CaptureState` but were never read)
through a shared `PlaybackTiming` so the AGC tick can log per-tick
deltas. Both counters climbed in lockstep, both idle and active —
the lockstep being the signature of buffer-size > ring-size.
Mitigation:
* Bump `RING_CAPACITY` 16 384 → 65 536 (4× the documented
max buffer × CHANNELS). Adds ~340 ms average ring latency,
which is bad for competitive gaming but acceptable as a
hold-the-line measure.
* Set `node.latency = "256/48000"` on both filter halves so
PipeWire targets a small buffer. The hint is advisory and
the system's quantum-limit ceiling still wins, but it costs
nothing and helps on systems with less aggressive defaults.
* Set `node.link-group` on both halves to the same value —
standard for `module-loopback`'s paired-stream pattern.
* Set `audio.rate` and `node.passive = true` on the processed
sink so it runs at the real sink's rate and follows the same
driver. Eliminates a redundant resampler at the monitor →
filter boundary and lands every headroom node under one
driver (confirmed via pw-top: all show as `+` followers of
Mbox).
* Re-order runtime startup so the initial sample rate is
computed before either the processed sink or the filter is
built; both now boot at the same rate.
What this does not do: fix the underlying architectural issue.
The two `pw_stream`s communicate via an rtrb ring with no
PipeWire-graph dependency edge between them, so producer/consumer
ordering within a quantum isn't enforced — the fix is to switch
to a single `pw_filter` node (input + output ports on the same
node, ordering by construction). That rewrite is the next move;
this commit just gets the soak unblocked.
|
||
|---|---|---|
| .. | ||
| headroom-cli | ||
| headroom-client | ||
| headroom-core | ||
| headroom-dsp | ||
| headroom-ipc | ||