Skip to content

Reverb behind the canceller: reverb_mix stage, Dattorro ASG vs wet/decay, dry-only-shift variant, dattorro_paper cost table (phase 2 item 7, part 1) - #83

Merged
tap merged 1 commit into
mainfrom
feat/reverb-stage
Oct 6, 2026

Conversation

@tap

@tap tap commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Phase 2 item 7, part 1: the reverb behind the canceller, measured. The spectral reverb returns on this harness in part 2 (HANDOFF item 11).

What lands

  • include/mutap/reverb_stage.h: tap::mu::reverb_mix<Sample, Reverb> (an afc_stage for afc_chain's reverb slot: y = (1 − w)·x + w·r, r = the reverb's L or (L + R)/2) and tap::mu::shifted_dry_mix<Sample, Shifter, Reverb> (y = (1 − w)·shift(x) + w·r(x), the reverb fed the unshifted bus). Any reverb with the FAUST shim's process(const Sample* const*, Sample* const*, int) noexcept; set_wet() / set_return_mode() noexcept; latency() 0; header-only, holds no FFT, no ABI tag; allocates its return buffers at construction only. FAUST stays in third_party/faust/. Added to mutap.h.
  • tests/test_reverb_mix.cpp: float/double plumbing with a stand-in reverb (formula, both returns, any block length, in place, dry-only topology, validation, RT contract). Both emulated selections run its float half (reverb_mix_test/0.*:ReverbMixConfigValidation.*:ReverbMixRtContract.*, 7 tests; the bare-metal count comment is now 108). FAUST is not compiled on the emulated targets (it was not before either).
  • Harness: decorrelated_loop gains a forward stage (the chain's reverb slot, reset per probe) — bit-identical without one: the karaoke measurement (cabin and mt5, held and speech, with and without the 5 Hz shift, seed 2, 5 s) gives the same limits to 17 digits on origin/main's and this branch's loop. tests/support/reverb_rig.h (plates, rigs, T30, mix level, phase 0's bare-loop analysis). karaoke_ramp_dump --reverb shipped|paper --wet --decay --damping --return --topology bus|dry --lib-shift --reverb-ir-only, 4th WAV channel = the chain output. tools/notebook/reverb_audible.py (PROTOCOL §7.3 flags; --control; per-run records; WAVs deleted after analysis; --recover).
  • Gated host rows tests/test_reverb_stage.cpp; MUTAP_SLOW sweep tests/test_reverb_stage_sweep.cpp (ProbeConvergence, Grid, Shift, BareLoopCost).
  • docs/reverb-afc.md, HANDOFF item 13 (item 11 now points at part 2), README component bullet, karaoke doc and FAUST README pointers.

Every number below: macOS 15.7.9 x86_64 (i9-8950HK), AppleClang 17.0.0, Release, double in the loop, five seed sets (2 22 42 62 82), shared machine. No second host ran the sweep or the audible driver.

Probe convergence (cabin, mt5; S1; wet 0.30; median ASG, dB)

