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 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. 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 ### Θ-W2-T2 — stereo-waveform-lanes
Delivered as specified: two stacked lanes, L above R, in stereo mode; one lane in mono; Delivered as specified: two stacked lanes, L above R, in stereo mode; one lane in mono;
+10 -73
View File
@@ -145,8 +145,8 @@ collision is real and the serialization is the correct answer.
the full narrative of each. Between them: the zone subsystem is retired, the wave's two the full narrative of each. Between them: the zone subsystem is retired, the wave's two
responsibility seams (the `sampler_core` split, the Sample-face band split) are in responsibility seams (the `sampler_core` split, the Sample-face band split) are in
place, the two extension-side capture-handoff defects are fixed, and the filter DSP has place, the two extension-side capture-handoff defects are fixed, and the filter DSP has
landed as a standalone pure module with no call site — Θ-W2-T1 integrates it into the landed as a standalone pure module — Θ-W2-T1 has since wired it into the voice path (see
voice path. below).
--- ---
@@ -158,77 +158,14 @@ band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which de
what T3's piano strip displays. Authoring any of this against the per-zone model means what T3's piano strip displays. Authoring any of this against the per-zone model means
writing storage plumbing W1 deletes. writing storage plumbing W1 deletes.
**Θ-W2-T2 (`stereo-waveform-lanes`) and Θ-W2-T3 (`toolbar-and-piano-strip`) have **All three tracks have landed** Θ-W2-T1 (`filter-voice-path`), Θ-W2-T2
landed** — see `docs/COMPLETED.md`. Between them: the waveform band now shows both (`stereo-waveform-lanes`), and Θ-W2-T3 (`toolbar-and-piano-strip`) — see
channels in stereo mode behind a type-enforced full-height overlay contract, and the `docs/COMPLETED.md` for the full narrative of each. Between them: the filter sits in the
chrome band's toolbar and piano strip are cleaned up per spec. **One track remains:** per-voice signal path with its own deck, the waveform band shows both channels in
Θ-W2-T1 (`filter-voice-path`), which puts the filter into the per-voice signal path and stereo mode behind a type-enforced full-height overlay contract, and the chrome band's
gives it its deck. The three tracks were disjoint by band — T1 owns the parameter model toolbar and piano strip are cleaned up per spec. The three tracks were disjoint by
and the deck band, T2 owned the waveform band, T3 the chrome band — and none band — T1 owned the parameter model and the deck band, T2 owned the waveform band, T3
re-allocates the band stack. the chrome band — and none re-allocated the band stack.
#### Θ-W2-T1 — `filter-voice-path`
**Goal.** Put the filter into the per-voice signal path as a new fixed processing point,
give it its deck, and relay the deck row in signal-flow order.
**Consolidates item 2 (integration half).** The DSP module is Θ-W1-T3. The filter
envelope's *curve treatment and mode-driven shape* arrive in Θ-W3 — this track ships the
filter envelope in the existing staged AHDSR shape.
**Surface boundary — owns:** `core/instrument/engine/` voice-render filter call site +
`zone_params.h` (filter parameters), `core/instrument/map/component_state_io` (version
bump for the filter parameters), `core/instrument/ui/knob_deck` + the **deck band** of
the Sample face (geometry, paint, input). Reads the band allocator; does not change it.
**Behavior.**
- **Pipeline position.** A new processing point in the sampler audio pipeline: **after
the pitch envelope, before the amp stage.** The amp envelope still shapes the filtered
result.
- **Per-voice.** Each sounding voice runs its own filter with its own envelope state —
not a shared instance-wide filter.
- **Parameters:** mode, cutoff, Q, mod amt — then the AHDSR controls.
- **Mod amt: bipolar, 100% to +100%, targeting cutoff** — covering the full range
from either end.
- Cutoff / Q / mode ranges are the module's, from Θ-W1-T3.
- **Velocity and key-tracking modulation ship with this item — not deferred.** The
filter gains velocity modulation following the amp's velocity-transfer-curve pattern
and key-tracking following the pitch key-tracking pattern. The follow-up established
the parallel, not new ranges or control layout — follow the cited precedents. (The
*bipolar domain and default* of the filter velocity curve, and its button's home, are
item 11 — Θ-W4-T2. This track wires the modulation path; that track shapes the curve.)
- **Off by default.** Pre-existing saved instances and freshly loaded captures sound
unchanged until the user engages it.
- **Parameters live in the instrument's one parameter set** (post-item-16) — no per-zone
storage, no Sample/Zone parity.
- **Label.** The user-facing deck-group label is **"Filter"**. "MM preamp" is working
shorthand for the DSP lineage, not UI text.
- **Deck reorder.** The knob deck row is relaid out in signal-flow order:
**pitch → filter → amp**.
**Acceptance criteria.**
- With the filter engaged, played notes are audibly filtered at the specified pipeline
point: the filter acts on pitched (post-pitch-envelope) signal and the amp envelope
still shapes the filtered result — audible ordering pitch → filter → amp.
- Mode, cutoff, Q, and mod-amt controls appear in a deck group labeled **"Filter"**.
- Cutoff sweeps the full audio spectrum on a log scale, fully open to fully closed; Q
spans 0.1 → 10 with √2 at the control's center; mod amt at 100% and at +100% each
drive cutoff across the full range, from opposite ends.
- High Q audibly emphasizes the cutoff region in both HP and LP modes.
- **Two simultaneously sounding voices at different envelope phases are filtered
independently** — per-voice processing is audible, not a shared filter.
- Filter parameters live in the one parameter set: they edit in one place and govern the
instrument as a whole.
- Velocity and key-tracking modulation of the filter are audible — velocity following
the amp-velocity-transfer-curve pattern, key-tracking following the
pitch-key-tracking pattern.
- The deck row reads pitch → filter → amp left-to-right.
- **The filter is off by default:** a project saved before this change reopens sounding
identical, and a freshly loaded capture sounds unchanged until the filter is engaged.
- The filter tick adds no allocation and no virtual dispatch to `process()`.
**Open questions.** None carried from item 2 — all three of its prior questions (other
mod sources, parameter storage side, neutral default) closed in the source doc.
--- ---