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;
+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
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
landed as a standalone pure module with no call site — Θ-W2-T1 integrates it into the
voice path.
landed as a standalone pure module — Θ-W2-T1 has since wired it into the voice path (see
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
writing storage plumbing W1 deletes.
**Θ-W2-T2 (`stereo-waveform-lanes`) and Θ-W2-T3 (`toolbar-and-piano-strip`) have
landed** — see `docs/COMPLETED.md`. Between them: the waveform band now shows both
channels in stereo mode behind a type-enforced full-height overlay contract, and the
chrome band's toolbar and piano strip are cleaned up per spec. **One track remains:**
Θ-W2-T1 (`filter-voice-path`), which puts the filter into the per-voice signal path and
gives it its deck. The three tracks were disjoint by band — T1 owns the parameter model
and the deck band, T2 owned the waveform band, T3 the chrome band — and none
re-allocates 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.
**All three tracks have landed** Θ-W2-T1 (`filter-voice-path`), Θ-W2-T2
(`stereo-waveform-lanes`), and Θ-W2-T3 (`toolbar-and-piano-strip`) — see
`docs/COMPLETED.md` for the full narrative of each. Between them: the filter sits in the
per-voice signal path with its own deck, the waveform band shows both channels in
stereo mode behind a type-enforced full-height overlay contract, and the chrome band's
toolbar and piano strip are cleaned up per spec. The three tracks were disjoint by
band — T1 owned the parameter model and the deck band, T2 owned the waveform band, T3
the chrome band — and none re-allocated the band stack.
---