Ladder gate execution ledger: 10 rulings, a bug the spec called a characteristic, and five classes of a self-contradicting comment #84

Closed
opened 2026-09-09 22:26:18 +00:00 by bcox · 1 comment
Owner

Execution record for ladder-gate (35 commits, merged from main at 8422929). The branch added dsp::VaFilterTopology::kLadderGate — the low-pass gate's coupling law driving dsp/ladder.h's four-stage saturating ladder — plus patch format minor 41, editor support, a measured onset characteristic, and the ladder-kick demo patch.

Filed because the SDD ledger is git-ignored and dies with the workspace.

The finding that mattered most was not in the plan

The stage shipped ladder-kick measuring an octave high. The audition verdict was that it rang too high, and the question was "I thought we could get down to 23Hz?"

That was right, and the design doc had written the cause down as an accepted characteristic:

kSvfFrequencyFloor is a normalized phase increment (1u << 21), so it is a fixed fraction of the internal rate — 23.44 Hz at 1x, 93.75 Hz at 4x. kLadderGate forces 4x for its saturator. So the oversampling ratio was moving the filter's reachable floor in hertz, which is the one thing oversampling must never do. VaCutoffLimitForRatio already held the ceiling invariant; the floor had no counterpart, and that helper's own comment flagged the asymmetry as "a finding rather than an omission".

The reason the floor had never been lowered is real and belongs to the SVF alone: a3 goes as g^2 and at 2^19 quantizes to 0.154 of a SvfMulCoeff (Q<0,20>) quantum — zero — leaving the second integrator lossless. Nobody had checked whether that binds the ladder. It does not: the ladder's coefficient is G = g/(1+g), linear in g, holding 402 quanta at the same increment. A factor of 2605.

increment Hz at 192 kHz SVF a3 in Q<0,20> ladder G in Q<0,20>
2^21 93.75 2.47 1606
2^19 23.44 0.154 → 0 402

Fixed by VaCutoffFloorForRatio(ratio) = kSvfFrequencyFloor / ratio, applied at the ladder branch only. The reachable floor is now 23.44 Hz at every ratio rather than 23.44 / 46.88 / 93.75 — it does not go below what 1x already reaches. Go/no-go measured first: at 2^19 the ladder still reaches exact raw zero at every feedback setting (worst case 536,098 samples against 225,421 at 2^21) and full-scale drive peaks at 0.662 against SvfState's ±256, identical to the 2^21 reading.

This also fixed the pre-existing kLadder topology. moog-lead, sh101-bass and tb303-line gained the same headroom; they render bit-identically only because their corners never went that low (minimum ~384 / ~126 / ~96 Hz across MIDI 0-127).

Rulings

Each with what it costs if wrong.

  1. Folded the vactrol-guard task into the DSP task. With only the guard, a kLadderGate slot still fell through to the SVF tail, which never reads opening — no observable effect, no possible test. Cost: a larger diff.
  2. Required forcing kAntiDiagonal and a zeroed morph. The spec asserted it and a later task tested it, but no task implemented it. Cost: a two-line revert.
  3. Moved the format's out-of-range topology byte 5 → 6 into the enumerator's own task. Adding kLadderGate = 5 made 5 real. Cost: none, forced.
  4. Format bump 37 → 39 → 41. Overtaken twice mid-flight: main took 37 (WVGD) and 38 (JUMP), then 39 (GLID) and 40 (the audio bus). Cost: none.
  5. Fixed the loader's six hand-copied enum bounds; left five dsp::LoopMode ones. Filed separately — those need a sentinel added to dsp/loop_mode.h. Cost: a fourth loop mode is silently dropped by any of five sites that is missed.
  6. Corrected minor 41's and the spec's "plays silent" claim. kBypass is pass-through, so an old reader plays the row unfiltered and ungated at full level — a much worse failure on a gate, where the filter is the amplitude contour.
  7. Then reversed course and fixed minor 32's identical claim too. Two contradictory compatibility statements 325 lines apart in the file whose whole purpose is being the compatibility record is worse than reaching into a shipped stage's history entry.
  8. Kept ladder-kick when its low-end bar proved unreachable, documenting the floor as a characteristic. Made moot by the floor fix, which the audition then demanded.
  9. Accepted the band test's re-derivation after the floor change. A measurement test, not a golden; bars re-placed against re-run injections.
  10. Capped the record-correction loop at five rounds rather than continuing. See below for why that was right and also not sufficient.

