diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index ffb79c4..e8ba2eb 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -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; diff --git a/docs/PLAN.md b/docs/PLAN.md index 6fe3d9a..d9ff9b5 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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. ---