Skip to content

fix: real-port loopback_ir daemon config carries the rig's -40 dBFS ceiling - #445

Merged
mkovero merged 1 commit into
mainfrom
issue-442-clamp-loopback-ir-real-ports
Sep 14, 2026
Merged

mkovero merged 1 commit into
mainfrom
issue-442-clamp-loopback-ir-real-ports

Conversation

@mkovero

@mkovero mkovero commented Sep 14, 2026

Copy link
Copy Markdown
Owner

closes #442

what changed

it_loopback_ir's real-port route (AC_LOOPBACK_OUT/IN set) spawns its daemon under an isolated HOME, and the config it wrote there had no drive_max_dbfs — so that daemon fell back to ac-core's -10 dBFS default, and the only thing holding a sweep to the rig's standing -40 dBFS ceiling (.agents/rig.md → hard constraints) was the request, AC_LOOPBACK_LEVEL_DBFS. This implements the architect's option A: a fixed RIG_DRIVE_CEILING_DBFS = -40.0 constant in the test, written into the config only on the real-port route (Some in Routing; None on the self-loop, which keeps its config byte-for-byte what it was).

Routing::from_env is split into a pure Routing::new (four raw optional values, no std::env calls) plus a thin from_env wrapper, so the harm-case test can build a real-port Routing without touching process-global env state other parallel tests read. write_loopback_config is split into build_loopback_config (returns the serde_json::Value) and a writer, and Daemon::spawn_jack composes the two — the harm test uses the same build_loopback_config the production path uses, so it cannot pass for the wrong reason.

files touched

docs/runbooks/rig-testing.md and scripts/rig/run-loopback-ir.sh were on the architect's file manifest for the case #441 (rig-testing-runbook, open, unmerged) lands first — it hasn't, and those files don't exist on main yet, so there is nothing to touch there. Left a comment on #441 naming the passages it will need to correct once it rebases past this PR (see "related").

test output

cargo test -p ac-daemon --test it_loopback_ir
running 7 tests
test loopback_ir_recovers_sharp_peak ... ignored, needs a live JACK server — see ARCHITECTURE.md
test round_trip_bound_tests::accepts_the_rigs_measured_round_trip ... ok
test round_trip_bound_tests::default_duration_binds_max_round_trip_at_runnable_sample_rates ... ok
test round_trip_bound_tests::refuses_peak_pinned_at_far_edge_even_in_a_binding_window ... ok
test round_trip_bound_tests::refuses_window_too_short_to_hold_max_round_trip ... ok
test self_loop_config_carries_no_drive_max_dbfs_key ... ok
test real_port_config_clamps_requested_level_to_rig_ceiling ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

cargo test -p ac-daemon (full suite): 177 unit + 156 integration passed, 0 failed, 4 ignored

Harm-case test verified red on all three required mutations (reverted after each, not committed):

  1. Delete the drive_max_dbfs key from build_loopback_config's output → sync reply -10.0 vs expected -40.0, assertion fails.
  2. Lower RIG_DRIVE_CEILING_DBFS to -30.0 → sync reply -30.0 vs the assertion's literal -40.0, fails (assertion deliberately does not reference the constant, so raising/lowering it cannot carry the assertion along and stay green).
  3. Write the ceiling key on the self-loop branch only (if routing.drive_max_dbfs.is_none()) → real-port config again has no key, sync reply -10.0, fails.

ZMQ schema changed

no

new dependencies

none

related

open questions for reviewer

None — the architect's option A was unambiguous and the three-mutation check confirms the harm-case test actually exercises it.

🤖 Generated with Claude Code

…eiling

it_loopback_ir's real-port route wrote a config with no drive_max_dbfs,
so its daemon fell back to the -10 dBFS default and the only thing
holding a sweep to the rig's standing -40 dBFS ceiling was the request
(AC_LOOPBACK_LEVEL_DBFS). Per .agents/rig.md hard constraints, a
request-side-only limit is not the interlock.

The real-port route now writes drive_max_dbfs fixed at -40.0
(RIG_DRIVE_CEILING_DBFS) into the config it builds for its own daemon.
The self-loop route is unchanged (no such key).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mkovero