Room, material Row 10 s 20 s 40 s 80 s 160 s Converged
cabin, held canceller +11.66 +11.74 +11.74 +11.74 10 s
cabin, held shipped 0.5 +10.98 +11.05 +11.05 +11.05 +11.05 10 s
cabin, held shipped 0.7 +10.98 +11.05 +11.05 +11.05 +11.05 10 s
cabin, held shipped 0.85 +13.71 +13.79 +13.79 +13.79 +13.79 10 s
cabin, held paper 0.5 +11.17 +11.25 +11.25 +11.25 +11.25 10 s
cabin, held 0.7 + 5 Hz whole bus +18.59 +18.67 +18.67 +18.67 10 s
cabin, held 0.7 + 5 Hz dry only +17.03 +17.11 +17.11 +17.11 10 s
mt5, held canceller +11.05 +11.05 +11.05 +11.05 10 s
mt5, held shipped 0.5 +11.56 +11.64 +11.64 +11.64 +11.64 10 s
mt5, held shipped 0.7 +12.64 +12.71 +12.71 +12.71 +12.71 10 s
mt5, held shipped 0.85 +12.44 +12.52 +12.52 +12.52 +12.52 10 s
mt5, held paper 0.5 +11.35 +11.35 +11.35 +11.35 +11.35 10 s
mt5, held 0.7 + 5 Hz whole bus +18.50 +18.57 +18.57 +18.57 10 s
mt5, held 0.7 + 5 Hz dry only +17.79 +17.79 +17.79 +17.79 10 s
cabin, speech canceller +21.02 +20.82 +20.23 +19.75 not converged
cabin, speech shipped 0.5 +21.70 +21.80 +21.31 +21.31 +20.82 not converged
cabin, speech shipped 0.7 +21.41 +21.60 +21.21 +20.72 +20.82 80 s
cabin, speech shipped 0.85 +20.53 +20.53 +20.33 +20.04 +19.94 80 s
mt5, speech canceller +23.26 +23.16 +22.87 +22.68 40 s
mt5, speech shipped 0.5 +24.53 +24.73 +24.14 +24.24 +24.14 40 s
mt5, speech shipped 0.7 +24.73 +24.73 +23.95 +24.14 +23.85 not converged
mt5, speech shipped 0.85 +24.04 +23.65 +23.65 +23.55 +23.36 80 s

Held note: every row converged at 10 s, decay 0.85 included (20–160 s identical, seed by seed). Speech: 3 of 8 rows did not converge by their longest probe; no speech row is gated. Grid and shift rows ran the held note on 20 s probes and speech on 80 s; the gated rows on 10 s.

Runaway ASG and cost behind the canceller (Grid; six rooms, both generator families)

Median over rooms of each room's five-seed median; cost = canceller-alone − with-plate (positive = the plate costs); [room range] {per-seed range}; matched = cost − mix level.

Held note, S1 (canceller alone +11.74 [+11.05, +12.42]):

Plate ASG Cost [rooms] {seeds} Mix level Matched
shipped 0.5, w 0.15 +10.98 +0.49 [−0.39, +2.05] {−4.00, +5.76} −0.96 +1.44
shipped 0.5, w 0.30 +11.46 +0.88 [−0.78, +2.44] {−4.30, +4.20} −1.69 +2.57
shipped 0.5, w 0.50 +12.62 −1.07 [−1.66, −0.39] {−4.30, +2.15} −2.03 +0.96
shipped 0.7, w 0.15 +10.68 +1.46 [−0.20, +1.76] {−3.91, +3.32} −0.93 +2.39
shipped 0.7, w 0.30 +11.45 +0.39 [−0.29, +0.68] {−4.00, +3.81} −1.55 +1.94
shipped 0.7, w 0.50 +14.49 −2.15 [−3.91, −1.86] {−4.98, +0.00} −1.62 −0.53
shipped 0.85, w 0.15 +12.03 −0.20 [−1.17, +2.34] {−4.30, +3.42} −0.83 +0.64
shipped 0.85, w 0.30 +13.32 −1.46 [−2.05, −0.49] {−6.35, +3.03} −1.13 −0.33
shipped 0.85, w 0.50 +16.33 −4.39 [−6.15, −3.52] {−8.50, −1.27} −0.55 −3.85
paper 0.5, w 0.15 +11.35 +0.59 [+0.10, +1.46] {−4.30, +3.61} −0.96 +1.54
paper 0.5, w 0.30 +11.35 +0.29 [−0.29, +1.46] {−4.79, +3.71} −1.69 +1.98
paper 0.5, w 0.50 +13.79 −1.56 [−2.73, −1.07] {−6.35, +0.29} −2.03 +0.47
shipped 0.5, w 0.30, damping 0.5 +11.84 +0.39 [−0.78, +0.59] {−3.52, +3.91} −1.71 +2.10
shipped 0.5, w 0.30, (L+R)/2 +11.54 +0.29 [−0.78, +1.07] {−4.30, +5.08} −1.70 +2.00
shipped 0.7, w 0.30, damping 0.5 +11.54 +0.29 [−1.07, +0.49] {−4.20, +3.42} −1.67 +1.96
shipped 0.7, w 0.30, (L+R)/2 +11.46 +1.07 [−0.29, +1.56] {−5.86, +3.61} −1.62 +2.69
shipped 0.85, w 0.30, damping 0.5 +13.20 −1.37 [−2.15, +0.49] {−6.05, +2.73} −1.58 +0.21
shipped 0.85, w 0.30, (L+R)/2 +12.81 −1.17 [−3.12, −0.49] {−5.66, +1.27} −1.32 +0.15

