Ladder gate execution ledger: 10 rulings, a bug the spec called a characteristic, and five classes of a self-contradicting comment #84
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
bcox/tymbal#84
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Execution record for
ladder-gate(35 commits, merged frommainat8422929). The branch addeddsp::VaFilterTopology::kLadderGate— the low-pass gate's coupling law drivingdsp/ladder.h's four-stage saturating ladder — plus patch format minor 41, editor support, a measured onset characteristic, and theladder-kickdemo 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-kickmeasuring 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:
kSvfFrequencyFlooris 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.kLadderGateforces 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.VaCutoffLimitForRatioalready 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:
a3goes asg^2and at 2^19 quantizes to 0.154 of aSvfMulCoeff(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 isG = g/(1+g), linear ing, holding 402 quanta at the same increment. A factor of 2605.a3inQ<0,20>GinQ<0,20>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 againstSvfState's ±256, identical to the 2^21 reading.This also fixed the pre-existing
kLaddertopology.moog-lead,sh101-bassandtb303-linegained 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.
kLadderGateslot still fell through to the SVF tail, which never readsopening— no observable effect, no possible test. Cost: a larger diff.kAntiDiagonaland a zeroedmorph. The spec asserted it and a later task tested it, but no task implemented it. Cost: a two-line revert.kLadderGate = 5made 5 real. Cost: none, forced.maintook 37 (WVGD) and 38 (JUMP), then 39 (GLID) and 40 (the audio bus). Cost: none.dsp::LoopModeones. Filed separately — those need a sentinel added todsp/loop_mode.h. Cost: a fourth loop mode is silently dropped by any of five sites that is missed.kBypassis 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.ladder-kickwhen its low-end bar proved unreachable, documenting the floor as a characteristic. Made moot by the floor fix, which the audition then demanded.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:
kLaddertopology.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:
file:linein the fileOnly 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-kickis real but is not evidence for the 24 dB/oct. Withinladder-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.04was removed, then replaced with a ratio. The absolute bar could not work: thegate_sweep_octaves = 7injection reads 0.0364, between the noisiest healthy draw (0.0309) and the bar, so a known-broken configuration passed it.body / midrangedoes 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=opt98/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.Read, and its outstanding work is done. Closing.
Ruling 5's deferral -- the five hand-copied
dsp::LoopModebounds -- 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, andmoog-leadat 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:
LadderPrewarpSpanShiftForsaid "see the ladder branch ofProcessfor 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_testran 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: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_testis 570 s -> 132 s under asan and drops fromsize = "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_test152 s (already #61),dsp:modal_layer_test108 s,patches:patches_test~85 s per shard across four,resynth:pipeline_test66 s,dsp:va_layer_test45 s,plugin:patch_format_test42 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.