problem
JackEngine::play_and_capture_cancellable (ac-daemon/src/audio/jack_backend.rs, read at origin/main 340f1c55) starts a one-shot measurement from the consumer thread in two steps:
self.rings.clear_meas_uncounted(); // l.426
self.state.one_shot_active.store(true, Ordering::Release); // l.427
The RT callback (Process::process, l.194–231) does two things each period, in this order:
- l.198: reads
one_shot_active to decide what to output: the one-shot, or silence.
- l.228: pushes that same period's input into the measurement ring.
clear_meas_uncounted (rings.rs l.121) calls the consumer's clear(), which drops only the samples buffered at that instant. capture_available (l.196) then pops the oldest n_total samples.
The race. A period can check the flag (step 1) before l.427 and push its input (step 2) after l.426. That period outputs silence, but leaves one full period of pre-stimulus input at the head of the ring. The one-shot starts in the next period. The returned capture is therefore misaligned with the stimulus by exactly one period, and every arrival derived from it (IR peak, onset, calibrate's τ) reads one period late:
- 256 samples on the FF400 rig at period 256
- 1024 at period 1024
Within the capture the offset is fixed and repeatable, and nothing in the returned data flags it.
Window. A period is exposed when the consumer's two statements land while the callback is between l.198 and l.228. The probability per capture is therefore about the callback's run time over the period length: small, but not the nanosecond gap between two adjacent statements. That estimate is derived from the code, not measured.
evidence
what it affects
acceptance criteria
related
#347, #359, #363 (one-period jumps), #460 (revision 2 risk and invariant b), #283, #437 (capture budget and ring sizing in the same code).
problem
JackEngine::play_and_capture_cancellable(ac-daemon/src/audio/jack_backend.rs, read atorigin/main340f1c55) starts a one-shot measurement from the consumer thread in two steps:The RT callback (
Process::process, l.194–231) does two things each period, in this order:one_shot_activeto decide what to output: the one-shot, or silence.clear_meas_uncounted(rings.rsl.121) calls the consumer'sclear(), which drops only the samples buffered at that instant.capture_available(l.196) then pops the oldestn_totalsamples.The race. A period can check the flag (step 1) before l.427 and push its input (step 2) after l.426. That period outputs silence, but leaves one full period of pre-stimulus input at the head of the ring. The one-shot starts in the next period. The returned capture is therefore misaligned with the stimulus by exactly one period, and every arrival derived from it (IR peak, onset,
calibrate's τ) reads one period late:Within the capture the offset is fixed and repeatable, and nothing in the returned data flags it.
Window. A period is exposed when the consumer's two statements land while the callback is between l.198 and l.228. The probability per capture is therefore about the callback's run time over the period length: small, but not the nanosecond gap between two adjacent statements. That estimate is derived from the code, not measured.
evidence
Code reading only. Found by the estimate_onset's causal bound is unreachable: no producer ever records position.distance_m #460 architect pass (revision 2, risks) and re-read at the lines above before filing.
Not observed so far on this build. On 2026-09-15, pupu, build
298f77dc, 186 one-shot captures showed no one-period offset:calibrateτ readings, all identical within each device-enumeration epoch;ac plot ircaptures, whose loopback brackets were all identical.That bounds the rate as uncommon on that host; it does not show the race is unreachable.
Same signature as τ must be measured more than once: a single reading can be one period wrong with nothing to detect it #347, plot_ir's arrival inherits the one-period jump with no corroboration — #347 guards only calibrate #359 and calibrate stores a τ one JACK period short and calls it corroborated — 42 of 97 rig runs, every one labelled "2 readings agree" #363 (a one-period jump). Those were attributed to pipewire-jack, and the jump disappeared when that stack was removed. This race is an in-process mechanism producing the same symptom on any JACK stack.
what it affects
plot_ir: a single capture's arrival, with no corroboration (plot_ir's arrival inherits the one-period jump with no corroboration — #347 guards only calibrate #359's concern, via a second mechanism).calibrate:measure_tau_twiceuses two one-shots. A race in exactly one of them gives two readings a period apart, which τ must be measured more than once: a single reading can be one period wrong with nothing to detect it #347's rule refuses. That's a correct refusal of a spurious result, not a stored error. A race in both, a stored error, would need two independent hits.acceptance criteria
rings.rs-level test driving the producer and consumer interleavings: for every interleaving point, the fixed sequence yields alignment, and the current clear-then-enable sequence, computed inside the test, yields the one-period offset at the exposed points. A test that only runs the happy interleaving cannot fail on this defect.related
#347, #359, #363 (one-period jumps), #460 (revision 2 risk and invariant b), #283, #437 (capture budget and ring sizing in the same code).