docs: close out Θ-W2-T1 filter-voice-path into COMPLETED

Move the landed filter-voice-path track from PLAN.md into COMPLETED.md with full
narrative (quantizer removal, editor floor raise). Mark Θ-W2 fully landed, matching
W1's phrasing; fix a stale forward reference.
This commit is contained in:
2026-07-30 18:23:42 -04:00
parent 9b1a49c640
commit 413967a205
2 changed files with 48 additions and 73 deletions
+38
View File
@@ -151,6 +151,44 @@ path.
pre- vs post-envelope placement is sound-defining), the drive dial's calibration, and
the fact that drive authority varies ~11 dB across the morph sweep.
### Θ-W2-T1 — filter-voice-path
Wires the pure filter module into ReaSampler 9000's per-voice signal path as a new fixed
processing point between the pitch envelope and the amp stage, gives it its own
knob-deck group, and relays the deck row in signal-flow order (pitch → filter → amp).
Each `Voice` owns its own `VoiceFilter` and a second `AdsrEnvelope` instance — per-voice,
never shared — and neither adds allocation or virtual dispatch to the per-sample path.
Parameters — morph position, cutoff, Q, drive, mod amount (bipolar ±100%, targeting
cutoff), velocity and key-tracking modulation, then AHDSR — live in the one parameter
set; `FilterParams` stores the filter module's own `FilterSettings` by value rather than
a parallel copy of the normalized positions. The filter is off by default and bit-exact
off — a project saved before the change reopens sounding identical, pinned by a
bit-equality test. `ComponentState`'s params payload moves v8 → v9, appending the filter
tail; a v8 blob is a strict prefix and lifts to the off/neutral filter default, with
non-finite filter fields falling back to neutral, pinned by a golden byte-literal
fixture. Deck composition was extracted into a new pure module
`core/instrument/ui/deck_groups`, with the pitch → filter → amp row order pinned by
test; the filter's AHDSR ships as its own `FILTER ENV` group sibling to `FILTER`,
mirroring the existing `PITCH` / `PITCH ENV` split, and a morph-law toggle (`Band` |
`Notch`) ships as the FILTER group's row toggle.
**Deviations from spec:**
- **An initial mod-quantizer was added, then rejected and removed.** A
`kFilterModSteps = 2048` step gate on the coefficient re-solve stair-stepped the
corner (~5.8 cents per step); Daniel rejected it. Replaced by a cutoff-only re-solve
(`VoiceFilter::setCutoffNorm`) that re-derives only `g = tan(π·fc/sr)` — Q's parabola,
the morph's cos/sin, and the folded mix are all cutoff-independent and stay cached
from `prepare()`; `filter_params` hoists its constant logs. Measured at 48 kHz,
Release, net of the sweep generator: kernel alone 2.8 ns/frame, full `prepare()` 56.9
ns, cutoff-only re-solve 15.5 ns — about 1.2% of a core for 16 continuously-swept
voices. The corner now sweeps continuously rather than stair-stepping.
- **The editor floor was raised to 840×620**, which is also its new default size, up
from a 560×460 floor at which the grown deck wrapped to four rows and pushed
`FILTER ENV` / `AMP ENVELOPE` / `VOICE` / `MASTER` off-screen with no scroll.
`kEditorMinWidth`/`kEditorMinHeight` now live in `sample_bands`, read by both the
shell's `checkSizeConstraint` and the opening `ViewRect`. Consequence worth
recording: a host with a saved editor rect below 840×620 is clamped up on reopen.
### Θ-W2-T2 — stereo-waveform-lanes
Delivered as specified: two stacked lanes, L above R, in stereo mode; one lane in mono;