Two patterns worth keeping

1. Self-contradicting comments cluster in summaries

Ten instances across the branch of a comment or document asserting something the measurements in its own file contradict. Examples:

  • A sentence reading "two octaves under that, so taking it unclamped would put the corner four octaves above the ceiling" — contradicting itself within one line.
  • A test comment claiming it would catch a refactor that demonstrably passed it.
  • A resonance justification refuted by a measurement three paragraphs below it.
  • "no sign of the next squaring buying less than the last", directly above the series 0.30, 0.44, 0.54, 0.57 (deltas +0.14, +0.10, +0.03).
  • A design-doc headline block still stating the pre-fix render while the same document said otherwise eight paragraphs down.
  • An acceptance bar whose reason ("reads no existing field differently") stopped covering the stage when the floor change reached the pre-existing kLadder topology.

They cluster in summaries — headline blocks, acceptance bars, status lines, test-plan bullets. Body prose sitting next to a number gets checked against it. A summary three sections away does not, and stays plausible while the thing it summarizes moves underneath it.

Every review was instructed to recompute rather than read, and that instruction found something in every round it was given — including two rounds that were themselves fixing this exact defect.

2. Each sweep finds the class the previous one missed

Five rounds of record correction, five distinct classes:

  1. wrong numbers
  2. stale strings (a figure that moved)
  3. stale quantities (a measurement the change invalidated)
  4. stale reasons (a conclusion that survived while its justification did not)
  5. references invalidated by an edit elsewhere in the same document — updating a status block's base commit silently invalidated every file:line in the file

Only class 5 was closed structurally: the design doc now carries no line numbers at all and names constructs instead, so it cannot recur. The other four were closed by sweeping, and sweeping is only as good as the list of things swept for. The honest summary is that the loop kept finding one more class rather than converging on empty.

A measurement that is not what it looks like

ladder-kick's 30–90 Hz band share reads 0.912, up from 0.351 before the floor fix. It is leakage-dominated. The meter's 40 ms Hann main lobe is ±50 Hz and the 30–90 Hz comb's top probe sits at 87.96 Hz, so a pure 24 Hz sinusoid with zero in-band energy reads 0.9998 there. The shipped figure sits under its own pure-tone leakage figure — exactly as the old 94 Hz ring's 0.351 sat under its 0.377.

What the share does establish — that the energy followed the corner down — survives, and the bar keeps teeth because a seven-octave span reads 0.103. Reproduced independently three times. Recorded in the test, the patch header and the design doc.

Related: the twenty-fold midrange gap against lpg-kick is real but is not evidence for the 24 dB/oct. Within ladder-kick, dropping to one pole moves that band only 0.006 → 0.009, inside the range the noise seed alone spans. The gap is two engines, two corners and two topologies at once.

A test bar that could not hold its own claim

midrange < 0.04 was removed, then replaced with a ratio. The absolute bar could not work: the gate_sweep_octaves = 7 injection reads 0.0364, between the noisiest healthy draw (0.0309) and the bar, so a known-broken configuration passed it.

body / midrange does work — measured draws 30.17 to 96.39 against injections at 5.396, 2.830 and 2.587, so a bar at 12 catches all three including the sweep-7 case no absolute bar could reach, with 2.5x and 2.2x margins. The reasoning that matters: a bar underneath a population is exposed to its lower tail, not its full span.

Verification

