0018 — clock.force-rate is the live re-clock lever; the reported rate is measured from hardware
Status: Accepted and built. Date: 2026-07-15
Context
The graph-clock controller was built on a belief stated ~6× in its own comments:
clock.force-rate is inert on a running PipeWire graph. Acting on that, it wrote
force-rate "best-effort" but reported the graph rate by reading the clock.rate metadata
key back — the graph target, which does not move. On the rig this meant the controller
forced the RME to 48 kHz while reporting 192 kHz, and an operator's 192 kHz pick "silently
no-op'd". The "inert" verdict was itself a measurement error: it read the wrong metadata key.
Decision
clock.force-rateIS the live re-clock lever. Proven live on the RME Babyface Pro: writing it drove the driver 96 → 192 → 48 kHz instantly, no restart (target rate must be inclock.allowed-ratesfirst — a force outside it is dropped silently).- The reported/effective rate is MEASURED from hardware — the ALSA driver's
Momentary freqin/proc/asound/card<N>/stream0(resolved from the default sink's card), snapped to a standard rate — never theclock.ratemetadata proxy. - Device-supported rates drive the UI menu (
clock.state.supportedRates, probed from the card), not a hardcoded list. - Persistence is two-layer: a managed
pipewire.conf.ddrop-in (default.clock.rate) so a cold PipeWire start opens at the picked rate, plus a boot re-assert (the server re-forces the persisted rate if the live driver has drifted). - Pure parsers + the probe live in a dependency-free leaf (
clock-rate-parse.ts/clock-rates.ts), separate from the IO probe (clock-device-rates.ts).
Consequences
- Rate reporting is honest; 192 kHz is reachable and holds across engine + PipeWire restarts.
- The old "restart audio to apply" UI note becomes a rare driver-refused signal, since a pick normally applies live.
- The controller measures, it never trusts a metadata echo — the same "measure, don't declare" principle ADR 0020 applies to plugin latency.
- Superseded belief: the earlier "force-rate is inert" assumption is wrong on this hardware and is purged from the code.