mkovero commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Sequencing note (architect's comment on #442): #441 (open, unmerged `rig-testing-runbook` branch) states the pre-#442 gap as current fact in three places. Left the details on #441 (#441 (comment)). If #441 merges before this one, this branch will need a rebase to pick up that correction rather than reintroducing the stale claim.

@mkovero

mkovero commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

spec coverage

criterion provenance covered notes
real-port daemon's config carries drive_max_dbfs not above rig's −40 dBFS ceiling assumed (operator policy from .agents/rig.md) Value quoted from .agents/rig.md:144 ("Emission ceiling is −40 dBFS, standing") matches RIG_DRIVE_CEILING_DBFS exactly. Doc is human-authored (git log: all commits by Markus Kovero) predating this issue, so this is policy the PR implements literally, not a number the PR is asserting on its own authority — no separating measurement applies to a policy constant.
harm-case test: real ports + AC_LOOPBACK_LEVEL_DBFS above ceiling → level reaching engine ≤ ceiling n/a (behavioral) real_port_config_clamps_requested_level_to_rig_ceiling requests 0.0 dBFS, asserts sync reply and report.stimulus.level_dbfs both == −40.0, both ≠ 0.0. Traced apply_drive_ceiling (handlers/mod.rs:220, f64::min) and plot.rs:646 — clamp is real, not a fake-backend artifact.
harm-case test must fail when key removed or ceiling raised n/a (behavioral) Verified independently, not just read: I removed the drive_max_dbfs write in build_loopback_config and reran the test — failed exactly as the PR body's mutation 1 claims (-10.0 vs expected -40.0). Restored, reran clean. Did not personally reproduce mutations 2/3 but they follow the same code path and the PR body documents them with matching before/after values.
self-loop route unchanged, existing tests pass n/a self_loop_config_carries_no_drive_max_dbfs_key is a key-presence guard (PR is honest that this alone doesn't prove the harm case — correct, that's what the test above is for). Full cargo test --workspace run clean (see below).
no silent fallback when ceiling source is missing assumed Fixed const, Some(RIG_DRIVE_CEILING_DBFS) unconditionally on the real-port branch of Routing::new — cannot be absent by construction, not merely "usually set."

standards conformance

standards check: not applicable — scope-none (issue #442 label; daemon/test-only change, no standard governs).

correctness issues

none found.

test coverage gaps

coverage is adequate. loopback_ir_recovers_sharp_peak's new applied-vs-ceiling assertion is correctly commented as a record guard, not the interlock — the PR doesn't overclaim what it proves.

suggested test additions

none.

scope issues

none. Files touched match the architect's manifest minus docs/runbooks/rig-testing.md and scripts/rig/run-loopback-ir.sh, both absent on main (justified in PR body — #441 hasn't merged) — verified git log --follow finds no such paths on main at this tip's base. Sequencing comment confirmed present on #441 (checked via gh issue view 441, matches PR body's claim word-for-word on what #441 needs to correct on rebase).

verdict

approve
cargo test --workspace clean (0 failed on a full non-truncated run), cargo clippy --workspace --all-targets -- -D warnings clean, cargo fmt --check clean, all five acceptance criteria covered and independently verified (including reproducing harm-case mutation 1 myself), no wire-schema or cross-crate impact, self-loop route provably untouched.

sent back to

no

rig verification required

no — the −40 dBFS value is a fixed constant asserted against a --fake-audio daemon; nothing here depends on the rig's actual measured behavior. (Note for the record, not a gate on this PR: the PR itself flags that pupu's speaker route −50 dBFS ceiling remains request-side-only, tracked as the architect's option-B follow-up, out of scope for #442.)

@mkovero

mkovero commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

rig-2026-09-14-pr445-loopback-ir-ceiling — rig record

Date (UTC): 2026-09-14 21:33–21:41 · Rig: pupu · Operator: Markus Kovero · Run by: Claude Code session
Verifies PR #445 (closes #442) on real ports: with AC_LOOPBACK_OUT/IN naming the reference loopback and
AC_LOOPBACK_LEVEL_DBFS set above the rig ceiling, the level reaching the engine is the ceiling, not the request.

Build under test

rev 799d7d8 (dirty 0, compiled_this_run=yes), rustflags -C target-cpu=x86-64
-C link-arg=-fuse-ld=mold, staged /home/mui/ac-test/799d7d8bba84-x86_64, sha256 verified on rig; it_loopback_ir
daemon path linked to the staged daemon. Not installed.
5459e10d…e5498 ac · f721a552…246b7 ac-daemon · c164cc5e…cc71 it_loopback_ir
Control build for one run: 28a051a (PR #437 tip before its merge), staged and sha256-verified earlier the same day.

Pre-flight

First attempt FAILED: JACK rate/period "none", port order empty. jackd was running (no restart since 18:38 EEST)
and listening on /dev/shm/jack_default_1000_0, but that path had been unlinked. mui has Linger=no and logind
RemoveIPC is at its default, so when the last login session closed, logind removed the socket file. Fixed for this
session: held an ssh session open, sudo systemctl restart jack-ac.service. Preflight then passed all checks
(96000/256/-S, analog capture block 1-8, ALSA rows, staged build, 0 xruns).

Physically connected

AN2 (system:playback_2) → cable → IN2 (system:capture_2); IN1 measurement mic. Cabling unchanged since the
19:49Z probe; port order re-verified silently by the scripts before every emission.

Clock state

FF400 internal clock, snd_fireface, jackd -S 96 kHz period 256.

Emission consent

Operator, 2026-09-14: "go ahead with the rig run after QA", and independent rig work. Loopback AN2→IN2 only; 2 s
Farina sweeps; −60 dBFS 1 kHz probes. Ceiling: standing −40 dBFS. The run under test is the server-side clamp
itself: drive_max_dbfs -40.0 in the config it_loopback_ir writes for its isolated-HOME daemon. Run B requested
−30 on purpose, with only the loopback cable on that output.

Runs

run build request test's level line IR peak_abs IR SNR IN2 independent capture (jack_rec)
A 799d7d8 −40 via run-loopback-ir.sh requested -40.0 dBFS → applied -40.0 dBFS (ceiling -40.0 dBFS) 2.619660 32.82 dB not captured
B (harm) 799d7d8 −30 (manual form; script refuses above ceiling) requested -30.0 dBFS → applied -40.0 dBFS (ceiling -40.0 dBFS) 2.619629 32.82 dB peak −30.8 dBFS; sweep 2.01 s, RMS −34.6
C (control) 799d7d8 −40, same method as B requested -40.0 dBFS → applied -40.0 dBFS 2.619649 32.83 dB peak −30.8 dBFS
D (control) 28a051a −40, same method (older build: no ceiling key, no level line) 2.619639 32.82 dB peak −30.8 dBFS

All four: test result: ok, exit 0, peak_offset +1711 samples = +17.8229 ms from centre.
Record header on the real-port route now reads (external ports, drive_max_dbfs -40.0 dBFS).

Pass stated before running: B's log says applied −40 against a −30 request, and B's IN2 level matches a −40 request
rather than sitting 10 dB above it.

Result — PASS

B requested −30 and the daemon applied −40. B's independent IN2 capture (peak −30.8 dBFS) is identical to C's
at −40, and B's IR magnitude matches C's to 1e-5. A clamp failure would have put B 10 dB above C on both. The
interlock #442 asks for is in the daemon config on the real-port route, and a hardware run exercises it.

Absolute level — explained, not a defect

IN2 read −30.8 dBFS peak for a −40 sweep. The same rig had read about −37 earlier the same day. Discriminated in order:

  • Not code. D, on the older 28a051a build, reads the same −30.8. The diff between the builds does not touch
    stimulus amplitude.
  • Rig gain, changed by the JACK restart. A −60 dBFS 1 kHz probe read IN2 −51.5 dBFS tone after the restart,
    against −57.5 at 19:49Z, i.e. +6.0 dB. IN1's mic channel read a dead −106 dBFS where room noise had been −74. ALSA
    readback (amixer cget) still returned the baseline (93=2 94=2 89=2 81=20,0 90=on,off), so preflight passed.
  • The documented restore (docs/rigs/pupu.md, same-value cset) changed nothing. Writing a different value first,
    then the baseline, restored it: the probe re-read IN2 −57.5 dBFS and IN1 room noise at −73.2 dBFS RMS.

Runs A–D therefore ran with the FF400 at driver defaults: about +6 dB loopback gain, phantom off. The pass rests on
the B-vs-C comparison, which is unaffected: same gain, same method, back to back.

Confound

  • Absolute IN2 levels in A–D are +6 dB against the documented baseline (above). Relative B/C/D comparisons hold.
  • jack_rec peak is a peak, not a sweep-envelope fit. RMS over the 2.01 s active window (−34.6 dBFS) agrees with it.
  • Speaker route (--route speaker, −50 dBFS script-only ceiling) not tested. PR fix: real-port loopback_ir daemon config carries the rig's -40 dBFS ceiling #445 states it does not fix that gap.

Rig state left behind

FF400 baseline forced back and verified by emission (IN2 −57.5 dBFS at −60, mic alive). jack-ac.service restarted
2026-09-15 00:3x EEST, running 96000/256/-S. No ac-daemon. Rig ~/.config/ac/config.json untouched. A held ssh
session keeps logind from removing the JACK socket until it exits. After that, the socket can vanish again on
last logout unless lingering is enabled or RemoveIPC=no is set. That is host config and the operator's call.

What should happen next

  1. fix: real-port loopback_ir daemon config carries the rig's -40 dBFS ceiling #445: rig evidence exists. Merge is the human gate. codex-approved is not yet on it.
  2. pupu host: loginctl enable-linger mui or RemoveIPC=no, so an idle rig keeps its JACK socket.
  3. docs/rigs/pupu.md on docs(rig): one runbook and scripts for testing binaries on a rig #441: "JACK restarts … do not [reset the FF400]" is contradicted by this run, and the
    restore block needs toggle writes (see the comment on docs(rig): one runbook and scripts for testing binaries on a rig #441).

@mkovero

mkovero commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

codex qa — PR #445 at 799d7d8

verdict: pass

All five acceptance criteria are covered. The real-port route unconditionally writes the repository policy ceiling of −40 dBFS into the isolated daemon config; the self-loop config remains unchanged; and the non-ignored harm test exercises the same config builder used by the JACK route. The literal −40 assertions would fail if the key were removed (falling back to −10), the ceiling were raised, or the key were written only on the self-loop branch.

The −40 dBFS criterion is tagged assumed, but it is a standing operator-policy bound stated in the human-authored .agents/rig.md, not an inferred physical threshold. No measurement can choose the consent policy between plausible alternatives; the relevant mechanism measurement is whether the configured clamp actually governs physical output. The later rig record on this PR supplies that evidence: a −30 dBFS request was applied at −40 dBFS, and its independent capture matched the −40 dBFS control.

gate

Claude QA workspace gate: inherited at current tip 799d7d8bba84fcff6d4780d9b5945246c3092d55
Codex targeted tests:

  • cargo test -p ac-daemon --test it_loopback_ir real_port_config_clamps_requested_level_to_rig_ceiling — pass (1 passed)
  • cargo test -p ac-daemon --test it_loopback_ir self_loop_config_carries_no_drive_max_dbfs_key — pass (1 passed)

unaddressed open questions

none

scope

none — the three touched files are justified by the architect manifest and the current base; the two other manifested paths belong to the still-unmerged #441 and do not exist on main.

@mkovero
mkovero merged commit 6feef79 into main Sep 14, 2026
mkovero added a commit that referenced this pull request Sep 14, 2026
Brings in #445 (#442: it_loopback_ir's real-port daemon config carries
drive_max_dbfs -40) and #388. Conflicts resolved:
- it_loopback_ir.rs: take main's module doc and expect message, which
  describe the now-closed isolated-HOME ceiling gap correctly; this
  branch's doc-pointer edits (rig-testing.md, pupu port examples) kept.
- docs/runbooks/loopback-ir.md: keep this branch's deletion; its #445
  paragraph is carried into rig-testing.md step 7c.

Corrects the passages #442's architect note named for this branch:
- rig-testing.md steps 5 and 7c, run-loopback-ir.sh header: the test's
  isolated-HOME daemon now clamps at -40 dBFS; the script's --level check
  is a convenience, and the only enforcement of the speaker ceiling.

Adds rig findings from 2026-09-14/15 on pupu:
- docs/rigs/pupu.md: a jack-ac.service restart reset the FF400 to driver
  defaults while ALSA readback kept the baseline; the same-value restore
  did nothing, toggle writes restored it, verified by a -60 dBFS probe.
- rig-testing.md step 3: preflight's ALSA rows read the driver cache, not
  the interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuCG3RuLHArnC8snRZVDYG
mkovero added a commit that referenced this pull request Sep 14, 2026
Brings in PR #388 (#369: per-lifecycle xrun refusal, refused_xrun) and
PR #445. Conflicts in ZMQ.md, ac-cli calibrate.rs, fake/hooks.rs,
fake/mod.rs, calibrate/tau/mod.rs and it_protocol/calibrate/tau.rs were
resolved keeping both refusals whole: every field, hook, tau_state value,
frame key, CLI rendering and test from both sides.

Precedence (#368/#369 merge decision, for review): a lifecycle whose
capture crossed an xrun skips its own SNR gate, and tau_result routes the
run to refused_xrun before the comparison, so an xrun-contaminated capture
is never reported as merely low-SNR. A clean lifecycle keeps #368's order
(SNR gate before the edge-margin check). New test
calibrate_reports_refused_xrun_over_low_snr_when_both_conditions_hold pins
it.

The developer session that resolved this merge was stopped by the host
before it could commit; the resolution was committed as it stood after
cargo fmt --check, cargo clippy -- -D warnings and cargo test --workspace
passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuCG3RuLHArnC8snRZVDYG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

it_loopback_ir's daemon clamps at the -10 dBFS default, not the rig ceiling, when driving real ports

1 participant