fastbuild 98/98 and --config=opt 98/98 at final HEAD; asan 98/98, ubsan 98/98 and tsan (//plugin:preset_race_test) 1/1 two commits earlier, with only comments and markdown since. Goldens unmoved throughout.

Execution record for `ladder-gate` (35 commits, merged from `main` at `8422929`). The branch added `dsp::VaFilterTopology::kLadderGate` — the low-pass gate's coupling law driving `dsp/ladder.h`'s four-stage saturating ladder — plus patch format minor 41, editor support, a measured onset characteristic, and the `ladder-kick` demo patch. Filed because the SDD ledger is git-ignored and dies with the workspace. ## The finding that mattered most was not in the plan The stage shipped `ladder-kick` measuring an octave high. The audition verdict was that it rang too high, and the question was "I thought we could get down to 23Hz?" That was right, and the design doc had written the cause down as an accepted *characteristic*: `kSvfFrequencyFloor` is a **normalized** phase increment (`1u << 21`), so it is a fixed fraction of the *internal* rate — 23.44 Hz at 1x, 93.75 Hz at 4x. `kLadderGate` forces 4x for its saturator. So the oversampling ratio was moving the filter's reachable floor in hertz, which is the one thing oversampling must never do. `VaCutoffLimitForRatio` already held the *ceiling* invariant; the floor had no counterpart, and that helper's own comment flagged the asymmetry as "a finding rather than an omission". The reason the floor had never been lowered is real and belongs to the SVF alone: `a3` goes as `g^2` and at 2^19 quantizes to 0.154 of a `SvfMulCoeff` (`Q<0,20>`) quantum — zero — leaving the second integrator lossless. **Nobody had checked whether that binds the ladder.** It does not: the ladder's coefficient is `G = g/(1+g)`, linear in `g`, holding **402 quanta** at the same increment. A factor of 2605. | increment | Hz at 192 kHz | SVF `a3` in `Q<0,20>` | ladder `G` in `Q<0,20>` | |---|---|---|---| | 2^21 | 93.75 | 2.47 | 1606 | | 2^19 | 23.44 | **0.154 → 0** | **402** | Fixed by `VaCutoffFloorForRatio(ratio) = kSvfFrequencyFloor / ratio`, applied at the ladder branch only. The reachable floor is now 23.44 Hz at every ratio rather than 23.44 / 46.88 / 93.75 — it does not go below what 1x already reaches. Go/no-go measured first: at 2^19 the ladder still reaches exact raw zero at every feedback setting (worst case 536,098 samples against 225,421 at 2^21) and full-scale drive peaks at 0.662 against `SvfState`'s ±256, *identical* to the 2^21 reading. **This also fixed the pre-existing `kLadder` topology.** `moog-lead`, `sh101-bass` and `tb303-line` gained the same headroom; they render bit-identically only because their corners never went that low (minimum ~384 / ~126 / ~96 Hz across MIDI 0-127). ## Rulings Each with what it costs if wrong. 1. **Folded the vactrol-guard task into the DSP task.** With only the guard, a `kLadderGate` slot still fell through to the SVF tail, which never reads `opening` — no observable effect, no possible test. *Cost:* a larger diff. 2. **Required forcing `kAntiDiagonal` and a zeroed `morph`.** The spec asserted it and a later task tested it, but no task implemented it. *Cost:* a two-line revert. 3. **Moved the format's out-of-range topology byte 5 → 6 into the enumerator's own task.** Adding `kLadderGate = 5` made 5 real. *Cost:* none, forced. 4. **Format bump 37 → 39 → 41.** Overtaken twice mid-flight: `main` took 37 (WVGD) and 38 (JUMP), then 39 (GLID) and 40 (the audio bus). *Cost:* none. 5. **Fixed the loader's six hand-copied enum bounds; left five `dsp::LoopMode` ones.** Filed separately — those need a sentinel added to `dsp/loop_mode.h`. *Cost:* a fourth loop mode is silently dropped by any of five sites that is missed. 6. **Corrected minor 41's and the spec's "plays silent" claim.** `kBypass` is pass-through, so an old reader plays the row unfiltered and ungated at full level — a much worse failure on a gate, where the filter *is* the amplitude contour. 7. **Then reversed course and fixed minor 32's identical claim too.** Two contradictory compatibility statements 325 lines apart in the file whose whole purpose is being the compatibility record is worse than reaching into a shipped stage's history entry. 8. **Kept `ladder-kick` when its low-end bar proved unreachable**, documenting the floor as a characteristic. Made moot by the floor fix, which the audition then demanded. 9. **Accepted the band test's re-derivation** after the floor change. A measurement test, not a golden; bars re-placed against re-run injections. 10. **Capped the record-correction loop at five rounds** rather than continuing. See below for why that was right and also not sufficient. ## Two patterns worth keeping ### 1. Self-contradicting comments cluster in summaries **Ten instances** across the branch of a comment or document asserting something the measurements in its own file contradict. Examples: - A sentence reading "two octaves under that, so taking it unclamped would put the corner four octaves above the ceiling" — contradicting itself within one line. - A test comment claiming it would catch a refactor that demonstrably passed it. - A resonance justification refuted by a measurement three paragraphs below it. - "no sign of the next squaring buying less than the last", directly above the series 0.30, 0.44, 0.54, 0.57 (deltas +0.14, +0.10, **+0.03**). - A design-doc headline block still stating the pre-fix render while the same document said otherwise eight paragraphs down. - An acceptance bar whose *reason* ("reads no existing field differently") stopped covering the stage when the floor change reached the pre-existing `kLadder` topology. **They cluster in summaries** — headline blocks, acceptance bars, status lines, test-plan bullets. Body prose sitting next to a number gets checked against it. A summary three sections away does not, and stays plausible while the thing it summarizes moves underneath it. **Every review was instructed to recompute rather than read, and that instruction found something in every round it was given** — including two rounds that were themselves fixing this exact defect. ### 2. Each sweep finds the class the previous one missed Five rounds of record correction, five distinct classes: 1. wrong **numbers** 2. stale **strings** (a figure that moved) 3. stale **quantities** (a measurement the change invalidated) 4. stale **reasons** (a conclusion that survived while its justification did not) 5. **references invalidated by an edit elsewhere in the same document** — updating a status block's base commit silently invalidated every `file:line` in the file Only class 5 was closed *structurally*: the design doc now carries no line numbers at all and names constructs instead, so it cannot recur. The other four were closed by sweeping, and sweeping is only as good as the list of things swept for. The honest summary is that the loop kept finding one more class rather than converging on empty. ## A measurement that is not what it looks like `ladder-kick`'s 30–90 Hz band share reads **0.912**, up from 0.351 before the floor fix. **It is leakage-dominated.** The meter's 40 ms Hann main lobe is ±50 Hz and the 30–90 Hz comb's top probe sits at 87.96 Hz, so a *pure* 24 Hz sinusoid with zero in-band energy reads **0.9998** there. The shipped figure sits under its own pure-tone leakage figure — exactly as the old 94 Hz ring's 0.351 sat under its 0.377. What the share does establish — that the energy followed the corner down — survives, and the bar keeps teeth because a seven-octave span reads 0.103. Reproduced independently three times. Recorded in the test, the patch header and the design doc. Related: the twenty-fold midrange gap against `lpg-kick` is real but is **not** evidence for the 24 dB/oct. Within `ladder-kick`, dropping to one pole moves that band only 0.006 → 0.009, inside the range the noise seed alone spans. The gap is two engines, two corners and two topologies at once. ## A test bar that could not hold its own claim `midrange < 0.04` was removed, then replaced with a ratio. The absolute bar could not work: the `gate_sweep_octaves = 7` injection reads 0.0364, *between* the noisiest healthy draw (0.0309) and the bar, so a known-broken configuration passed it. `body / midrange` does work — measured draws 30.17 to 96.39 against injections at 5.396, 2.830 and 2.587, so a bar at 12 catches all three including the sweep-7 case no absolute bar could reach, with 2.5x and 2.2x margins. The reasoning that matters: **a bar underneath a population is exposed to its lower tail, not its full span.** ## Verification fastbuild 98/98 and `--config=opt` 98/98 at final HEAD; asan 98/98, ubsan 98/98 and tsan (`//plugin:preset_race_test`) 1/1 two commits earlier, with only comments and markdown since. Goldens unmoved throughout.
Author
Owner

Read, and its outstanding work is done. Closing.

Ruling 5's deferral -- the five hand-copied dsp::LoopMode bounds -- was filed as #85 and closed earlier today (57bd868, e2c1350), which also took the four other enums read by their last named value.

#86 is fixed (271645b) and #87 is answered with a measurement (696cb75); see each issue for the detail. #87 moved on two of its own points: the step peaks at +2.47 dB at r = 0.624, at the lower edge of the window it named rather than inside it, and moog-lead at r = 0.55 crosses the boundary on every note. The obvious fix -- blending the two spans -- was built and measured, and it re-introduces the exact single-band regression the two-band placement exists to prevent.

Both patterns in this ledger were still live, and each cost something this pass.

Pattern 1, self-contradicting summaries. TheRampNeverMakesTheSubOnsetPeakWorseThanNoCorrection's injection note claimed the documented injection "reproduces both known failures above and fails this test; confirmed while writing it, not merely asserted." Run: it fails at 1000 Hz/r = 0.60, and at neither named row. The note is corrected. It is exactly the shape the ledger describes -- a summary three sections from the thing it summarizes, plausible and stale, and in an injection note, which is the last place one should be.

Also circular: LadderPrewarpSpanShiftFor said "see the ladder branch of Process for why this is a comparison rather than a continuous function," and that branch said "see that function's own comment for ... why it is not one point." They pointed at each other and neither answered it. There is now a measured answer at one end and a pointer at the other.

Pattern 2, each sweep finds the class the previous one missed. This one is a sixth class, and it is not a record defect at all -- it is a test whose probe grid is coarser than the margin it asserts on. That sweep probes at 1% of center, about 17 cents, while the margins at the two rows it names are 6.6 and 2.0 cents. A correction gone net worse at either snapped to the same grid point as the uncorrected peak and read as equal, so the suite accepted a documented regression. A uniformly fine grid catches it at 150 s against 38; refining only around the coarse winner catches it at 48 s, which is what it does now.

That class -- an assertion finer than the instrument measuring it -- is worth adding to the ledger's list of five. It is invisible to every sweep the ledger describes, because nothing about the text is wrong: the numbers, strings, quantities, reasons and references were all accurate. Only the resolution was insufficient, and the test was green.

A seventh class, found because the flake that started this was a test brushing its timeout. //dsp:va_filter_test ran 570 s under asan, 320 s of it in one test. The cause is the same sin four times over -- a fixed brute-force span sized for a worst case instead of a check for the condition:

  • a bisection 16 deep resolving 1.5e-5 against a tightest bar of 0.01, and the depth compounds, since converging that close to the onset lands on rings that take proportionally longer to die;
  • a solver bisecting 80 times per sample on a bracket narrower than a double after 54;
  • a settle of 1500000 samples whose own comment said the number was chosen "by watching the peak plateau well before" it -- padding past a condition instead of checking for it;
  • a peak sweep over 0.6 to 1.2 of center where no curve has ever peaked outside 0.85 to 0.96.

Plus, in //editor:patch_view_id_conflict_test, a mouse-hover scan across an 8000-pixel window -- a height chosen so no page is ever clipped, not one any page reaches -- at 3 px a step: 2667 full-page draws per sweep iteration, most over empty space.

Every measurement is unchanged (identical, or within 3.4e-4), and each test still fails on the bug it exists to catch, re-verified after every cut. va_filter_test is 570 s -> 132 s under asan and drops from size = "large" to "medium"; bazel was already reporting the size as too big. The id-conflict target is 93 s -> 60 s.

Still slow and untouched, at fastbuild (asan is roughly 4x): dsp:modal_loop_test 152 s (already #61), dsp:modal_layer_test 108 s, patches:patches_test ~85 s per shard across four, resynth:pipeline_test 66 s, dsp:va_layer_test 45 s, plugin:patch_format_test 42 s. I have not profiled those, so I am not claiming they share the pattern -- only that they are the remaining budget.

Verification at final HEAD (3c5b7af): fastbuild, --config=opt, asan, ubsan all 101/101, tsan (//plugin:preset_race_test) 1/1. Goldens unmoved.

One thing left untracked: closing #87's step properly needs a prewarp span that tracks the measured onset table continuously rather than interpolating between two points on it. That is a re-derivation, not a smoothing, and wants its own issue if you want it followed.

Read, and its outstanding work is done. Closing. **Ruling 5's deferral** -- the five hand-copied `dsp::LoopMode` bounds -- was filed as #85 and closed earlier today (`57bd868`, `e2c1350`), which also took the four other enums read by their last named value. **#86 is fixed** (`271645b`) and **#87 is answered with a measurement** (`696cb75`); see each issue for the detail. #87 moved on two of its own points: the step peaks at +2.47 dB at r = 0.624, at the lower edge of the window it named rather than inside it, and `moog-lead` at r = 0.55 crosses the boundary on every note. The obvious fix -- blending the two spans -- was built and measured, and it re-introduces the exact single-band regression the two-band placement exists to prevent. **Both patterns in this ledger were still live, and each cost something this pass.** *Pattern 1, self-contradicting summaries.* `TheRampNeverMakesTheSubOnsetPeakWorseThanNoCorrection`'s injection note claimed the documented injection "reproduces both known failures above and fails this test; confirmed while writing it, not merely asserted." Run: it fails at 1000 Hz/r = 0.60, and at **neither** named row. The note is corrected. It is exactly the shape the ledger describes -- a summary three sections from the thing it summarizes, plausible and stale, and in an injection note, which is the last place one should be. Also circular: `LadderPrewarpSpanShiftFor` said "see the ladder branch of `Process` for why this is a comparison rather than a continuous function," and that branch said "see that function's own comment for ... why it is not one point." They pointed at each other and neither answered it. There is now a measured answer at one end and a pointer at the other. *Pattern 2, each sweep finds the class the previous one missed.* This one is a sixth class, and it is not a record defect at all -- it is a **test whose probe grid is coarser than the margin it asserts on**. That sweep probes at 1% of center, about 17 cents, while the margins at the two rows it names are 6.6 and 2.0 cents. A correction gone net worse at either snapped to the same grid point as the uncorrected peak and read as equal, so the suite accepted a documented regression. A uniformly fine grid catches it at 150 s against 38; refining only around the coarse winner catches it at 48 s, which is what it does now. That class -- an assertion finer than the instrument measuring it -- is worth adding to the ledger's list of five. It is invisible to every sweep the ledger describes, because nothing about the text is wrong: the numbers, strings, quantities, reasons and references were all accurate. Only the resolution was insufficient, and the test was green. **A seventh class, found because the flake that started this was a test brushing its timeout.** `//dsp:va_filter_test` ran **570 s under asan**, 320 s of it in one test. The cause is the same sin four times over -- a fixed brute-force span sized for a worst case instead of a check for the condition: - a bisection 16 deep resolving 1.5e-5 against a tightest bar of 0.01, and the depth compounds, since converging that close to the onset lands on rings that take proportionally longer to die; - a solver bisecting 80 times per sample on a bracket narrower than a double after 54; - a settle of 1500000 samples whose own comment said the number was chosen "by watching the peak plateau well before" it -- padding past a condition instead of checking for it; - a peak sweep over 0.6 to 1.2 of center where no curve has ever peaked outside 0.85 to 0.96. Plus, in `//editor:patch_view_id_conflict_test`, a mouse-hover scan across an 8000-pixel window -- a height chosen so no page is ever clipped, not one any page reaches -- at 3 px a step: 2667 full-page draws per sweep iteration, most over empty space. Every measurement is unchanged (identical, or within 3.4e-4), and each test still fails on the bug it exists to catch, re-verified after every cut. `va_filter_test` is 570 s -> 132 s under asan and drops from `size = "large"` to `"medium"`; bazel was already reporting the size as too big. The id-conflict target is 93 s -> 60 s. Still slow and untouched, at fastbuild (asan is roughly 4x): `dsp:modal_loop_test` 152 s (already #61), `dsp:modal_layer_test` 108 s, `patches:patches_test` ~85 s per shard across four, `resynth:pipeline_test` 66 s, `dsp:va_layer_test` 45 s, `plugin:patch_format_test` 42 s. I have not profiled those, so I am not claiming they share the pattern -- only that they are the remaining budget. Verification at final HEAD (`3c5b7af`): fastbuild, `--config=opt`, asan, ubsan all 101/101, tsan (`//plugin:preset_race_test`) 1/1. Goldens unmoved. One thing left untracked: closing #87's step properly needs a prewarp span that tracks the measured onset table continuously rather than interpolating between two points on it. That is a re-derivation, not a smoothing, and wants its own issue if you want it followed.
bcox closed this issue 2026-09-10 18:52:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bcox/tymbal#84
No description provided.