From 25c63fb8079199ee2cd5ba89ba64e9df56acff84 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 30 Jul 2026 11:16:21 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20close=20out=20=CE=98-W1-T3=20and=20?= =?UTF-8?q?=CE=98-W2-T2/T3=20into=20COMPLETED;=20add=20the=20filter=20dire?= =?UTF-8?q?ctory=20to=20the=20architecture=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 +- docs/COMPLETED.md | 80 +++++++++++++++++++++++ docs/PLAN.md | 163 +++++----------------------------------------- 3 files changed, 98 insertions(+), 150 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c77c9dd..d17fbe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **ReaSampler** is a per-project audio sample-bank capture tool that builds two artifacts: the REAPER extension (`reaper_reasampler`) and **ReaSampler 9000**, a Windows-only VST3 sampler instrument (`reasampler_9000.vst3`, `core/instrument/` + `shell/instrument/`, second CMake target `reasampler_vst`, gated on the vendored `vendor/vst3sdk` submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout: `core/` never includes REAPER or VST3 SDK types, `shell/` is where those hosts are actually touched, `app/` is the extension entry point. Every REAPER API name cited in project docs is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. -Per-module detail — what each file owns, its invariants — lives in the nineteen per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below. +Per-module detail — what each file owns, its invariants — lives in the twenty per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below. ## Settled decisions @@ -69,7 +69,7 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde ## Architecture: the load-bearing split -`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the nineteen directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth. +`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the twenty directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth. | Directory | Scope | |---|---| @@ -77,6 +77,7 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde | `src/core/audio/` | pure audio-data math | | `src/core/capture/` | pure logic behind the capture pillar | | `src/core/instrument/` | pure VST3-instrument core (engine / map / ui) | +| `src/core/instrument/engine/filter/` | pure per-voice resonant TPT/SVF filter (HP→BP→LP / HP→notch→LP morph, drive stage; no call site yet) | | `src/core/json/` | the hand-rolled JSON lexical layer | | `src/core/model/` | the pure bank/sample index and its multi-bank container | | `src/core/reclaim/` | pure prune orphan computation | diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index 59b4e89..cf398cf 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -84,3 +84,83 @@ FX-button drop path. explicitly a soak ("a single pass is not a gate") and item 6 needs a live container drop; both require REAPER and are outstanding. The code is merged; the acceptance gates are not closed. + +### Θ-W1-T3 — filter-dsp-port + +Lands the per-voice resonant filter as a standalone pure module +(`core/instrument/engine/filter/`, five files: `filter_params`, `filter_coeffs`, +`filter_morph`, `filter_saturate`, `voice_filter`) — concrete `VoiceFilter` type, no +vtable, no allocation in `process()`. **No call site**; Θ-W2-T1 wires it into the voice +path. + +**Deviations from spec — the spec itself changed mid-flight, headline first:** +- **The Cortex-M4 biquad port was superseded entirely by a TPT/SVF topology, Daniel's + call.** Measurement found the firmware's high-pass resonance feedback tap vestigial: + its stated rationale was inverted (the HP numerator approaches 1 as cutoff falls, not + zero — it's the LP numerator that collapses), it *reduced* HP resonance everywhere it + ran, and it carried unwanted sample-rate and input-level dependence. It was a Q15 + fixed-point workaround for a ~17-bit cancellation float32 doesn't suffer. Daniel's + ruling on the resulting level-dependent resonance bloom: *"was a feature on the + hardware (one knob colorful HP for master FX), wrong choice for this approach."* +- **Two discrete modes (HP/LP) became a continuous morph, with two selectable morph + laws** — HP→BP→LP (default) and HP→notch→LP (Oberheim SEM) — chosen at `prepare()` + via a `MorphLaw` enum on `FilterSettings`. Zero per-sample cost, verified by diffing + emitted assembly (byte-identical between laws). +- **A configurable drive stage was added:** an in-loop soft limiter on the band-pass + integrator state, normalized 0..1, with a radial dial planned for Θ-W2. Drive at 0 is + bit-exact linear. +- **`Biquad1PoleLP` was struck (Daniel's call) and never ported.** +- **Q spans the full 0.1–10 with √2 at the control centre**, replacing the firmware's + 0.707-floored mapping — as originally specified. +- **No reference sample rate exists anywhere in the module** — rate reaches the DSP + only via `g = tan(π·fc/sr)`. An interim fix that anchored a feedback tap to a + `1/48000` constant was superseded by the rewrite. +- **The rewrite fixed a float32 conditioning defect the biquad carried:** Direct Form I + measured −27% peak error at 20 Hz / 192 kHz; TPT measures +0.034%. +- **Still open, deliberately:** drive's maximum depth (4.0, set by measurement — at 64 + the resonant peak inverted below passband) and the absence of makeup gain both await + an ear pass against the real dial. +- **The module has no call site** — integration is Θ-W2-T1, which also carries three + recorded decisions of its own: the envelope-order choice (drive is level-dependent, so + 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-T2 — stereo-waveform-lanes + +Delivered as specified: two stacked lanes, L above R, in stereo mode; one lane in mono; +overlays draw once at full stacked height. + +**Deviations from spec:** +- **A mono source under stereo mode draws one lane**, not two — lane count keys off + `stereoMode && sourceChannels >= 2`, not channel mode alone, since a mono capture in + stereo mode is dual-mono and a second lane would be the redundant duplicate the spec + forbids. Review confirmed this is the only reading consistent with the decode path. +- The full-height overlay contract Θ-W3 and Θ-W4 consume is **type-enforced, not + merely documented**: an `OverlayArea` wrapper type that lane rects cannot satisfy. +- A single-slot per-channel PCM cache was added to the editor session, invalidating + alongside the existing PCM cache. + +### Θ-W2-T3 — toolbar-and-piano-strip + +Delivered as specified: one toolbar font, no zone-count label, full-width piano strip, +uniform key widths, note-name tooltips, root displayed and settable. + +**Deviations from spec:** +- **The non-uniform key widths were integer quantisation, not aliasing** — the old + `keyEdgeToX` truncated an exact rational, alternating 6px/7px. Θ-W6's general + antialiasing audit inherits nothing on key *widths* as a result, though key *edges* + are still drawn unantialiased. +- **Uniform integer key widths and gap-free edge-to-edge tiling are mutually + exclusive** — 75 white keys do not divide an arbitrary width. The residue now lands + in symmetric end gutters, 37px each side at the shipped 840px default — the maximum + of a sawtooth with period 75px of window width. Daniel accepted this provisionally, + pending how it looks in REAPER. +- The whole control run moved into the toolbar row, not just preview and mono/stereo — + the full-width strip left the velocity knob and curve button nowhere else to go. +- Root drag became absolute-tracking rather than pixel-delta, since black keys + overlaying white give no single pixels-per-semitone rate. +- **Width uniformity is guaranteed in client pixels only.** Nothing in the instrument + consumes a DPI scale factor, so host-side scaling is unverified — Θ-W6 already + carries a "confirm the fix survives DPI scaling" item and now genuinely inherits it. +- A stale-hover latch was fixed across **all** drag kinds and both drag-termination + paths, wider than the strip work that surfaced it. diff --git a/docs/PLAN.md b/docs/PLAN.md index 851afe7..6fe3d9a 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -140,65 +140,13 @@ collision is real and the serialization is the correct answer. ### Θ-W1 — Collapse and re-seam -**Θ-W1-T1 (`zone-retirement`) and Θ-W1-T2 (`capture-handoff-bugs`) have landed** — see -`docs/COMPLETED.md`. Between them: the zone subsystem is retired and 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. **One track remains:** -Θ-W1-T3 (`filter-dsp-port`), which lands the filter DSP as a standalone pure module — -so that from W2 on, tracks fill bands and call modules rather than fighting over -god-TUs. - ---- - -#### Θ-W1-T3 — `filter-dsp-port` - -**Goal.** Land Daniel's Cortex-M4 resonant high/low-pass filter as a pure, tested -module with the settled parameter ranges — **no call site**, so it is disjoint from -every other track by construction. - -**Consolidates item 2 (DSP half).** The integration half is Θ-W2-T1. - -**Surface boundary — owns:** a new `src/core/instrument/engine/filter.{h,cpp}` (name -[propose]) plus its `filter_tests` target and the CMake row. No existing file is edited -except `CMakeLists.txt`. - -**EXTERNAL PREREQUISITE — this track cannot start without it.** The filter processor is -based on Daniel's own Cortex-M4 resonant high/lowpass filter code. **That code is an -input Daniel supplies at implementation time** — it is not in this repo, and neither -this plan nor `TODO-1.0.md` characterizes it beyond "resonant high and lowpass." -Dispatching this track before the source is in hand is a stall, not a start. - -**Behavior.** -- **Modes:** high-pass and low-pass now. **Modes beyond the pass filters are explicitly - deferred to later** — do not build a mode-extension framework for them. -- **Cutoff:** the full audio spectrum, **log scaled**, from fully open to fully closed. -- **Q:** **0.1 to 10** (the same range as the curve exponent), scaled so **√2 sits at - the center** of the control. -- **State is per-voice.** The module carries no shared/instance-wide state; each - sounding voice runs its own instance with its own coefficients and history. -- **Concrete type, no interface.** Two modes select by branch or compile-time-known - dispatch. No `IFilter`, no virtual `tick()` — this lands directly on the per-voice - per-sample path. -- Sample-rate-aware by parameter, not by hardcoded constant (the standing - no-hardcoded-sample-rates ruling). - -**Acceptance criteria.** -- The module builds and tests without REAPER, VST3, or a DAW. -- Cutoff sweeps the full audio spectrum on a log scale, from fully open to fully closed, - with the mapping asserted in tests at the endpoints and the decade midpoints. -- Q spans 0.1 → 10 with √2 at the control's center — asserted, not eyeballed. -- High Q measurably emphasizes the cutoff region (resonance) in **both** HP and LP - modes; the test asserts the resonant peak, not just monotone rolloff. -- Coefficient updates are stable across a full-range cutoff sweep at audio rate — no - blow-up, no NaN, no denormal stall. -- No virtual dispatch and no allocation in the per-sample entry point. - -**Open questions.** -- **Module name and file placement [propose]** — `core/instrument/engine/filter` is the - obvious home; confirm at review. -- **Whether mode is a runtime branch or a compile-time-known dispatch [propose]** — - decided by what the ported DSP actually costs per sample; the constraint is - "no vtable," not "no branch." +**All three tracks have landed** — Θ-W1-T1 (`zone-retirement`), Θ-W1-T2 +(`capture-handoff-bugs`), and Θ-W1-T3 (`filter-dsp-port`) — see `docs/COMPLETED.md` for +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. --- @@ -210,9 +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. -**Three tracks, in priority order.** They are disjoint by band: T1 owns the parameter -model and the deck band, T2 the waveform band, T3 the chrome band. None re-allocates the -band stack. +**Θ-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` @@ -279,92 +232,6 @@ mod sources, parameter storage side, neutral default) closed in the source doc. --- -#### Θ-W2-T2 — `stereo-waveform-lanes` - -**Goal.** Show both channels in the waveform visual when the instrument is in stereo -mode, without duplicating any overlay. - -**Consolidates item 7.** - -**Surface boundary — owns:** `core/instrument/ui/waveform_view` and the **waveform -band** of the Sample face (geometry, paint, input). Reads channel mode; does not own -the mono/stereo control (that widget relocates in T3 — coordinate on state, not on -layout). - -**Behavior.** -- In **stereo mode**, the waveform visual shows **both L and R channels, left on top** - — two stacked lanes. -- In **mono mode**, a single channel shows — no redundant duplicate lane. -- The display keys off the active channel mode. -- **Overlays draw once, at full height.** Overlays that ride the waveform — the envelope - overlay, markers, and item 9's loop region when it lands — draw **once at full height - across both stacked lanes**, not per lane. Land the full-height overlay contract now - even though the envelope overlay is reworked in W3 and the loop region arrives in W4; - both consume this contract. -- **Stereo processing is linked.** One editor, one set of controls governing both - channels — no per-channel parameter divergence, no per-channel editing surface. - -**Acceptance criteria.** -- A stereo capture in stereo mode shows two stacked lanes, L above R, each a true view - of its channel's content — an asymmetric-channel capture visibly differs between lanes. -- Mono mode shows exactly one lane. Switching modes updates the display accordingly. -- In stereo mode, waveform-riding overlays render **once** at full stacked height — no - duplicated per-lane copies — and stay legible across both lanes. -- No per-channel controls appear; every edit applies identically to both channels. - -**Open questions.** None — item 7's overlay-layout question closed in the source doc -("one full height; stereo linked processing, one editor"). - ---- - -#### Θ-W2-T3 — `toolbar-and-piano-strip` - -**Goal.** Clean up the top toolbar, free the note-range piano strip to full width, and -make the key pattern uniform and self-describing. - -**Consolidates item 12.** - -**Surface boundary — owns:** `core/instrument/ui/keyboard_strip`, `core/ui/tooltip` (as -consumer), and the **chrome band** of the Sample face (toolbar layout, title, piano -strip — geometry, paint, input). Reads the band allocator; does not change it. - -**Behavior.** -- **Font cleanup.** One consistent font treatment across the top toolbar text. -- **Zone count label removed** from the title. (Doubly settled — item 16 removed its - referent in W1.) -- **Preview and mono/stereo controls relocate** next to the browse and zoom buttons, - freeing the **note-range piano strip to take the full width**. -- **Uniform piano keys.** Some keys currently render skinnier than others — Daniel - suspects pixel aliasing, but the requirement stands regardless of cause: keys of the - same class render at uniform width. (If aliasing turns out to be the cause, the - *general* audit is item 13 — Θ-W6; the uniform-width requirement is this track's - regardless.) -- **Note-name tooltips.** Hovering a piano key shows its note value (C4 etc., DAW - convention), following the instrument's existing tooltip conventions. -- **What the strip means with no zones.** With zone bars gone, the strip's surviving - jobs are the **root display/affordance** (item 15's answer round makes root explicitly - resample-stable, so it must remain visible and settable here) and the note-name - tooltips — plus a range display only if W1-T1's key-range answer kept one. - -**Acceptance criteria.** -- Top toolbar text renders in one consistent font treatment; the title carries no zone - count. -- Preview and mono/stereo sit adjacent to browse/zoom; the piano strip spans the full - editor width. -- **Same-class keys are equal pixel width at any window width and at any DPI scale.** -- Hovering any piano key shows its note name in a tooltip. -- The root note is displayed on and settable from the strip. - -**Open questions.** -- **The strip's exact contents [propose]** — jointly owned with item 16's key-range - answer, which W1-T1 delivers. Propose the final inventory (root affordance + - tooltips, ± range display) at this track's review. -- **The preview button's glyph is not this track's** — item 11 (Θ-W4-T2) replaces its - inner text with a glyph in the position this track gives it. Ship the button with its - current text; do not pre-empt the glyph. - ---- - ### Θ-W3 — The staged envelope system **Depends on W2 for:** the filter envelope's existence — items 1, 8, and 14 govern