Held note, S3 (canceller alone +18.40 [+17.25, +18.59]): every raw cost ≤ −0.10 dB (largest matched +1.62); full table in the doc. Speech, S1, 80 s (canceller +19.75): shipped w 0.30 at decay 0.5 / 0.7 / 0.85 costs −1.17 / −0.88 / −0.29 (matched +0.51 / +0.67 / +0.84).

Did not separate: damping 0.5 vs 0.0005, (L+R)/2 vs L, paper vs shipped at decay 0.5 — every difference (≤ 1.07 dB) inside the rows' room ranges.

Shift rows (runaway, bisected; six rooms; held note, 20 s; shipped plate, w 0.30)

S1 2 Hz S1 5 Hz S3 2 Hz S3 5 Hz
canceller +11.74 +18.40
shift alone +17.50 +17.23 +19.38 +18.89
whole bus + plate 0.5 +17.52 +19.36 +20.45 +20.33
dry only + plate 0.5 +14.10 +16.91 +18.18 +20.16
whole − dry, 0.5 (rooms < 0) +4.49 (0/6) +1.46 (0/6) +1.95 (0/6) +0.20 (3/6)
whole bus + plate 0.7 +17.99 +18.57 +20.35 +20.25
dry only + plate 0.7 +15.45 +17.11 +19.67 +20.45
whole − dry, 0.7 (rooms < 0) +3.42 (0/6) +0.68 (1/6) +1.17 (1/6) +0.68 (3/6)

Audible rows (cabin, held, ramp + howl criterion on c per PROTOCOL §7.3; dB over exact_msg_db; medians of 5 seeds)

Chain S1 audible S1 runaway (ramp) S3 audible S3 runaway (ramp)
dry +0.18 +1.08 +0.21 +1.62
canceller +16.57 +20.70 +12.37 +21.37
+ 2 Hz † +4.03 (control +5.84 / +6.10) +20.13 +6.50 +18.73
+ 5 Hz † +4.43 (control +5.28 / +5.37) +19.39 +2.18 +19.49
+ plate 0.5, w 0.15 +16.53 +21.46 +10.38 +23.01
+ plate 0.5, w 0.30 +12.89 +22.07 −4.77 ‡ +23.31
+ plate 0.7, w 0.15 +13.53 +20.89 +11.44 +23.18
+ plate 0.7, w 0.30 +10.46 +21.76 +3.62 ‡ +24.19
2 Hz whole / dry + 0.5, w 0.15 † +5.92 / +8.55 +20.71 / +21.31 +6.77 / +7.90 +20.68 / +21.63
5 Hz whole / dry + 0.5, w 0.15 † +6.66 / +6.48 +20.93 / +21.28 +2.95 / +3.53 +21.01 / +22.24
2 Hz whole / dry + 0.5, w 0.30 † +7.53 / +8.51 +21.70 / +22.24 +7.74 / +8.98 +22.62 / +22.62
5 Hz whole / dry + 0.5, w 0.30 † +6.43 / +8.06 +22.12 / +22.66 +5.71 / +7.10 +22.91 / +23.55
2 Hz whole / dry + 0.7, w 0.15 † +6.62 / +8.71 +20.85 / +20.85 +7.50 / +8.68 +18.99 / +22.12
5 Hz whole / dry + 0.7, w 0.15 † +6.93 / +7.13 +20.45 / +21.75 +3.12 / +4.45 +20.26 / +21.12
2 Hz whole / dry + 0.7, w 0.30 † +8.87 / +7.69 +20.47 / +21.47 +6.74 / +6.19 +21.31 / +22.67
5 Hz whole / dry + 0.7, w 0.30 † +5.73 / +10.30 +21.67 / +22.62 +4.63 / +9.46 +22.15 / +24.42

† Worst case for the shift (synthetic held note). Shift-alone "control": c re-analysed with each plate's T30 in the hold, as the product rows are (1.185 / 2.117 s). ‡ Some seeds audible below the dry loop's limit (per seed −13.55 … +1.89 at 0.5); not examined. Dry-only − whole-bus per-seed median > 0 in 13 of 16 pairs (audible) and 16 of 16 (ramp runaway) — the opposite of the bisected runaway at S1. The canceller row is not karaoke-afc.md's +14.96: §7.3 flags (no --dechirp without a shift) vs that doc's --dechirp on every row.

dattorro_paper bare-loop cost (phase 0's method: 4 fixtures at 4096 taps, d = 480, L, median of 4 = mean of middle two; bound / exact, dB; reproduces phase 0's as-shipped table to the digit)

decay (T30 L) damping w 0.15 w 0.30 w 0.50 matched (w 0.30)
0.3 (1.73 s) 0.0005 −0.21 / −0.50 −0.27 / −0.51 −0.20 / −0.41 +1.45
0.5 (2.12 s) 0.0005 +0.08 / −0.31 +0.30 / +0.06 +0.74 / +0.43 +1.99
0.5 0.2 −0.03 / −0.34 +0.09 / −0.01 +0.37 / +0.38 +1.79
0.5 0.5 −0.16 / −0.37 −0.18 / −0.16 −0.00 / +0.15 +1.53
0.7 (3.79 s) 0.0005 +0.76 / +0.38 +1.61 / +1.14 +2.77 / +2.42 +3.16
0.7 0.2 +0.34 / +0.22 +0.80 / +0.87 +1.53 / +1.99 +2.42
0.7 0.5 −0.02 / −0.04 +0.14 / +0.37 +0.98 / +0.97 +1.81
0.85 (8.25 s) 0.0005 +2.30 / +1.43 +4.32 / +4.00 +6.59 / +6.47 +5.45
0.85 0.2 +0.91 / +0.88 +2.78 / +3.11 +4.92 / +5.35 +4.19
0.85 0.5 +0.33 / +0.13 +2.04 / +1.42 +3.92 / +4.12 +3.62

T30 L / (L+R)/2 at decay 0.3 / 0.5 / 0.7 / 0.85: paper 1.7252 / 2.1186 / 3.7882 / 8.2457 s (1.5975 / 2.3626 / 3.8598 / 8.1939); as shipped 1.0144 / 1.1846 / 2.1173 / 4.6220 s. At equal decay the paper plate costs more; at equal T30 (paper 0.5 vs shipped 0.7) less (+0.30 vs +1.11 bound at w 0.30).

Gated (ReverbStage.*, host-only)

  • Mix over both vendored plates (≤ 1e-12), dry-only topology, T30s (±0.01 s) and mix levels, three bare-loop cells (±0.02 dB).
  • CancellerHoldsBehindThePlates (cabin, S1, held, 10 s): median cost < +3.5 dB for shipped 0.5 w 0.15 / 0.30, 0.7, 0.85 and paper 0.5 at w 0.30 (measured −0.28 / +0.56 / +0.28 / −1.69 / +0.00); median ASG > +7 (min measured +10.72).
  • WholeBusShiftHoldsMoreRunawayThanDryOnly (cabin, S1, 2 Hz, decay 0.5, w 0.30): per-seed median whole − dry > 0 (measured +3.09; seeds +3.94 +3.09 +7.59 +0.84 +2.53).

