From a689fb75eb8a4a6bef4610732dfc0d7a434e06fc Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 29 Jul 2026 23:09:53 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20add=20PLAN.md=20=E2=80=94=20post-1.0=20?= =?UTF-8?q?roadmap=20consolidating=20TODO-1.0's=20seventeen=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Greek-lettered phases (Θ instrument overhaul, Ξ resample loop): sequenced waves, parallel tracks on disjoint surfaces, per-track acceptance criteria, carried-forward open questions, traceability table. TODO-1.0.md retained as verbatim-provenance appendix. --- docs/PLAN.md | 1517 ++++++++++++++++++++++++++++++++++++++++++++++ docs/TODO-1.0.md | 9 + 2 files changed, 1526 insertions(+) create mode 100644 docs/PLAN.md diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..328c069 --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,1517 @@ +# PLAN + +The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas and +sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be +dispatched against directly. + +## What this doc is, and how it relates to the others + +- **`docs/PLAN.md`** (this file) — the active on-deck specification list. Each track is + written so a specialist brief is writable from this file alone: goal, consolidated + source items, surface boundary, behavior, acceptance criteria, open questions, + prerequisites. +- **`docs/TODO-1.0.md`** — retained as the **verbatim-provenance appendix**. It holds + Daniel's raw asks and every answer round, unedited; where this plan compresses a + behavior bullet, that file is the backing record. Items are cited here by number + (e.g. "consolidates items 1, 8, 14"). It is not a work queue any more; this file is. +- **`docs/TODO.md`** — deferred follow-ups with recorded rationale, unrelated to the + seventeen (with one flagged intersection: see "Flagged for awareness" below). +- **`docs/COMPLETED.md`** / **`docs/ARCHIVE.md`** — doc-keeper's. When a track here + finishes, its point is removed from this file and appended to `COMPLETED.md` with any + deviation between landed code and spec noted. +- **`docs/product/`** — the product-design reasoning behind prior phases. Grep for a + cited section rather than reading a file whole. + +**Worktree slug convention:** `p-w-t-`. Greek phase letters +transliterate: **Θ → `th`**, **Ξ → `xi`**. So Θ-W1-T1 dispatches into +`pth-w1-t1-zone-retirement`. + +## Decision state + +**Nothing in this plan awaits a Daniel decision.** Every question carried forward from +`TODO-1.0.md` is classified **[verify]** (answerable by reading code or running the DAW) +or **[propose]** (a design call made at implementation review with a proposal, not a +Daniel call). That classification is preserved per question, attached to the track that +will answer it. + +### Flagged for awareness — not blocking, but decision-grade + +1. **Item 15 has an unresolved cross-artifact seam, and it is the phase's largest + unknown.** The instrument is a read-only bank consumer by invariant + (`src/shell/instrument/CLAUDE.md`), and the one previous attempt at an + instrument→extension relay (S13) closed with a **DEGRADED** spike verdict and was + deferred (`docs/TODO.md`). Resample requires that crossing. Ξ-W2-T1 opens with the + architecture decision and this plan names the candidates rather than assuming the + deferred relay shape; see that track. Consequence to hold: if every candidate fails + verification, item 15's "one click from inside the VST" framing is what gives, not + the read-only invariant — the fallback is a bindable extension-side action. +2. **Item 17's "100% robust" and `docs/TODO.md`'s deferred persisted-instance-identity + fix sit one day apart and point opposite ways.** On 2026-07-28 Daniel accepted + shipping the safe-but-incomplete usage tracking and deferring the persisted-nonce + fix; on 2026-07-29 he required the tracking be "consolidated and made 100% robust." + My reading: those do not conflict — "robust" is a *safety* strength claim (no + destructive act follows from ambiguity), while the deferred wart is a *completeness* + one (prune stops reclaiming after a reopen; the bank folder grows, nothing is lost). + Ξ-W1-T1 carries this as a [propose] question: fold the deferred fix in, or restate + the deferral explicitly in `docs/TODO.md` terms. I am flagging it because the + consolidation is the natural moment to do it, not because it blocks. +3. **Phase Ξ Wave 1 is concurrency-safe with Phase Θ** (from Θ-W2 onward). See + "Running Θ and Ξ concurrently" below. Whether to spend a specialist that way is a + scheduling call, not a plan decision. + +## Phase-wide acceptance criteria + +These bind every track in both phases and are stated once here rather than repeated +per track. + +### Structural (root `CLAUDE.md`, Daniel 2026-07-28) + +- **More directories is a must; more files is good; ~600-line file ceiling.** The + ceiling is the *bar*; a responsibility seam is the *method*. Bisection-to-hit-the- + number is rejected. `sampler_core.cpp` (956 lines) is the standing documented + hot-path exception — Θ-W1-T1 re-seams it, and any surviving over-ceiling TU must + carry the same explicit justification. +- **Templates where earned** — compile-time dedup with zero runtime cost, off the hot + paths. Not for types that differ in name only. +- **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts anywhere; + prefer static polymorphism where the types are compile-time-known. + +### Performance guardrails + +Root `CLAUDE.md`'s five extension-side guardrails (peaks envelope compute, audition, +realtime-capture tick, JSON, `FxBypassGuard`) are unchanged by this plan; no track here +touches them. The instrument adds a sixth surface that binds every Θ track: + +- **`process()` — the per-voice-per-sample path — takes no new indirection.** The + filter tick, the envelope evaluation (staged *and* spline), the pitch-shift read, and + the loop read all sit on it. Concrete, inlineable types only: **no `IEnvelope`, no + `IFilter`, no virtual per-voice `tick()`**. A filter with two modes is a + branch-predictable switch or a compile-time-known dispatch, never a vtable. Spline + evaluation is a binary search over a point array, not a polymorphic curve object. +- **No allocation, no file I/O, no bridge call in `process()`.** The off-audio-thread + `reloadInstrument` + atomic pointer swap stays the only way new state reaches the + audio thread. Every new parameter this plan adds follows that path. +- **A split that would add a hot-path indirection is out of scope — rework it or drop + it.** + +### Product invariants + +- **Capture and placement are separate acts.** No track here may place a timeline item. + Item 15's bake explicitly must not, and one of its candidate architectures uses a + *temporary* arrange mutation — that candidate must leave the arrange byte-identical. +- **Prune is the single, exclusive file-deletion authority.** Item 15's "replace" never + deletes bytes; item 17's consolidation may not weaken any protection prune has today. +- **The instrument never writes the bank.** Item 15 is the first feature that needs to, + and it resolves that by *asking the extension*, not by breaching the invariant. +- **Migration bar: a project saved before a change reopens sounding identical.** Holds + everywhere except item 16's genuinely-multi-zone case, where Daniel deliberately + relaxed it. +- **Every pure module gets a `_tests` target** that runs without REAPER or a + DAW. New pure modules in this plan are not optional-test. + +--- + +## Phase Θ — ReaSampler 9000: one parameter set, a filter, shapeable envelopes, a legible editor + +**Ships:** the instrument with the zone system retired, a resonant HP/LP filter stage in +the voice path, curve-shapeable and spline-drawable envelopes on all three EGs, +Gate-mode loop sustain, and a re-laid, high-DPI-clean editor — plus the two extension +drag/drop defects that block getting captures into it. + +**Consolidates items** 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16. + +**The organizing constraint.** Three surfaces in the instrument are single-writer by +nature and dictate the wave shape: + +1. **The parameter model** (`zone_params.h` + `component_state_io`) — every parameter + addition touches both. Two tracks adding parameters concurrently is a merge fight + and two competing `ComponentState` version bumps. +2. **The voice render path** (`sampler_core.cpp`) — filter insertion, envelope + evaluation, loop read, and the Trigger tail all live there. +3. **The Sample face** (`editor_paint_sample.cpp` 516 lines / `editor_input_sample.cpp` + 583 lines) — every UI item repaints it, and both are already at the ceiling. + +Parallelism in this phase therefore comes from **splitting the Sample face into bands** +(done once, in Θ-W1-T1) and from **extension-side work being genuinely disjoint** — not +from running two parameter-model tracks at once. Where a wave has one track, the +collision is real and the serialization is the correct answer. + +--- + +### Θ-W1 — Collapse and re-seam + +**Three tracks, in priority order.** This wave removes a whole subsystem, re-seams the +two files everything downstream must edit, and 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-T1 — `zone-retirement` + +**Goal.** Retire the zone mapping system: ReaSampler 9000 becomes one capture = one +parameter set — and, in the space that deletion clears, land the two responsibility +seams the rest of the phase depends on. + +**Consolidates item 16.** (Also discharges the superseded per-zone storage language in +items 2, 9, 11, 12 before those items are built.) + +**Surface boundary — owns:** +- `src/core/instrument/map/` — `sample_map` (zone payload → one parameter set), + `component_state_io` (zones-payload version bump + migration). +- `src/core/instrument/engine/` — `zone_params.h`, `sampler_core.{h,cpp}` (the + responsibility seam). +- `src/core/instrument/ui/` — `editor_geometry` (Zone-face layout removal + the band + seam), `keyboard_strip` (zone-highlight geometry removal). +- `src/shell/instrument/` — all eight editor face-axis TUs, `editor_internal.h`, + `processor_state.cpp`, `reasampler_processor.{h,cpp}`. +- `CMakeLists.txt` target list (shared, low-conflict). + +**Behavior.** +- **One capture = one parameter set.** The instrument holds one loaded capture and one + set of playback parameters governing it. No zones, no per-zone divergence, no keymap + of captures. Every playback parameter edits in exactly one place; no gesture can + express per-zone divergence. +- **The zone-mapping surface goes away** — the dedicated zone-editing face and its + authoring affordances (add/delete zone, the per-zone parameter panel, the Low/High/ + Root zone legend). **The root note survives as a first-class parameter of the one + set** — only its zone-legend housing goes. +- **The per-zone/per-instance storage distinction collapses.** There is nothing for + VOICE and MASTER to be exceptions to and no second surface to hold in parity. +- **Migration: adopt the first zone's capture and parameters.** A saved multi-zone + instance lifts by adopting its first zone's capture and that zone's parameters; the + remaining zones drop from the instance. Dropping a zone touches no file and no bank + entry. The sounds-identical bar is **deliberately relaxed** for a genuinely + multi-zone instance — Daniel has no projects with zones in use, so it has no real + referents. Single-zone instances lift losslessly. +- **The single-capture experience is unchanged** — a loaded capture still plays across + the keyboard repitched from its root, key-tracking still applies. +- **No effect on the bank or the extension.** Captures, banks, capture/placement + separation, and the prune safety model are untouched. + +**Structural deliverables (this wave's real output for everything downstream).** +- **The `sampler_core` responsibility seam.** With zone resolution gone, what remains + divides cleanly: note routing / allocation / stealing / mono stack / panic (per-note, + cold) versus the per-voice per-sample render (hot). Split along that seam. Envelope + evaluation comes out as a concrete, header-inline evaluator — **never** a virtual + `tick()` on the per-voice path (structural heuristic 3 names this blowout by name). + The exact seam is [propose]; the constraint is not. +- **The Sample-face band seam.** Split `editor_paint_sample` / `editor_input_sample` / + the Sample-face half of `editor_geometry` into **three bands — chrome, waveform, + decks** — with one pure band-stack allocator owning the vertical inventory. Land the + band inventory the phase needs (chrome band, waveform band sized for two stacked + lanes, deck row) so W2's three tracks *fill* bands rather than re-allocate them. The + band allocator is a shared read-only surface from W2 on; no later track re-owns it + without saying so. +- Both TUs are at the ~600 ceiling before this work; neither may exit it over. + +**Acceptance criteria.** +- The editor exposes no zone-mapping surface anywhere: no zone view or button, no + add/delete-zone affordance, no per-zone parameter panel, no Low/High zone legend. The + root-note control survives. +- Every playback parameter edits in exactly one place and governs the instrument as a + whole; no gesture can express per-zone divergence. +- A single-capture instance saved before this change reopens sounding identical — same + keyboard response, same parameters, same root. +- A multi-zone instance saved before this change reopens holding its first zone's + capture with that zone's parameters — no error, no file touched, no bank entry + disturbed. +- The one parameter set round-trips save/reload intact. +- Bank, capture, placement, and prune behavior are unchanged. +- `sampler_core` and the Sample-face TUs are split by responsibility; every resulting TU + is under ~600 lines or carries an explicit hot-path justification. No virtual dispatch + is introduced on the per-voice per-sample path — verify by inspection of the split. + +**Open questions.** +- **Does any key-range concept survive? [propose]** With zones gone, does the capture + respond across the entire keyboard (repitched from root), or does a user-settable + low/high playable range survive as a plain parameter of the one set? Lean, consistent + with the simplification motive: **no range concept — full-keyboard response** — with + a low/high pair re-addable later as ordinary parameters if missed. **Θ-W2-T3 consumes + this answer** for the piano strip's contents; answer it here. +- **Which zone is "first"? [verify]** The instance's existing zone ordering. No + real-world stakes given the migration rationale. +- **The two responsibility seams [propose]** — see Structural deliverables. + +--- + +#### Θ-W1-T2 — `capture-handoff-bugs` + +**Goal.** Fix the two extension-side defects that break getting a capture out of the +bank and into a target — one intermittent, one deterministic. + +**Consolidates items 5, 6.** + +**Surface boundary — owns:** +- `src/shell/actions/drag_out_win.{h,cpp}`, `src/shell/actions/instrument_drop_win.{h,cpp}` +- `src/core/ui/drag_out`, `src/core/wire/instrument_drop` +- `src/shell/panel/panel_drag.cpp` +- `src/ingest.{h,cpp}` (physically at `src/` root — see rider below) + +Fully disjoint from the instrument; runs concurrently with T1 and T3 without contact. + +**Behavior.** +- **Item 5 — drag-out sometimes lands without audio.** Extension side, not the VST. + Dragging a capture out of the pool or a named bank *sometimes* produces a drop that + does not contain the audio; retrying works. Both details are load-bearing: the failure + is intermittent and a retry succeeds. Expected: every completed drag-out delivers the + capture's audio at the drop target, first try, every time. +- **Item 6 — drop onto FX container loads the instrument without the capture.** + Dropping a capture onto an FX *container* loads a ReaSampler 9000 instance but without + the capture; dropping the same capture onto the FX *button* works as expected. The + FX-button path is the reference; the container path must match it. + +**Acceptance criteria.** +- **Item 5 (soak gate — the failure is intermittent, so a single pass is not a gate):** + across a sustained session of varied drag-outs — pool and bank sources, including the + first drag after other bank activity — every drop yields a playable file containing + the audio, with no retry ever needed. +- **Item 6:** a capture dropped onto an FX container yields an instrument instance with + that capture loaded and immediately playable, indistinguishable (apart from where the + FX sits) from the FX-button drop. The FX-button path remains unregressed. +- `instrument_drop`'s all-or-nothing contract holds on the container path too: any + failure rolls back via `TrackFX_Delete`, leaving no orphaned instance. + +**Open questions.** None carried from the source items (both are bug reports recorded +without root-cause analysis, per the no-code-reads constraint they were written under). + +**Opportunistic rider (not required; propose at review).** `ingest.cpp` / `ingest.h`, +`ext_keys.h`, and `resource.h` physically live at `src/` root rather than under +`shell/actions/` — a documented structural wart Phase Q's reorg did not close. This +track is the only one in the phase that opens `ingest`; re-homing it here is cheap and +otherwise needs its own dispatch. Take it or explicitly decline it. + +--- + +#### Θ-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." + +--- + +### Θ-W2 — Filter in the voice path; waveform and chrome bands + +**Depends on W1 for:** the one-parameter-set model and the `sampler_core` seam that T1 +writes the filter into; the `filter` module T1 wires up; the Sample-face band split and +band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which decides +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-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. + +--- + +#### Θ-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 +*three* envelopes, and the filter is the third; and for the Filter deck, which must +exist before it can receive a corner radio switch and inner curve dials. + +**One track.** This is the phase's densest collision: the envelope parameter model, the +per-sample envelope evaluation, the deck-band inner dials, and the waveform-band overlay +are one system with one source of truth (the settled S-VIEW rule: node-drag and +slider-edit read/write the same model, structurally, not via a listener chain). Splitting +it into model and UI tracks would put two writers on `zone_params.h` + +`component_state_io` and two writers on the overlay's forward/inverse map pair. The +serialization is the correct answer. + +#### Θ-W3-T1 — `staged-envelope-curves` + +**Goal.** Grow the envelope-overlay editor from an amp-only fixture into the shared +graphical surface for every envelope, give every envelope shapeable segments, fix the +overlay's layout and contrast failures, consolidate the Trigger amp fade pair into the +AHD — and, in the same region, kill the Trigger×Preserve end-of-sample click. + +**Consolidates items 1, 8, 14, 4.** + +**Surface boundary — owns:** +- `core/instrument/engine/` — the envelope evaluator (from W1's seam), `zone_params.h`, + `pitch_shift.{h,cpp}` (item 4's tail), the voice-render envelope and tail path. +- `core/instrument/map/` — `trigger_seam`, `component_state_io` (curve exponents, the + pitch Hold stage, the Trigger AHD, migration). +- `core/instrument/ui/` — `envelope_overlay`, `envelope_edit`, `knob_deck` (the inner + dial). +- `src/shell/instrument/` — the deck band (inner dials, radio switches) and the + waveform band's **overlay layer only** (T2's lane geometry from W2 is consumed, not + re-owned). + +**Behavior — the overlay surface (item 1).** +- **Radio switch per envelope deck.** Each envelope knob-deck group (AMP ENVELOPE, + PITCH ENV, and now Filter) gains a radio switch in the corner of its deck. Selecting a + deck's radio makes *that* envelope the one displayed and editable in the graphic + waveform overlay — replacing today's always-on Amp AHDSR. The switch is exclusive: + **one overlay-active envelope at a time, or none** — no-envelope-shown is a valid + state, not an error. +- **Default overlay: none.** The editor opens with **no envelope selected**. +- **Segment curve values on all envelopes.** Amp AHDSR, Pitch AHD, and Filter AHDSR all + gain an editable curve value per *sloped* segment. The curve is exponential; the + per-segment parameter is the exponent scalar, range **0.1 to 10**. **1.0 is the linear + neutral** (y = x^1.0 is linear). +- **Which segments are sloped: every stage except Hold and Sustain.** For an AHDSR that + is Attack, Decay, Release; for the Pitch AHD, Attack and Decay. Hold is flat + everywhere and carries no curve dial. +- **Inner dial on curvable-segment knobs.** Every knob controlling a sloped, curvable + segment gains an **inner dial**: its own inner arc, its own hover accent (tertiary + purple), its own needle, its own numerical label. The inner dial controls that + segment's curve exponent; the outer knob keeps controlling the segment's time/level + value as today. +- **Curve editing in the overlay.** Dragging on a segment **adds a round mid-segment + knot** whose drag adjusts that segment's curve — the overlay is a curve-edit surface + in its own right, alongside (not instead of) the inner dial. Same one-model rule as + the existing draggable nodes: knot-drag and inner dial cannot diverge. +- **Overlay recolor.** Envelope segments change from secondary blue to **tertiary + purple** — secondary blue contrasts poorly against the primary green waveform behind + it. Re-verify the tertiary-purple-on-green pair against `theme`'s contrast-floor + assertions. +- **Pre-existing instances load at exponent 1.0** — the linear neutral — so their + audible envelope behavior is unchanged. + +**Behavior — layout policy and the pitch AHD (item 8).** +- **The release segment anchors to the right edge** of the overlay. Release is dragged + from its **top node** (the node joining sustain to release) instead of the bottom + corner; the bottom corner — the envelope's end point — becomes **fixed/anchored**, not + draggable. Today's failure is that with little or no release the sustain portion + occupies only a small stretch and the whole figure reads off-center. +- **Spec change: the pitch envelope becomes AHD** — Attack → Hold → Decay. **Hold ranges + 0 to 100% of the time remaining after Attack and Decay** (100% of + `sample length − (attack + decay)`). At 100% Hold fills all remaining time; at 0% it + takes none. +- **The A + H + D ≤ sample-length bound holds by construction, not by a clamp.** Hold is + expressed as a fraction of what is left, so the sum cannot overflow. Do not add a + separate clamp; none is needed. +- **Scope rule — split on the sustain stage.** The 1:1-overlay property and the + combined-time bound apply to **all envelopes that do NOT have a sustain stage**; + envelopes **with** a sustain stage get the right-anchored-release policy instead. The + two policies **coexist rather than merge**. Daniel's rationale: the 1:1 mapping only + makes sense for trigger, not gated envelopes. **The pitch envelope's D is not a + release, so it is not right-anchored.** + +**Behavior — mode-driven envelope shape (item 14).** +- **Trigger mode: fades out, AHD in.** In Trigger mode the amp envelope becomes an AHD + per item 8's definition. The fade-in-length and fade-out-length controls **go away in + Trigger mode**; Attack and Decay carry those roles. Daniel's motive is stated: + reducing code for the same job — one staged-envelope design covering what is currently + two mechanisms. +- **The Trigger AHD's sloped segments get the full curve treatment** — exponent 0.1–10, + inner dials, mid-segment overlay knots, tertiary-purple rendering. +- **Gate mode unchanged** — the amp envelope stays the AHDSR with the right-anchored + release. +- **The filter envelope follows the same rule.** Stated once, governing all three: + **pitch is always AHD (1:1 overlay); amp and filter are AHDSR in Gate + (right-anchored release) and AHD in Trigger (1:1 overlay).** The rule selects by the + envelope's **current shape under the active playback mode**, not by which processor it + modulates. No new rule is needed — item 8's rule already decides every case. +- **Pre-existing instances reopen sounding identical.** A Trigger instance's prior + fade-in/fade-out contour must be reproduced by the loaded AHD. The evident mapping + (Attack ← fade-in, Decay ← fade-out, Hold ← the full remainder, exponents at whatever + reproduces the prior fade shape) is a verify-at-implementation detail. **A prior zero + fade-out is Decay = 0** — the abrupt end stays representable, so nothing the old + controls could express is lost. + +**Behavior — the Trigger×Preserve click (item 4).** +- In Trigger mode — polyphonic or monophonic alike — with the pitch engine in + **Preserve mode only**, the end of the sample produces an audible click. The scoping is + the useful part of the report and is recorded as given: Trigger × Preserve × + end-of-sample; Varispeed is not implicated. +- Expected: a Trigger one-shot in Preserve ends silently — no click or discontinuity at + the sample end, in either voice mode. +- **Why it is in this track and not earlier:** the defect sits in the region the + fade-out currently governs, and this track retires that mechanism. Fixing it in a + prior wave means fixing it, then re-verifying it under the surviving mechanism + anyway — and the only other instrument-side track before this one owns the whole + engine. The gate below is the *post-consolidation* gate. + +**Acceptance criteria.** +- Each envelope deck shows a corner radio switch; activating one puts that envelope in + the overlay, editable there, and the overlay tracks the switch immediately. At most one + envelope is overlay-active; with none active the overlay draws no envelope. +- The editor opens with no envelope overlay-active. +- Every curvable-segment knob shows the inner dial (inner arc, tertiary-purple hover + accent, needle, numeric label); sweeping it 0.1 → 10 visibly reshapes the overlay + segment and **audibly** reshapes the envelope on played notes. Hold and Sustain knobs + carry no inner dial. +- Dragging on an overlay segment adds a round mid-segment knot; dragging the knot adjusts + that segment's curve, and the segment's inner dial reflects the same value. +- Overlay envelope segments render in tertiary purple and are clearly legible against the + primary green waveform. +- With release at zero or minimum, the sustain segment extends to (near) the right edge — + the overlay reads full-width, not bunched left. Dragging the sustain→release top node + adjusts release; the bottom-right corner is fixed and not draggable. +- Envelopes with a sustain stage follow that anchoring policy; the pitch envelope's Decay + is **not** right-anchored. +- The pitch envelope plays and displays three stages — Attack, Hold, Decay — with Hold + spanning 0–100% of the time remaining after Attack and Decay. **No combination of A, H, + D exceeds the sample length, and no clamp fires, because none exists.** +- The overlay of any sustain-less envelope is 1:1 with the waveform's time axis: a stage + boundary at N seconds sits over the waveform at N seconds. +- In Trigger mode the amp deck shows Attack / Hold / Decay — **no fade-in or fade-out + control anywhere in Trigger mode** — with curve inner dials on Attack and Decay and + none on Hold. +- Switching between Gate and Trigger switches the amp *and* filter envelope surfaces + (deck and overlay) between AHDSR and AHD accordingly. +- The Trigger amp AHD behaves stage-for-stage like the pitch AHD — Hold semantics, curve + treatment, and overlay mapping match. (The observable proxy for the consolidation + motive: one staged-envelope design, several consumers.) +- **Migration:** a project saved before this change reopens sounding identical — every + curve at exponent 1.0, and a Trigger instance's prior fade contour (including a zero + fade-out's abrupt end) audibly reproduced by the loaded AHD. +- **Item 4's gate, under the surviving mechanism:** a Trigger-mode note played in + Preserve, in both Poly and Mono, ends with no audible click at the sample end — + verified by ear *and* by inspecting the rendered output for a terminal discontinuity. + Varispeed playback is unchanged. + +**Open questions.** +- **Stage-value state across the Gate/Trigger switch — shared or per-mode? [propose]** + The Gate AHDSR and Trigger AHD share stage names (A, H, D); whether they share *values* + (one envelope whose S and R fall away in Trigger) or keep per-mode state (item 3's + "save but inactive" dual-state precedent) is unspecified — and the question covers the + **filter envelope too**. Migration leans per-mode for the amp: an old instance carries + both its AHDSR values and its Trigger fade values, and a shared-value model cannot + preserve both modes' prior sound at once. The filter is new and carries no migration + weight either way; matching the amp's answer is the natural default. Resolve with a + proposal at review — the sound-identical gate must hold for whichever mode a saved + instance plays in. + +--- + +### Θ-W4 — Loop sustain and the velocity deck + +**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain +is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves +modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for +the filter's existence and on W2-T3 for the preview button's toolbar position. + +**Two tracks, in priority order.** Disjoint: T1 owns the engine loop path and the +waveform band's marker layer; T2 owns the deck band, the curve popup, and the preview +button in the chrome band. Neither touches the other's band. + +#### Θ-W4-T1 — `gate-loop-sustain` + +**Goal.** Establish (or restore) loop points, and make a Gate-mode loop function as the +sustain — indefinite playback until note-off, with a crossfaded seam. + +**Consolidates item 9 (both halves: the suspected regression and the feature spec).** + +**Surface boundary — owns:** the voice-render loop read path (post-W1 seam), +`core/instrument/map/component_state_io` (loop parameters), `core/instrument/ui/ +waveform_view` markers, and the **waveform band's marker layer**. Reads W2-T2's lane +geometry and W3's overlay contract; re-owns neither. + +**Behavior.** +- **Regression half.** Daniel's observation: loop points appear to have been lost. + Whether they were genuinely removed from playback or are merely unexposed in the + current UI **must be verified as the first act of implementation** — it was not + guessable in the source doc, which was written under a no-code-reads constraint. +- **Feature half.** In **Gate mode**: a loopable section with **parameterized start and + end points** and a **parameterized crossfade on loop reset**. The loop functions as the + sustain — indefinite playback cycling the loop until note-off, then release. +- **Loop-sustain is a Staged/Gate-mode feature.** Gate mode is unavailable while a + Spline EG is active (item 3, Θ-W5), so Spline mode remains full-sample-length playback + with no loop. This track must not assume Gate is always available. +- **Storage:** loop parameters belong to the instrument's one parameter set. There is no + per-zone side to confirm. +- **Preserve-mode contract (existing, do not break):** *loop the source, shift the + output* — loop points stay source-frame facts. +- **Reset-scope note for Ξ:** item 15 classifies loop points as **baked-in** — they + shaped the render and old positions are meaningless against new audio — so they reset + on a resample. Nothing to build here; recorded so Ξ-W2-T1 does not re-derive it. + +**Acceptance criteria.** +- In Gate mode with a loop defined, a held note sustains indefinitely, audibly cycling + the loop section; note-off exits into the release stage. +- With a nonzero crossfade the loop seam is smooth — **no click at the loop reset** — and + crossfade length audibly follows its parameter. +- Loop start, end, and crossfade are user-parameterized, editable, and persisted across + save/reload. +- Whatever the regression finding, the end state is loop points exposed and functional + per this spec. +- Looping adds no allocation to `process()`; the crossfade is computed on the per-sample + path without a branch-heavy or virtual detour. + +**Open questions.** +- **Regression verification — removed vs. unexposed [verify].** The first act of + implementation. Report the finding; the end state is the same either way. +- **Crossfade units and range [propose].** Unspecified. Surface a proposed default at + implementation review (ms is the likely denomination given item 10 puts time constants + in ms). +- **Editing surface for loop start/end [propose].** Waveform markers, knobs, or both, is + unspecified. The waveform display is the natural home for range markers — and + `waveform_view` already owns generic named draggable markers with clamp and + zero-crossing snap, which is a strong lean — but Daniel has not said. Propose at review. + +--- + +#### Θ-W4-T2 — `velocity-deck-and-bipolar-curves` + +**Goal.** Give the three velocity-curve popups one home, make the pitch and filter curves +bipolar and off-by-default, and replace the preview button's text with a glyph. + +**Consolidates item 11.** + +**Surface boundary — owns:** `core/instrument/engine/velocity_curve` (bipolar y-domain), +`core/instrument/ui/curve_popup`, the **deck band** (the new VELOCITY group), and **the +preview button in the chrome band** at the toolbar position W2-T3 gave it. Does not touch +the waveform band. + +**Behavior.** +- **The VELOCITY deck.** All three velocity-curve popup buttons (amp, pitch, filter) live + **together in a new control deck group labelled "VELOCITY", placed to the left of the + VOICE group.** This **supersedes the original ask's per-section placement** — Daniel + confirmed the MASTER placement was a real contention point: **MASTER is reserved for + other, post-voice-mixer concerns** he will add to later, so the velocity curves do not + belong there. No velocity-curve button appears in MASTER, PITCH, or Filter. +- **Bipolar pitch/filter transfer functions.** Pitch and filter velocity transfer + functions are **bipolar: y range [−1, 1], default y = 0** — flat at zero, meaning + velocity modulation of pitch and filter is **off until the user draws a curve**. +- **Amp stays unipolar at [0, 1]**, and its existing flat-unity default is unchanged. +- **Storage:** the curves live in the instrument's one parameter set. +- **Preview glyph.** The preview button's inner text is replaced with a glyph. Proposed + at product level: a right-pointing **play triangle** — the universal audition read. + **Daniel's constraint: no new dependencies** — a statically embedded bitmap or + equivalent that plays nicely with the existing LICE drawing path. + +**Acceptance criteria.** +- The three velocity-curve buttons sit together in a deck group labelled **VELOCITY**, + immediately to the left of the VOICE group; no velocity-curve button appears in MASTER, + PITCH, or Filter. +- Opening the pitch or filter curve shows a **bipolar editor ([−1, 1]) defaulted flat at + y = 0**; played velocities produce no pitch/filter modulation until a curve is drawn, + then audibly follow it. +- The amp curve's domain ([0, 1]) and flat-unity default are unchanged. +- The velocity curves persist in the one parameter set and round-trip save/reload. +- The preview button shows the glyph (no text) and stays legible in **all** interaction + states; **no new build or runtime dependency is introduced.** + +**Open questions.** +- **Does a user-facing pitch velocity transfer curve already exist, or does this item + introduce it? [verify]** Unverifiable in the source doc under its no-code-reads + constraint. If absent, this track introduces it. Check before scoping. +- **Bipolar rendering in the shared spline editor [propose].** Item 3 (Θ-W5) enhances the + one spline implementation for hard points; this track must render and edit a bipolar + y-domain alongside the amp curve's unipolar one. Land the bipolar domain here in the + existing editor; Θ-W5 then adds hard points to the same editor without re-deciding the + domain. + +--- + +### Θ-W5 — Spline EGs + +**Depends on W4 for:** the bipolar velocity-curve domain (W4-T2) — the spline algorithm +is **singly implemented and multi-referenced**, so its enhancement must land against the +final consumer set, and the last consumer to change domain is the pitch/filter velocity +curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline mode" is +only a real, testable rule once Gate has something to be unavailable *for*. It also +depends on W3's radio switch, which is how a spline contour reaches the overlay at all. + +**One track.** The spline algorithm is one implementation with several consumers; a +second concurrent writer to it collides by construction. + +#### Θ-W5-T1 — `spline-egs` + +**Goal.** Offer a free-drawn alternative to every staged envelope: switch any EG from +Staged to Spline and draw the contour directly — with the monotone-spline machinery +already proven by the velocity curve, enhanced so sharp corners are possible rather than +everything smoothed. + +**Consolidates item 3.** + +**Surface boundary — owns:** `core/instrument/engine/velocity_curve` (the shared spline +implementation — hard points), the spline evaluator on the voice-render path, +`core/instrument/ui/curve_popup` + the overlay's spline mode +(`envelope_overlay`/`envelope_edit`), `core/instrument/map/component_state_io` (dual +state persistence), and the deck band's **disabled-knob rendering only**. + +**Behavior.** +- **Mode toggle per EG.** Every processor that has an envelope (pitch, filter, amp) can + switch its Staged EG to a **Spline EG**. +- **Dual state — save but inactive.** Both the Staged and the Spline state are persisted; + switching modes keeps the inactive one **saved but inactive**. No conversion, no + discard — round-tripping Staged↔Spline restores the other mode's shape untouched. +- **Gate unavailable in Spline mode.** Gate mode is **not available while a Spline EG is + active**; the spline **always covers the full sample length** — a pure time function + over the sample, i.e. the Trigger/one-shot playback model. +- **Time axis: normalized, visually 1:1.** The contour is normalized to the full sample + length and the overlay represents **the time axis of the actual sample visually 1:1**. + Consequently a different-length capture rescales the stored contour to its own length, + shape preserved proportionally. +- **Point-editing grammar. Left-click adds a point; right-click deletes a point; + control-click toggles a point hard/smooth.** (The source doc's second follow-up said + alt-click delete; the third **supersedes** it with right-click — matching the + velocity-curve popup's already-shipped right-click node delete, giving **one + point-editing grammar across both spline consumers, with no migration on either side**.) +- **Point-count ceiling: 128.** Floor: the two endpoints implied by full-length coverage. + Daniel floated 64 and raised it to 128 explicitly so the cap does not limit **long + rhythmic phrases, which require high resolution to be interesting** — at roughly two + points per articulation event, 64 is about two bars of 16ths and 128 about four. **The + ceiling is a musical bound, not a performance one** (segment lookup is logarithmic; the + editor's 8 px minimum node separation is the practical density limit anyway). **An + engineer tempted to lower this number must read that motivation first.** +- **Staged controls disabled while Spline is active.** That envelope's staged segment + knobs are **disabled — inert, not merely inaudible** — including their inner curve dials + (the dial is part of the knob). The dormant staged state is edited only by switching + back to Staged. +- **Not globally monotone.** Contours don't have to rise and fall and are not globally + monotone; the monotone guarantee is **per-segment** — all soft points between any hard + points are smooth/monotone, with no overshoot between adjacent points. +- **Default contour: a smooth y = 1 − x** — a smooth downward slope over the full sample + length. (The Spline-EG default; not a change to the velocity→amp curve's existing + default.) +- **Hard points — the algorithm enhancement.** The one spline implementation is enhanced + to support **multiple segments that don't minimally smooth**, so hard points are + possible: the contour is defined by **one or more monotone spline functions** which + together form the full time function — e.g. the first three points form a curved segment + connecting **at a sharp angle** to the next three, finishing the contour over the full + sample length. A hard point does no curve smoothing on either adjacent segment; the + natural sharp angle stands instead of a continuous derivative. +- **Shared across consumers.** The enhancement applies to the one implementation and flows + to every consumer: the **velocity→amp transfer curve** gains hard-point support by the + same change, as do the pitch, filter, and amp EGs in spline mode. +- **Hot-path constraint.** Spline evaluation runs per-voice per-sample. It is an indexed + binary search over a point array plus a polynomial evaluation — **not** a polymorphic + curve object, and not a per-sample allocation. With a 128-point ceiling the search is + ≤7 steps; cache the segment index across samples where the read position is monotone. + +**Acceptance criteria.** +- Each of the pitch, filter, and amp EGs offers a Staged/Spline mode switch; in Spline + mode the overlay (via the W3 radio switch) shows and edits the drawn contour, and played + notes audibly follow it. +- Left-click on the contour adds a point; right-click on a point deletes it; control-click + toggles it hard/smooth. Points are smooth by default; a hard point renders a **visible + sharp angle with no smoothing on either adjacent segment**, and the discontinuous slope + is audible where the modulation target makes it so (e.g. a pitch EG corner). +- A contour accepts points up to the **128-point ceiling**; attempting to add beyond it is + refused **without disturbing the existing contour**. The two endpoints cannot be deleted. +- A contour of several segments joined at hard points plays back over the full sample + length exactly as drawn — including contours that rise and fall freely (no + globally-monotone restriction), with **no overshoot between any adjacent pair of + points**. +- A freshly created Spline EG shows the smooth y = 1 − x default contour. +- While a Spline EG is active, **Gate mode is not selectable**; the spline plays as a pure + time function over the full sample length. +- The overlay contour spans the full displayed sample width, 1:1 with the sample's time + axis; loading a different-length capture rescales the contour to the new length with the + drawn shape preserved proportionally. +- While a Spline EG is active, that envelope's staged segment knobs **and their inner + curve dials** render disabled and reject edits; switching back to Staged re-enables them + with values exactly as left. +- **Staged↔Spline round-trip preserves both states**: switch to Spline, draw, switch back + — staged values exactly as left; switch forward again — the contour exactly as drawn. + Both survive save/reload. +- The velocity→amp transfer-curve editor supports the same control-click hard/smooth + toggle with identical rendering behavior, and its existing right-click node delete + matches the Spline EG's delete gesture unchanged — **one point-editing grammar across + both consumers.** +- Spline evaluation adds no allocation and no virtual dispatch to `process()`. + +**Open questions.** None — item 3's two remaining questions (point-count bounds; gesture +convergence) closed in the source doc's third follow-up round. + +--- + +### Θ-W6 — Editor legibility pass + +**Depends on W5 for:** the last change to a drawn surface. Item 13 is an audit whose +output is a disposition list over "every class of drawn surface," and item 10's sizing +pass is judged by eye over the finished layout — running either while the spline contour, +the disabled-knob state, or the deck inventory is still moving means auditing and then +re-auditing. The source doc names this sequencing as an observation (item 13 after the +layout/knob work); this plan adopts it as the boundary. + +**One track.** Both items repaint essentially every surface in the editor; concurrent +tracks would collide everywhere. + +#### Θ-W6-T1 — `legibility-and-antialiasing` + +**Goal.** Make the editor legible — bigger knobs and labels, time constants in ms, +double-click reset per ring — then audit every drawn surface for high-DPI clean rendering. + +**Consolidates items 10, 13.** Sequence internally: sizing first, audit second. + +**Surface boundary — owns:** `core/instrument/ui/knob_deck` (sizing geometry), +`shell/panel/draw_kit` (the shared LICE draw shell — arcs, lines, slopes), and all three +Sample-face bands for repaint-level changes. **Note the blast radius:** `draw_kit` is +shared with the docked bank panel, so an antialiasing change there lands on the extension +UI too. That is desirable, but it puts the panel in this track's verification scope. + +**Behavior — sizing and ergonomics (item 10).** +- **Radial knobs and their text labels grow** — both are currently too small. No target + size was given; this is a visual-judgment change accepted by eye. +- **Time-constant labels display in ms, not seconds.** A display-unit change; this makes + no claim about internal representation. +- **Double-click on any radial knob resets it to its default value.** +- **On dual-ring knobs, each ring is its own reset target**: double-click the outer ring + resets the time/level value; double-click the inner curve dial resets the exponent to + **1.0** (the settled linear neutral) — each independently, without touching the other. + +**Behavior — antialiasing audit (item 13).** +- **A review pass, not a point fix.** Audit each class of drawn surface and confirm it + renders with antialiasing (or an equivalent) suitable for high-DPI, high-resolution + displays. Where one renders visibly aliased, bring it to the smooth standard. +- Daniel named the visibly pixely surfaces: **radial arcs, waveform lines, envelope + segment slopes.** By the time this runs, those include the inner dials (W3), the stereo + lanes (W2-T2), the staged *and* spline segment slopes (W3, W5), the loop markers + (W4-T1), and the piano key edges (W2-T3). +- The outcome is observable, not procedural. + +**Acceptance criteria.** +- Knobs and labels are legibly larger; **Daniel signs off on the result by eye.** +- **Every** time-constant label reads in ms. +- Double-click resets any radial knob to its default; on dual-ring knobs, double-clicking + the inner dial resets **only** the exponent (to 1.0) and double-clicking the outer ring + resets **only** the value. +- Radial arcs (including the inner dials), waveform lines (including the stereo lanes), + and envelope segment slopes (staged **and** spline) render smooth — **no visible jaggies + at 100% scale or on a high-DPI display.** +- **The audit produces a short disposition list**: surfaces checked, which needed work, + which were already clean. A required deliverable, not a side effect — it is what makes + the audit re-runnable. +- Daniel signs off by eye on the named surfaces. +- The docked bank panel is re-verified for regression, since `draw_kit` is shared. + +**Open questions.** +- **Target sizes [verify by eye].** No numeric target was given; item 10's own note is + that sizing is judged by eye. Bring a build, not a spec. +- **Whether the piano-key width defect was aliasing [verify].** W2-T3 fixes uniform width + regardless of cause; this audit confirms whether aliasing was the cause and whether the + fix survives DPI scaling. + +--- + +## Phase Ξ — The resample loop + +**Ships:** one consolidated, fully robust provenance/usage tracking system, and on top of +it the one-click in-sampler resample — dial → bake → dial again, with the bank as the +medium each iteration passes through. + +**Consolidates items 15, 17.** + +**Why these two and not more.** Item 17 is a **prerequisite of meaning** for item 15's +bank-side half: the replace-vs-add rule is "does provenance-tied usage exist," which +denotes nothing until the consolidated lineage records exist. Item 17 is otherwise +independent of the editor chain — which is what makes Ξ-W1 concurrency-safe with Θ. + +**Ξ-W2 onward requires Phase Θ complete.** Item 15 presupposes the processing surface it +bakes — "filtering, pitching, amp all set up nice" is the instrument items 1, 2, 3, and 14 +build — and its settled reset scope enumerates the filter parameters, the spline contours, +and the loop points by name. Baking a processing chain that does not exist yet is not a +schedule preference; the feature is not expressible. + +--- + +### Ξ-W1 — Consolidated tracking, and the programmed-note model + +**Depends on:** nothing in this phase. **Concurrency-safe with Phase Θ from Θ-W2 onward** — +see the note at the end of this phase. + +**Two tracks, in priority order.** Disjoint: T1 is entirely extension-side record-keeping; +T2 is a new pure module with no existing call site. + +#### Ξ-W1-T1 — `tracking-consolidation` + +**Goal.** Make the provenance/usage territory **one system, 100% robust** — recipe +provenance, live-instance usage, and recapture lineage as facets of the same +record-keeping, answering both safety-critical consumers from one place. + +**Consolidates item 17.** + +**Surface boundary — owns:** `core/model/provenance`, `core/model/owned_manifest`, +`core/wire/sample_usage`, `core/reclaim/prune_reconcile`, `shell/persist/` +(`usage_scan`, `prune_fs`, `ext_state_io`'s manifest/provenance keys), +`shell/actions/prune_action`. Instrument-side touch is limited to the usage-publish block +at the tail of `reloadInstrument` in `shell/instrument/processor_reload.cpp` — **this is +the one file shared with Phase Θ; if run concurrently, this track owns that block and +Θ-W1-T1 does not touch it.** + +**Behavior.** +- **One system, not three mechanisms.** Today the territory holds two separately-grown + mechanisms plus one new demand: + 1. the **capture-recipe fingerprint** — a thin reproducibility record of how a capture + was made, deliberately not a restorable chain, conservatively recording nothing when + the situation is ambiguous; + 2. the **instance-usage tracking** — each live instance declares the captures it holds, + so prune can never delete a capture a live instance is using, with a fail-safe stance + that unreadable usage state halts prune entirely; + 3. item 15's demand for **recapture lineage** — records tying usages of a capture to it + "by the provenance/recaptureing system," deciding replace-vs-add at bake time. + These stop being separate ad-hoc mechanisms. Every consumer — the prune's protection + decision, the resample's replace-vs-add decision, and any future lineage reader — is + answered from the one system. +- **What "100% robust" observably means.** Daniel stated the strength, not the mechanics. + Three observable implications follow from the stakes, and no further specifics are + invented: + - **No silent gaps.** Every capture the system itself creates is tracked from the moment + of its creation; a recapture carries its lineage **from birth, never backfilled**. + There is no window in which a system-created file exists untracked. + - **Fail-safe on unreadable or ambiguous state.** Tracking state that cannot be read + never yields the destructive answer: the prune deletes nothing (today's settled stance + — preserved and generalized, not relaxed), and the resample never takes the replace + branch on unreadable lineage. + - **Consumers cannot disagree.** The prune's protection answer and the resample's + tied-usage answer are different questions with different universes — item 15 settles + that the replace-vs-add universe is **narrower** than the prune-protection universe — + but both are computed from the same records, so they cannot drift apart. +- **Existing guarantees are the floor.** Consolidation must not weaken anything settled: + prune still deletes only the system's own orphans and never a referenced or live-held + capture; the fail-safe abort on unreadable usage state survives; the recipe + fingerprint's record-nothing-when-ambiguous conservatism survives **for the recipe + half**. The lineage half is the one place that conservatism is foreclosed — item 15's + replace-vs-add must be computable, so a recapture's lineage record is mandatory. +- **Why this is safety-critical, stated once:** this territory gates the system's only + file-deletion authority (prune) and its only capture-replacement act (resample) — the + two places where a tracking error loses a user's audio or sound. + +**Acceptance criteria.** +- **One consolidated tracking system answers both safety-critical consumers:** the prune's + protected set and the resample's replace-vs-add decision are each computed from it, per + their own settled rules; **no separate ad-hoc tracking mechanism remains in the + territory.** +- **No silent gaps:** a recapture created by item 15's bake is tracked from the instant it + exists — a bake followed immediately by a prune, or by a second bake, behaves correctly + with no window in which the recapture is untracked or its lineage absent. (Testable + ahead of item 15 by simulating a system-created file through the same path.) +- **Fail-safe throughout:** with tracking state made unreadable, the prune deletes nothing + (and reports what blocked it, per today's behavior) and the resample never takes the + replace branch; **no destructive act follows from ambiguity, anywhere in the territory.** +- **Every protection settled today holds undiminished after consolidation:** a capture + held by a live instance cannot be pruned; files the system did not create are + untouchable; unreadable usage state still halts the prune. +- **A pre-existing bank lifts into the consolidated system with no loss of protection and + no spurious lineage**, and never-recorded remains distinguishable from unreadable. +- **Item 15's other-references case is decidable:** for any capture, "does provenance-tied + usage exist" has a definite yes/no answer. +- The pure fold decisions stay pure and unit-tested without a DAW; the REAPER/filesystem + half stays in `shell/persist`. + +**Open questions.** None awaits a Daniel decision — the requirement and its strength are +his; the shape is review work. +- **The consolidated shape [propose].** What "one system" concretely is — one record + family, one authority, how the three facets relate — is design work proposed at review. +- **The lineage record [propose, jointly with item 15's naming-and-lineage question].** + What constitutes a "usage tie," when it is written, whether it is ever severed, and + whether iteration lineage is user-readable from the bank. +- **The recipe-fingerprint half of a recapture [propose].** A resample's recipe is the + instrument's own settings, not a track chain; whether the fingerprint records a + resample-shaped recipe — and what its conservatism means there — is proposed at review. + (The lineage half has no record-nothing option; the recipe half may keep one.) +- **Never-recorded vs. unreadable [propose].** Pre-existing captures predate lineage + records, and the two absences demand opposite treatment: never-recorded means no tied + usage exists (replace is legitimate); unreadable means fail-safe. How the consolidated + system distinguishes them — and how pre-existing banks lift in without weakening any + protection they enjoy today — is proposed at review. +- **NEW — does this absorb the deferred persisted-instance-identity fix? [propose].** + `docs/TODO.md` carries "Persist ReaSampler 9000 instance identity to let prune reclaim + de-referenced captures after reopen," deferred 2026-07-28 as low-risk (safe, but the + bank folder grows unbounded after a reopen). "Consolidated and made 100% robust" + arrived the next day. My reading is that they do not conflict — robustness is a *safety* + claim, the wart is a *completeness* one — but the consolidation is the natural moment to + revisit it, and the constraint it must handle is unchanged (a persisted identity is + inherited by a Ctrl+D in-place duplicate; a divergent clone must still be detected and + protected fail-safe without reintroducing the sibling-drop bug). **Fold it in or + explicitly restate the deferral at review; do not leave it ambiguous.** + +--- + +#### Ξ-W1-T2 — `note-program-model` + +**Goal.** Land the programmed-capture-signal model as a pure, tested module: musical +divisions, tempo resolution, and offset anchoring — the arithmetic both the bake and the +popup will read. + +**Consolidates item 15 (the capture-signal model; the bake is Ξ-W2-T1 and the popup UI is +Ξ-W3-T1).** + +**Surface boundary — owns:** a new pure module (`core/instrument/map/note_program` or +similar, name [propose]) plus its tests target and the CMake row. New files only; no +existing file edited except `CMakeLists.txt`. Disjoint from T1 by construction. + +**Behavior.** +- **Note length is a musical division, not a free duration.** Chosen from divisions + spanning **1/64th through 64/1, with dotted and triplet multipliers** (Daniel's + examples: `1/8.`, `1/4t`, `1/16`, `4/1`). +- **Beats resolve against the project tempo under the cursor.** A beat-denominated value — + the note-length division always, the offsets when expressed in beats — resolves to time + against "the project tempo under the item cursor" (Daniel's phrase; read plainly: the + tempo in effect at the project's cursor position when the preview or bake runs). The + module takes the tempo as a parameter; **reading it from REAPER is the shell's job**, + not this module's. +- **Offsets anchor to note-on and note-off.** The start offset is relative to the + programmed note's **note-on**; the end offset is relative to its **note-off**. +- **Offsets are expressed in ms AND in beats** — the two denominations are two views of + one stored value, and the conversion is this module's. Note length is + musical-division-only. +- **Velocity is explicit** — a plain 1–127 value carried in the same record. Material + because the velocity transfer curves modulate amp (and, post-Θ, pitch and filter) at + that velocity. +- **The record is one struct**, round-trippable, that the bake renders from and the popup + edits. One source of truth: **preview and bake cannot diverge** because they read the + same record through the same resolver. + +**Acceptance criteria.** +- The division set spans 1/64 through 64/1 inclusive, with dotted and triplet variants, + and each resolves to the correct duration at a given tempo — asserted at the extremes + and at Daniel's four named examples. +- The same division yields a correspondingly different duration when the supplied tempo + differs (tests assert the proportionality, not a hardcoded rate). +- Offsets round-trip losslessly between ms and beats at a given tempo. +- Anchoring is explicit in the type: a start offset is note-on-relative and an end offset + is note-off-relative, and the resolved window is computed from both plus the note length. +- **No hardcoded sample rate and no hardcoded tempo** anywhere in the module (the standing + rate-free-seconds ruling). +- Builds and tests without REAPER, VST3, or a DAW. + +**Open questions.** +- **Are negative offsets meaningful? [propose]** Unchanged from the source doc. A negative + start offset (capture beginning before note-on) is plausible; a negative end offset + (truncating before note-off) is also plausible. Propose the answer with the type. +- **The denomination seam [propose].** The first answer round expressed the offsets "in ms + AND in beats" while note length is musical-division-only. The plain reading is that the + ms/beats duality applies **to the offsets only**. If an ms display or entry for note + length seems wanted at implementation, propose it at review rather than assuming either + way. + +--- + +### Ξ-W2 — The bake chain + +**Depends on Ξ-W1 for:** the consolidated lineage records that make replace-vs-add +computable (T1) and the programmed-note record the offline pass renders (T2). **Also +depends on all of Phase Θ** — see the phase note above. + +**One track.** The bake is one gesture and one chain; the architecture decision at its +head governs every step after it. + +#### Ξ-W2-T1 — `resample-bake-chain` + +**Goal.** One click bakes the dialed sound into a bank capture, re-points the instance at +it, and hands the instrument back at neutral — the dial → bake → dial-again loop, run +without leaving the sampler. + +**Consolidates item 15 (the chain; the popup UI is Ξ-W3-T1).** + +**Surface boundary — owns:** whichever seam the architecture decision selects (see below), +plus `shell/capture/capture_orchestrator` (a resample entry point alongside +`captureAndIndexOne`), `core/model/bank_model` + `owned_manifest` at the add boundary, +`shell/actions` (a new action, if the decision needs one), and the instrument-side bake +trigger. **Does not own** the capture-signal popup's UI — that is Ξ-W3-T1. + +##### The architecture decision — this track's first deliverable, before any chain work + +Item 15 requires a click **inside the VST3 editor** to cause a **bank write**, and the +instrument is a read-only bank consumer by invariant. The one prior attempt at this +crossing (S13) closed **DEGRADED** and was deferred. Resolve this before building the +chain. Two orthogonal choices: + +**Decision 1 — how the click crosses to the extension.** +- **(1a) Ext-state request key + extension-side timer poller**, with a claim/clear nonce — + the shape S13 spiked and deferred. It needs a new instrument WRITE seam into ext-state, + though the `rsusage_`-prefixed guarded write already establishes the precedent that such + a seam can exist without weakening the read-only-*bank* invariant. Known cost: the + cross-process handshake race the S17 spec rejected. +- **(1b) Direct action invocation over the VST-host bridge — the lead candidate.** A + REAPER-hosted VST3 already resolves REAPER API functions **by name** over the host + callback (that is how the instrument reads `GetProjExtState`), and can fetch its own + host context — the track/take/project it was instantiated in. If `NamedCommandLookup` + and `Main_OnCommand` resolve the same way, the editor can invoke the extension's bake + action **synchronously**, with no request key, no poller, and no nonce — which dissolves + the S13 problem for this case rather than re-litigating it. **[verify]** against + `vendor/reaper-sdk/sdk/` (`reaper_plugin.h`, `video_processor.h`, + `reaper_plugin_functions.h`) that both resolve over the callback, and that calling them + from the editor's UI thread is safe. This is a verification task, not an assertion — do + not build on it until confirmed. +- **(1c) No crossing — a bindable extension-side action only.** Fallback. It abandons + Daniel's "from directly inside the ReaSampler 9000" framing, so it is the answer only if + (1a) and (1b) both fail. If it is taken, say so explicitly rather than quietly shipping + a panel button. + +**Decision 2 — what actually renders the audio.** +- **(2a) Headless voice-engine render, extension-side.** The extension reads the instance's + component state, reconstructs the engine from the shared pure core, renders the + programmed note, and banks the result through the normal capture add-path. Exact and + fully deterministic. **Trade-off:** the extension currently links `component_state_io` + but deliberately **not** `sampler_core`/`pitch_shift` — `component_state_io` was split out + precisely to avoid pulling the voice engine into the extension. This reverses that. +- **(2b) REAPER offline render of the hosting track**, with the programmed note. Reuses + `OfflineRenderBackend` and with it the whole precision-invariant apparatus — exact + bounds, bit-identical repeats, relative paths, tail control — for free. **Trade-offs:** + it needs a *temporary* arrange mutation to program the note (the realtime backend's + temp-track-created-and-removed-cleanly is the precedent), and the existing scope model + has no "this one FX only" scope, so post-instrument track FX would be baked in unless a + new bypass scope is added. The arrange must be byte-identical afterward. +- **(2c) The instrument renders, the extension banks it.** The instrument produces the + audio (it already owns the engine) into a temp file or shared buffer; the extension + moves it into the bank and indexes it. Splits the work along the existing ownership + line — but adds a handoff artifact and its cleanup. + +**Recommended sequence for the decision:** verify (1b) first — if it holds, the crossing +is nearly free. Then pick between (2a) and (2b) on the strength of the +"only-the-instrument, not the track chain" requirement: Daniel's ask says "send a trigger +or gate **through the sampler** offline," which favours (2a) or (2c) unless (2b) gains a +single-FX bypass scope. Record the verdict in this track's review, and if the verdict is +DEGRADED, escalate rather than improvise — the fallback is (1c) + (2a). + +**Behavior — the chain.** +- **One gesture, whole chain.** A single click performs: offline pass → capture → bank + update (replace or add-distinct) → instance re-point → parameter reset. **One action + from the user's side, not a wizard.** +- **Offline pass through the instrument's own processing.** The audio is produced by + sending a trigger or gate through the sampler **offline** — the instrument's own voice + path, with filtering, pitching, and amp exactly as dialed. The recapture is of that + processed output, not of the raw source. +- **The recapture is a bank capture like any other** — project-relative, indexed, + browsable from any surface that browses the bank, recorded in the owned-file manifest, + and governed by the same safety rules as every file the system creates. +- **The note is the capture's root.** The rendered note is the capture's root note — which + is exactly why **the root-note parameter is not reset by resampling**: capturing at root + is what makes root survivable, and resetting it would detune every subsequent iteration. +- **Gate's hold and tail come from the programmed window.** The programmed note length is + the Gate hold bound — the gate holds for the note length, then releases; a Gate loop + cycles within the held span and the render still terminates. The **end offset** is the + home of the tail policy: captured time past the note's end is where the release rings. +- **Replace, or add distinct.** When nothing else references the source capture, the + recapture **replaces** it as the bank entry; when other references exist, the original + entry stays and the recapture is **added as a new distinct capture**. "Other reference" + means **any usage of the source capture tied to it by the provenance/recapture system** + — Ξ-W1-T1's records. Read plainly: the reference universe is the resample system's own + lineage records, **not** the prune-protection universe. Bank multi-membership, items + placed in the arrange, and a plain hold by another instance outside any recapture + lineage do **not** force add-distinct, and need not for safety — the superseded file + survives until prune, and prune's protection universe is unchanged and broader. +- **Replacement never destroys audio bytes.** "Replace" means the bank entry now denotes + the recapture; the superseded file is not deleted. Prune remains the system's only + file-deletion authority — resample writes a new file and retires the old one to + reclaimable-by-prune status; it never overwrites or deletes it. Until a prune reclaims + it, the pre-bake audio survives on disk — **the iterate loop's built-in recovery floor.** +- **The instance re-points** at the new/corrected bank capture and plays the baked sound. +- **Parameters reinitialize to default** — destructive to the dialed settings **by + design**: the processing now lives in the recaptured audio, and neutral controls are the + starting point for the next iteration. +- **Reset scope: only what the bake baked in.** Ratified by Daniel, not derived: + - **Reset** (their effect is in the audio): the envelope contours — staged and spline + alike — the filter parameters, master gain, the pitch envelope/engine settings, the + velocity transfer curves (their effect at the programmed velocity is in the audio), and + the loop points (they shaped the render, and old positions are meaningless against new + audio). + - **Survive** (mapping facts, not present in the audio): the **root note**, whatever + remains of key mapping (key-tracking; any key-range concept Θ-W1-T1 settled), and the + VOICE group (polyphony behavior leaves no trace in a single rendered note). +- **No timeline item, ever.** Resampling is a capture act: it writes a file to the bank and + updates the index; nothing is placed in the arrange view. Any framing that auto-inserts + the recapture is invalid — and if architecture (2b) is chosen, its temporary programming + item must leave the arrange byte-identical. +- **Undo/recovery: a plus, not a requirement.** Welcome if it falls out cheaply; the + feature ships without it. The guaranteed recovery path is the superseded file surviving + on disk until a prune reclaims it. + +**Acceptance criteria.** +- On a dialed-in instrument, one click yields all of: a recapture in the bank, the instance + holding that recapture, and the baked-in audio parameters at their defaults — with the + root note and the other surviving mapping parameters untouched. +- **The bake is audible and faithful:** after the click, playing the programmed capture + note (root, at the programmed length, offsets, and velocity, in the active mode) through + the now-neutral controls sounds as the dialed instrument sounded just before the click — + the processing has moved from the controls into the audio. +- **A Gate-mode bake terminates on its own:** the gate holds for the programmed note + length, then releases — even with loop-sustain active, the render ends. No indefinite + capture. +- **After the bake the root note is unchanged** — iteration never detunes; the next bake + plays the same root. +- **Sole-reference case:** the bank afterwards shows the recapture where the source + capture's entry was; no other bank entry is disturbed. **Other-references case** + (provenance-tied usage of the original exists, computed by Ξ-W1-T1): the original entry + is untouched, a distinct new entry appears, and **every other holder of the original + sounds exactly as before.** +- **The click deletes no file:** the superseded audio file still exists on disk afterwards, + and only a later prune — under the settled orphan rules, only when nothing references it + — can reclaim it. +- **The arrange timeline is untouched:** no item appears anywhere, on any track, and if a + temporary programming item was used it is gone and the arrange is byte-identical. +- **Iteration composes:** dial → click → dial → click bakes the second pass onto the + first's result, repeatable indefinitely. +- **Save/reload:** an instance holding a recapture reloads and plays it exactly like any + other loaded capture. +- The bake adds nothing to `process()` — the offline pass runs off the audio thread. + +**Open questions.** +- **The architecture decision [propose, with a verification step].** See above. The (1b) + bridge verification is **[verify]**; the choice among the candidates is **[propose]**. +- **Extension presence [propose].** The instrument plays self-contained with the extension + absent, but the bank is the extension's surface and resampling mutates the bank. The + natural answer is that resample **requires the extension present and is cleanly + unavailable — not silently lossy — without it**; propose the exact behavior at review. +- **Reset-scope edge cases [verify].** The rule is settled and the per-parameter + classification is ratified. Only a genuinely new parameter — one arriving with a Θ track + and absent from the ratified lists — is classified **against the rule** and surfaced at + review. Not a new Daniel call. (Θ adds: curve exponents → reset; spline contours → + reset, already named; the filter's velocity/key-tracking mod → reset with the filter; + loop crossfade → reset with the loop points; the Staged/Spline mode flag → classify.) +- **Naming and lineage [propose, jointly with Ξ-W1-T1's lineage-record question].** When + add-distinct fires, the new capture needs a display name (derived from the original?), + and the bank some way to read iteration lineage across repeated bakes. One proposal, + both tracks. +- **Provenance of the recapture — homed in item 17**, i.e. Ξ-W1-T1. A resample's recipe is + the instrument's own settings, not a track's chain. Nothing to decide here beyond + consuming that answer. + +--- + +### Ξ-W3 — The capture-signal popup + +**Depends on Ξ-W2 for:** the bake chain that the popup programs and its preview must not +diverge from. The acceptance criterion "preview and bake cannot diverge" requires one +render path, which W2 owns — building the preview against a second path is exactly the +defect the criterion exists to catch. + +**One track.** + +#### Ξ-W3-T1 — `capture-signal-popup` + +**Goal.** Let the user program the capture signal — note length, offsets, velocity — and +hear it before committing the bake. + +**Consolidates item 15 (the popup sub-feature).** + +**Surface boundary — owns:** `core/instrument/ui/curve_popup`'s sibling — a new pure popup +geometry module for the capture-signal sheet (mirror the `curve_popup` precedent: centered +sheet, width/height clamps, title row, Close button rect, outside-sheet dismissal test) — +plus the editor's popup paint/input wiring in the deck or chrome band, and the preview +trigger's call into W2's render path. Reads Ξ-W1-T2's note-program record; does not +re-own it. + +**Behavior.** +- **A popup menu programs the capture signal:** **note length**, **start and end offsets — + in ms AND in beats** — and **velocity**. +- **A preview trigger button** auditions the capture note exactly as currently programmed + — the user hears the bake before committing it — and the offline pass renders that same + programmed performance. +- **Note length is a musical-division picker** spanning 1/64th to 64/1 with dotted and + triplet multipliers. +- **Offsets are anchored** — start to note-on, end to note-off — and each is readable and + editable in **both** ms and beats, the two views of one stored value. +- **Beat-denominated values resolve against the project tempo under the cursor.** The + shell reads the tempo; the arithmetic is Ξ-W1-T2's. +- **The programmed velocity is the render velocity** — material because the velocity + transfer curves modulate amp, pitch, and filter at that velocity. +- House-consistent: drawn through the shared kit by palette role, geometry pure and + CTest-covered, no decorative animation. + +**Acceptance criteria.** +- The popup exposes: note length as a musical-division picker spanning **1/64th to 64/1 + with dotted and triplet multipliers**; start and end offsets, **each readable and + editable in both ms and beats**, anchored to note-on and note-off respectively; and + velocity. +- **Its preview trigger auditions the capture note exactly as programmed, and the bake + renders that same programmed performance — preview and bake cannot diverge.** Verify + structurally (one render path), not just by ear. +- **Beat-denominated values resolve against the project tempo under the cursor:** the same + programmed division yields a correspondingly different rendered duration when the tempo + at the cursor differs. +- The programmed signal persists with the instance and round-trips save/reload. +- The popup's geometry and dismissal test are pure and unit-tested; no hit-test math lands + in shell code. + +**Open questions.** Both of Ξ-W1-T2's residuals (negative offsets; the note-length +denomination seam) surface here as UI consequences — if T2 answered them, this track +implements the answer; if T2 deferred either, this track is where it becomes visible and +must be closed. + +--- + +### Running Θ and Ξ concurrently + +**Ξ-W1 is concurrency-safe with Phase Θ from Θ-W2 onward.** Ξ-W1-T1 is extension-side +record-keeping (`core/model`, `core/wire`, `core/reclaim`, `shell/persist`, +`shell/actions`); Ξ-W1-T2 is a new pure module. Neither is on Θ's critical path. + +**Two conditions.** +1. **Not during Θ-W1.** Θ-W1-T1 re-seams the whole instrument, including + `processor_reload.cpp`, which is the one file Ξ-W1-T1 also touches (the usage-publish + block). Wait for Θ-W1 to land. +2. **Ξ-W1-T1 owns the usage-publish block** for the duration; no Θ track edits it. + +**Recommendation:** run Ξ-W1 concurrently if a specialist is spare — it ships prune +robustness early and removes the largest dependency from Ξ-W2's critical path. Otherwise +sequence it after Θ. This is a scheduling call, not a plan decision. + +--- + +## Traceability — all seventeen items + +The check that nothing was dropped. Every row points at a track that exists above. + +| # | Item (short) | Phase-Wave-Track | Worktree slug | +|---|---|---|---| +| 1 | Envelope editor: radio switch, curve dials, overlay recolor | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` | +| 2 | MM preamp Filter — resonant HP/LP stage | Θ-W1-T3 (DSP) **+** Θ-W2-T1 (integration) | `pth-w1-t3-filter-dsp-port`, `pth-w2-t1-filter-voice-path` | +| 3 | Alternative Spline EGs | Θ-W5-T1 | `pth-w5-t1-spline-egs` | +| 4 | Bug: end-of-sample click, Trigger × Preserve | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` | +| 5 | Bug: drag-out sometimes lands without audio | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` | +| 6 | Bug: FX-container drop loses the capture | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` | +| 7 | Stereo waveform shows both channels | Θ-W2-T2 | `pth-w2-t2-stereo-waveform-lanes` | +| 8 | Release anchoring; the Pitch AD becomes AHD | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` | +| 9 | Loop points — regression + Gate loop-sustain | Θ-W4-T1 | `pth-w4-t1-gate-loop-sustain` | +| 10 | Knob/label sizing, ms units, double-click reset | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` | +| 11 | Preview glyph; VELOCITY deck; bipolar curves | Θ-W4-T2 | `pth-w4-t2-velocity-deck-and-bipolar-curves` | +| 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` | +| 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` | +| 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` | +| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` | +| 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` | +| 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` | + +### Deliberate compressions + +Recorded so a reader of `TODO-1.0.md` can see what this plan did to the source, rather +than discovering it later: + +- **Item 2 is split across two waves.** The DSP port (Θ-W1-T3) is deliberately separated + from the integration (Θ-W2-T1) so the external-input dependency on Daniel's Cortex-M4 + code sits on a standalone, disjoint track instead of blocking a wave. Item 2's + acceptance criteria are split accordingly — the range/resonance assertions land in + W1-T3's tests, the audible/pipeline/deck criteria in W2-T1. +- **Item 2's filter envelope ships twice.** W2-T1 ships it in the existing staged AHDSR + shape; W3-T1 gives it the curve treatment and the mode-driven Gate→AHDSR / + Trigger→AHD shape. This is deliberate: waiting would put the filter behind the whole + envelope system. +- **Item 4 lands in Θ-W3, not Θ-W1.** Its fix region is the region item 14 retires, and + the only earlier instrument-side track owns the entire engine. Its acceptance gate is + stated as the post-consolidation gate. It is not gated behind the *whole* editor chain + — three waves of six — and the source doc itself requires re-verification under the + surviving mechanism either way. +- **Item 11's filter velocity curve is split from item 2's filter velocity + modulation.** W2-T1 wires the modulation path (following the amp/pitch precedents); + W4-T2 sets the curve's bipolar domain and default and homes its button. Neither track + can do the other's half. +- **Item 15's undo/recovery is carried as a note, not a criterion** — Daniel set it at + exactly "a plus." The guaranteed recovery floor (the superseded file surviving until + prune) is a criterion. +- **Nothing else was compressed.** Every other item's behavior bullets and acceptance + criteria are carried at full strength into the track that owns it. + +--- + +## Outline at a glance + +``` +Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor + W1 Collapse and re-seam + T1 zone-retirement ......................... 16 + T2 capture-handoff-bugs .................... 5, 6 + T3 filter-dsp-port ......................... 2 (DSP) [needs Daniel's M4 code] + W2 Filter in the voice path; waveform and chrome bands + T1 filter-voice-path ....................... 2 (integration) + T2 stereo-waveform-lanes ................... 7 + T3 toolbar-and-piano-strip ................. 12 + W3 The staged envelope system + T1 staged-envelope-curves .................. 1, 8, 14, 4 + W4 Loop sustain and the velocity deck + T1 gate-loop-sustain ....................... 9 + T2 velocity-deck-and-bipolar-curves ........ 11 + W5 Spline EGs + T1 spline-egs .............................. 3 + W6 Editor legibility pass + T1 legibility-and-antialiasing ............. 10, 13 + +Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward) + W1 Consolidated tracking, and the programmed-note model + T1 tracking-consolidation .................. 17 + T2 note-program-model ...................... 15 (model) + W2 The bake chain [requires all of Phase Θ] + T1 resample-bake-chain ..................... 15 (chain) + W3 The capture-signal popup + T1 capture-signal-popup .................... 15 (popup) +``` diff --git a/docs/TODO-1.0.md b/docs/TODO-1.0.md index 55d0f3a..303fed4 100644 --- a/docs/TODO-1.0.md +++ b/docs/TODO-1.0.md @@ -1,5 +1,14 @@ # TODO-1.0 +> **`docs/PLAN.md` is now the roadmap.** All seventeen items below have been +> consolidated into areas and sequenced into the Phase → Wave → Track hierarchy in +> `docs/PLAN.md`; that file is what implementation specialists are dispatched against, +> and each of its tracks is self-sufficient for a brief. **This file is retained as the +> verbatim-provenance spec appendix** — Daniel's raw asks and every answer round, +> unedited, are the source of truth behind PLAN.md's compressed behavior bullets. Its +> traceability table maps each item number below onto the track that owns it. Nothing in +> this file changes as work lands; PLAN.md points move to `docs/COMPLETED.md`. + Post-1.0 queue for ReaSampler — chiefly the 9000 instrument, plus two extension-side bugs. Items 1–3 are the first batch, in Daniel's ordering (2026-07-28); items 4–13 are a second batch (2026-07-28, later the same day);