Skip to content

JACK one-shot capture can start one period early: ring clear and one-shot enable race the RT callback #467

Description

@mkovero

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:

  1. l.198: reads one_shot_active to decide what to output: the one-shot, or silence.
  2. 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

  • The one-shot's first output sample and the first captured sample are aligned by construction. Either the RT callback establishes the capture start in the same period it starts the one-shot, or the consumer drains to a sample index latched by the RT side. Two consumer-side statements racing the callback is not acceptable.
  • Test against the rejected implementation, for example a 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.
  • The fake backend cannot model a ring race (its on-demand path has no ring). Nothing may claim fake-backend coverage of this.

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:architectArchitect agent acted on itagent:triageTriage agent acted on itbugSomething isn't workingready-to-implementSpec complete, developer can pick upsoftwareac Python/Rust codebasetier-1ac-core/measurement/ — QA runs the standards check

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions