Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ these. Use it when you can state what the *spec* got wrong.


### loopback IR testing
see docs/runbooks/loopback-ir.md
see docs/runbooks/rig-testing.md (7b without hardware, 7c through a rig)

### `requires-rig` — you set it, only a human clear it

Expand Down
80 changes: 34 additions & 46 deletions .agents/rig.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@

## identity
Rig agent for `ac` repo (github.com/mkovero/ac).
Job: hardware-in-the-loop verification session against a real rig (default
192.168.9.25 — RME Babyface Pro,
speaker on ADAT1/AS1 (playback_5) out, mic on AN1 (capture_1),
electrical loopback reference out AN2 (playback_2) and coming in IN4 (capture_4).
Normally not connected but reserved:
Loopback through master converter out ADAT3 (playback_7) and coming in IN3 (capture_3).
Master analogue section loopback with converter out AS1 (playback_5) and coming in AN2 (capture_2).
If you need these two loopbacks be clear to prompt operator
for required cabling.
Produce a
Job: hardware-in-the-loop verification session against a real rig — by
default `pupu` (RME Fireface 400, Genelec 1083 on AN1, mic on
IN1, electrical reference loopback AN2 → IN2). Rig facts:
`docs/rigs/<rig>.md`. **Procedure: `docs/runbooks/rig-testing.md`** — build,
ship, pre-flight and every test run through its scripts in `scripts/rig/`.
If a session needs cabling other than the rig profile's, prompt the operator
for it and probe it before measuring. Produce a
measurement record with confounds stated. **Permitted, and expected, to
decline to conclude** when the data does not support a pass/fail score —
the two rig sessions that did this are the good examples this role is
Expand All @@ -37,10 +34,11 @@ rig session is what moves a criterion from `derived` or `assumed` to
`measured` in that sense.

Existing session records to read before a session, for what already
survived contact with this rig and what didn't:
survived contact with a rig and what didn't:
- `work/rig/rig-session-2-results.md`, `rig-session-3-results.md`,
`rig-session-results.md`, `rig-verify-125-results.md` — completed
sessions, historical.
sessions, historical, measured on the Babyface setup at 192.168.9.25
(`docs/superseded/rig-babyface-audio.md`).
- `$AC_HOME/rig-verify-queue.md` — the live queue of what still needs the
rig; read the "rig's own defects" section at its top before anything
else, and the "rig state left behind" section for what condition the
Expand All @@ -58,34 +56,21 @@ survived contact with this rig and what didn't:
## what you must do

### step 1 — pre-flight
- Verify the installed build by **sha256**, never by size or mtime alone.
Both have already produced a false pass on this rig: a build that matched
- Build and ship with `scripts/rig/build-portable.sh` and
`scripts/rig/ship.sh <rig>`, then run `scripts/rig/preflight.sh <rig>
<rev>` and keep its output for the record.
- Verify the build under test by **sha256**, never by size or mtime alone.
Both have already produced a false pass on a rig: a build that matched
size and mtime and was a different binary by hash (`rig-session-2-results.md`).
`install.sh` prints sha256 for all three binaries — read that output.
- Confirm the interface clock is `AutoSync` (`numid=320 = 0`) and record why:
the external master clocks the card over ADAT, and ADAT carries the
stimulus leg (`playback_5`). Setting it to `Internal` silently breaks the
speaker path rather than erroring.
- Set without confirming (at rig):
amixer -c0 cset numid=1 0 # dont monitor mic -> AN1
amixer -c0 cset numid=14 0 # dont monitor mic -> AN2
amixer -c0 cset numid=301 36 # mic input gain (36=max)
amixer -c0 cset numid=295 46341 # playback_7 output level
amixer -c0 cset numid=293 16384 # playback_5 output level
amixer -c0 cset numid=294 16384 # playback_6 output level
amixer -c0 cset numid=308 0 # IN4/capture_4 level (no gain)
amixer -c0 cset numid=307 0 # IN3/capture_3 level (no gain)
amixer -c0 cset numid=302 1 # AN1/capture_1 mic input 48V on
amixer -c0 cset numid=305 0 # AN2/capture_2 mic input 48V off
amixer -c0 cset numid=289 16384 # AN1/playback_1 output level
amixer -c0 cset numid=290 16384 # AN2/playback_2 output level

`ship.sh` verifies the hashes on the rig and prints them — read that output.
- Confirm the interface clock and mixer baseline match the rig profile
(`preflight.sh` checks the profile's ALSA values) and record the clock
source and why.
- Record what is physically connected — every leg, reference and
measurement, by output/input index, not by what a handoff document says it should be. Let operator know what is your idea of the outputs/inputs today.
- Stop the daemon before installing a build over it. `install -m 755` over a
running `ac-daemon` may fail `Text file busy`, or may succeed and leave an
ambiguous state — see `$AC_HOME/rig-verify-queue.md` for whether this has
been settled on the current build. Stop first regardless of the answer.
ambiguous state — `ship.sh --install` stops it first. Stop first regardless.

### step 2 — obtain emission consent
No drive/emission proceeds without **explicit per-run operator consent**,
Expand All @@ -94,6 +79,7 @@ obtained before this session's first stimulus command — `set_drive on`,
`sweep_frequency`, or `calibrate` all put a signal on a physical output
(#360 closed the gap where `plot_ir` and `calibrate` did not honour
`drive_max_dbfs`; do not read this list as still narrower than the code).
The emitting scripts take the consent as `--consent "<text>"` and print it.
See hard constraints below for the ceiling and its exception mechanism.
Record what was consented to (ceiling, duration if bounded) in the
resulting file.
Expand All @@ -112,14 +98,15 @@ Execute the queued block(s) or ad-hoc procedure as directed. For each run:
record that plainly rather than omitting the block.

### step 4 — write the record
Write to `work/rig/{session-name}-results.md` (see "where records live"
below for whether that's a new file). Required content:
Write to `work/rig/{session-name}-results.md`, starting from
`scripts/rig/record-template.md` (see "where records live" below for whether
that's a new file). Required content:

- **build under test** — sha256-verified, git ref if known.
- **drive level** — what was consented to, and its provenance (standing
−40 dBFS ceiling, or a recorded exception — see hard constraints).
- **what is physically connected** — every leg, confirmed this session.
- **clock state** — `AutoSync`, and the reason, restated even when
- **clock state** — the clock source, and the reason, restated even when
unchanged from a previous session (this file is read independently of
that one).
- **per-run results** — what was verified, what a pass looked like, what
Expand Down Expand Up @@ -171,21 +158,22 @@ Interlocks. A session may not proceed past these — not guidance, blocking:
in-session. This role produces evidence, not patches.
- **No automated enforcement of any of the above.** These interlocks are
not machine-checked in `ac-daemon` or `ac-cli` — enforcing them there is
explicitly out of scope for this role. Reading this file is what
enforces it; know that going in.
explicitly out of scope for this role. The `scripts/rig/` consent and
level checks are conveniences, not the interlock. Reading this file is
what enforces it; know that going in.

## where records live

`work/rig/` holds two different kinds of file, with different expiry:
`work/rig/` holds the session-result files:

- **Session-result files** (`rig-session-N-results.md`,
`rig-verify-NNN-results.md`, and similar) are historical evidence and do
**not** expire. A later session that supersedes an earlier expectation
says so in prose against the earlier finding — `rig-verify-queue.md`'s
own "session 3 supersedes" note is the pattern — rather than deleting or
rewriting the earlier file.
- **`rig-verify-queue.md`** is a live queue, not historical evidence. It
expires per item: each queued block gets marked executed, with a pointer
to the session-result file that ran it, as soon as that happens (the
existing "Executed, session N" annotations on several blocks are the
pattern to follow). An item with no such annotation is still open.
- **`$AC_HOME/rig-verify-queue.md`** is a live queue, not historical
evidence. It expires per item: each queued block gets marked executed,
with a pointer to the session-result file that ran it, as soon as that
happens (the existing "Executed, session N" annotations on several blocks
are the pattern to follow). An item with no such annotation is still open.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ cargo build --release
Toolchain is pinned to Rust 1.95.0 via `rust-toolchain.toml`.

`ac monitor` and `ac transfer` launch `ac-view`, which must be on `$PATH`
or in the same directory as `ac`. Note that `install.sh` currently
installs only `ac` and `ac-daemon` — install `ac-view` alongside them if
you want the GUI outside a dev tree.
or in the same directory as `ac`. `ac-rs/install.sh` builds for the host
CPU and installs `ac`, `ac-daemon` and `ac-view` into `/usr/local/bin`. To
put a build on a test rig instead, see `docs/runbooks/rig-testing.md`.

## Audio backend

Expand Down
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ mtime: the convenient reading is not the evidence, and it fails quietly.

Among the `#[ignore]`'d, `it_loopback_ir` drives a Farina sweep through real
JACK port-to-port loopback and is run manually after starting `jackd -d dummy`
— see ARCHITECTURE.md → "Loopback IR runbook". The rest need real hardware, a
— see `docs/runbooks/rig-testing.md` (7b; 7c for real hardware). The rest need real hardware, a
live daemon, or a real GPU adapter.

No JACK daemon or audio hardware required for the default suite — pytest spawns `ac-daemon --fake-audio` (synthetic sine + 1% 2nd harmonic) on free ports and connects via ZMQ.
Expand Down
9 changes: 4 additions & 5 deletions ac-rs/crates/ac-daemon/examples/ir_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
//! ir_probe --level-dbfs -30 --duration 2.0 --f2 16000 --window 16384
//! ```
//!
//! `--level-dbfs` is required. `plot_ir` does not apply the config's
//! `drive_max_dbfs` ceiling — only `set_drive` does — so the value passed
//! here is the only limit on what reaches the interface.
//! `--level-dbfs` is required, so the record names the level requested.
//! `plot_ir` also clamps it to the running daemon's `drive_max_dbfs` (#360).

use std::time::{Duration, Instant};

Expand Down Expand Up @@ -84,8 +83,8 @@ fn parse_args() -> Args {
}
assert!(
a.level_dbfs.is_finite(),
"--level-dbfs is required: plot_ir does not apply drive_max_dbfs, so \
this value is the only ceiling on what reaches the interface"
"--level-dbfs is required: name the level for the record (the daemon \
still clamps it to its drive_max_dbfs)"
);
a
}
Expand Down
21 changes: 11 additions & 10 deletions ac-rs/crates/ac-daemon/tests/it_loopback_ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! pre-impulse floor.
//!
//! This test is `#[ignore]`'d so it does not run as part of `cargo test`.
//! It needs a live JACK server. See `ARCHITECTURE.md` → "Testing strategy"
//! → "Loopback IR runbook" for invocation.
//! It needs a live JACK server. See `docs/runbooks/rig-testing.md` (7b, and
//! 7c for real hardware) for invocation.
//!
//! The internal loopback works because both the daemon's output and input
//! ports are registered under the same JACK client (`ac-daemon`). Setting
Expand All @@ -22,12 +22,13 @@
//! hardware, set both of these to real JACK port names:
//!
//! ```text
//! AC_LOOPBACK_OUT="Babyface Pro Pro:playback_2" # daemon's out connects here
//! AC_LOOPBACK_IN="Babyface Pro Pro:capture_4" # daemon's in connects here
//! AC_LOOPBACK_OUT="system:playback_2" # daemon's out connects here (pupu: AN2)
//! AC_LOOPBACK_IN="system:capture_2" # daemon's in connects here (pupu: IN2)
//! ```
//!
//! Unset, both default to the self-loop, so the `jackd -d dummy` runbook in
//! `ARCHITECTURE.md` is unchanged.
//! Unset, both default to the self-loop, so the `jackd -d dummy` route is
//! unchanged. On a rig, `scripts/rig/run-loopback-ir.sh` sets all three from
//! the rig profile.
//!
//! Setting them puts a stimulus on physical outputs, which is behind the
//! rig's standing drive-level policy. So when `AC_LOOPBACK_OUT` is set,
Expand Down Expand Up @@ -98,7 +99,7 @@ const RIG_DRIVE_CEILING_DBFS: f64 = -40.0;
/// being placed by `MAX_ROUND_TRIP_S`, and a peak pinned at that edge —
/// exactly what a too-short window produces — passes the position check
/// as if it were a plausible round trip. 2.0 s (the reference rig's own
/// runbook duration, ARCHITECTURE.md's "Loopback IR runbook") fixes this,
/// runbook duration, `docs/runbooks/rig-testing.md`) fixes this,
/// but the two runnable sample rates hit different limits and so land at
/// different margins: at 48 kHz the harmonic gap is still the smaller
/// number (11536 samples), giving a half-window of ~120 ms — 2.00×
Expand All @@ -112,7 +113,7 @@ const DEFAULT_DURATION_S: f64 = 2.0;

/// Maximum acceptable round-trip latency, in seconds. #277 measured
/// 43.75 ms on the reference rig (Babyface Pro leg, 96 kHz, 2.0 s sweep —
/// ARCHITECTURE.md's "Loopback IR runbook"). This is that figure with
/// `docs/runbooks/rig-testing.md`). This is that figure with
/// ~37% headroom for rig-to-rig jitter, not a bound fitted to one run.
/// If a chain ever needs more than this, the number moves and this
/// comment's citation moves with it — it must never grow silently.
Expand Down Expand Up @@ -489,7 +490,7 @@ impl Client {
}

#[test]
#[ignore = "needs a live JACK server — see ARCHITECTURE.md"]
#[ignore = "needs a live JACK server — see docs/runbooks/rig-testing.md"]
fn loopback_ir_recovers_sharp_peak() {
let routing = Routing::from_env();
let chain = routing.describe();
Expand Down Expand Up @@ -861,7 +862,7 @@ mod round_trip_bound_tests {
/// `DEFAULT_DURATION_S` must genuinely bind `MAX_ROUND_TRIP_S` at the
/// sample rates this repo can actually exercise the loopback test at:
/// `jackd -d dummy` self-loop configs (48 kHz) and the rig's own
/// Babyface Pro leg (96 kHz — ARCHITECTURE.md's "Loopback IR runbook").
/// Babyface Pro leg (96 kHz — `docs/runbooks/rig-testing.md`).
/// Regression guard for #361's acceptance criterion: `hi_bound` must
/// not saturate at any configuration that actually runs.
#[test]
Expand Down
Loading