Verification

  • cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DMUTAP_WERROR=ON -DMUTAP_BUILD_KARAOKE_DUMP=ON && cmake --build build: clean.
  • ctest --test-dir build --output-on-failure: 100% tests passed, 0 tests failed out of 396 (3233.67 s; the 3 locally flaky tests passed this run). CancellerHoldsBehindThePlates 133.29 s, WholeBusShiftHoldsMoreRunawayThanDryOnly 63.73 s on 4 threads.
  • git diff --name-only origin/main | xargs pre-commit run --files: Passed. scripts/tidy.sh on the three new TUs: clean; direct clang-tidy-18 -p build-tidy on each: 0 warnings, 0 errors.
  • Fingerprints / icount: no library source the harness or the bench workloads include changed (the only library change is the new header and its mutap.h include, which only tools/capi uses).
  • Not compiled locally with GCC (none on this machine); CI's Linux and M55 legs are the GCC check.

Wall times

Sweep Wall Threads
ProbeConvergence (610 bisections) 22607.7 s 3
Grid (1350) 24684.3 s 3
Shift S1 (420) 17611.7 s 1
Shift S3 (420) 4813.9 s 4
BareLoopCost 53.5 s 1
Audible driver (240 runs + 20 control re-runs) ≈ 5 h 10 min + 786.2 s + 3104.9 s 1 job

The audible driver's first invocation filled the scratch disk (≈ 180 MB of WAV per run) and died after 230 runs; it now deletes WAVs and checkpoints per run, and --recover rebuilt those 230 records (runaway from the gain log, scratch-checked against the WAVs in 230 of 230).

Not done

Speech at S3; the paper plate in the loop at decay 0.7 / 0.85; bisected shift rows at wet 0.15 / 0.50; audible rows beyond the cabin held note and the shipped plate; float32 in the loop; backing track; two mics; the dattorro~ Max external (out of scope).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A

…sured behind the canceller (phase 2 item 7, part 1)

include/mutap/reverb_stage.h: tap::mu::reverb_mix<Sample, Reverb>, an
afc_stage for the chain's reverb slot, y = (1 - w) x + w r with r the
reverb's L or (L + R) / 2 return, and shifted_dry_mix<Sample, Shifter,
Reverb>, the dry-only-shift topology (the reverb fed the unshifted bus).
Any reverb with the FAUST shim's process(in, out, n); header-only, no FFT,
no ABI tag, latency() 0, allocation only at construction. Float plumbing
suite test_reverb_mix.cpp in both emulated selections (7 tests).

Harness: decorrelated_loop gains a forward stage (reset per probe;
bit-identical without one against origin/main on the karaoke
measurement), tests/support/reverb_rig.h, karaoke_ramp_dump --reverb and
tools/notebook/reverb_audible.py (PROTOCOL 7.3 flags, per-run records,
WAVs deleted after analysis, --recover, --control).

Measured (macOS 15.7 x86_64, AppleClang 17, Release, five seed sets;
docs/reverb-afc.md): every held-note row converged at 10 s (decay 0.85
included), the speech envelope did not everywhere by 160 s; behind the
canceller the plate costs at most +1.46 dB of runaway ASG (six rooms,
S1; none at S3) while the audible limit falls with wet and decay; the
dry-only shift is audible later in 13 of 16 pairs, bisected runaway
favours the whole-bus shift; the dattorro_paper bare-loop table and
T30s. Gated host rows in test_reverb_stage.cpp; the MUTAP_SLOW sweep in
test_reverb_stage_sweep.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A
@tap
tap merged commit 94561b3 into main Oct 6, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant