From 1f24c4b09553d00022689956be046893dbb4d91c Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 29 Jul 2026 15:09:48 -0400 Subject: [PATCH] docs: 1.0 documentation restructure Split root CLAUDE.md into 19 per-directory files scoped to their source area. Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md, PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals into docs/TODO.md. --- CLAUDE.md | 195 +- COMPLETED.md | 3258 ----------------- CONTEXT-ARCHIVE.md | 1230 ------- CONTEXT.md | 2779 -------------- PLAN.md | 619 ---- TODO.md | 17 - docs/ARCHIVE.md | 1065 ++++++ docs/COMPLETED.md | 8 + TODO-1.0.md => docs/TODO-1.0.md | 0 docs/TODO.md | 133 + .../audit-notes/q-w0-t2-architecture.md | 3 +- .../audit-notes/q-w0-t3-env-constants.md | 10 +- docs/product/capture-tail.md | 31 +- docs/product/code-organization.md | 16 +- docs/product/code-quality-audit.md | 3 +- docs/product/design-view.md | 8 +- docs/product/midi-playback.md | 118 +- docs/product/multi-bank.md | 5 +- docs/product/provenance.md | 8 +- docs/product/removal-and-prune.md | 31 +- docs/product/versioning-and-release.md | 6 +- docs/product/visual-design-language.md | 15 +- src/app/CLAUDE.md | 35 + src/core/audio/CLAUDE.md | 13 + src/core/capture/CLAUDE.md | 69 + src/core/instrument/CLAUDE.md | 223 ++ src/core/json/CLAUDE.md | 20 + src/core/model/CLAUDE.md | 81 + src/core/reclaim/CLAUDE.md | 41 + src/core/ui/CLAUDE.md | 92 + src/core/util/CLAUDE.md | 19 + src/core/version/CLAUDE.md | 20 + src/core/view/CLAUDE.md | 104 + src/core/wire/CLAUDE.md | 94 + src/shell/actions/CLAUDE.md | 53 + src/shell/bank_ops/CLAUDE.md | 42 + src/shell/capture/CLAUDE.md | 55 + src/shell/instrument/CLAUDE.md | 105 + src/shell/panel/CLAUDE.md | 70 + src/shell/persist/CLAUDE.md | 60 + src/shell/view/CLAUDE.md | 85 + 41 files changed, 2731 insertions(+), 8108 deletions(-) delete mode 100644 COMPLETED.md delete mode 100644 CONTEXT-ARCHIVE.md delete mode 100644 CONTEXT.md delete mode 100644 PLAN.md delete mode 100644 TODO.md create mode 100644 docs/ARCHIVE.md create mode 100644 docs/COMPLETED.md rename TODO-1.0.md => docs/TODO-1.0.md (100%) create mode 100644 docs/TODO.md create mode 100644 src/app/CLAUDE.md create mode 100644 src/core/audio/CLAUDE.md create mode 100644 src/core/capture/CLAUDE.md create mode 100644 src/core/instrument/CLAUDE.md create mode 100644 src/core/json/CLAUDE.md create mode 100644 src/core/model/CLAUDE.md create mode 100644 src/core/reclaim/CLAUDE.md create mode 100644 src/core/ui/CLAUDE.md create mode 100644 src/core/util/CLAUDE.md create mode 100644 src/core/version/CLAUDE.md create mode 100644 src/core/view/CLAUDE.md create mode 100644 src/core/wire/CLAUDE.md create mode 100644 src/shell/actions/CLAUDE.md create mode 100644 src/shell/bank_ops/CLAUDE.md create mode 100644 src/shell/capture/CLAUDE.md create mode 100644 src/shell/instrument/CLAUDE.md create mode 100644 src/shell/panel/CLAUDE.md create mode 100644 src/shell/persist/CLAUDE.md create mode 100644 src/shell/view/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 4b1cc1b..85c890e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repo identity and current state -**ReaSampler** is a per-project audio sample-bank capture tool that builds two artifacts: the REAPER extension (`reaper_reasampler`) and **ReaSampler 9000**, a Windows-only VST3 sampler instrument (`reasampler_9000.vst3`, `core/instrument/` + `shell/instrument/`, second CMake target `reasampler_vst`, gated on the vendored `vendor/vst3sdk` submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout. CONTEXT.md is the authoritative spec — settled decisions, invariants, guardrails, and not-yet-built specs; it is large, so locate the relevant phase section by grepping its headings and read only that section with an offset rather than reading it whole. Build detail for landed phases lives in CONTEXT-ARCHIVE.md. Every REAPER API name cited there is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. A post-S-VIEW DAW-fix pass has landed (all 52 suite tests green): envelope nodes fully editable in both modes (every Gate stage A/H/D/S/R + Trigger zero-fade-out node, param-domain schematic scaling, 8 px min node separation, all nodes clamped in-canvas); gap-free per-column waveform render (`columnMinMax` homed in `peaks`, `waveformColumnCount` in `component_geometry`, shared via `drawWaveform`); `param_slider` `Knob` primitive (7→5 o'clock arc, needle, vertical-drag); zone-bleed fix 3a (`reconcileSingleCaptureZones` in `sample_map`). The voice-system redesign is also landed: `sampler_core` gains user-parameterized voice count (1–32, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` retrigger/legato toggle), an isolated `PreviewCard` (dedicated preview voice outside the MIDI pool — never steals from/into it; unity-Preserve zero-latency bypass scoped to it), and two-tier panic (CC 123 = release, CC 120 = immediate hard-stop incl. Trigger one-shots); processor sums the preview card alongside the engine + drain, `retireIdleDrain()` retires fully-idle drain snapshots, and voice-param edits rebuild from the already-decoded PCM (no bank re-read/WAV re-decode) via the drain-slot swap; `ComponentState` envelope bumped v6→v7 (voiceCount/voiceMode/monoTrigger bytes; pre-v7 blobs lift to 16/Poly/Retrigger). **FB1 Sample-view recomposition (r11) has also landed** (suite 55/55 green): all linear sliders replaced by radial **knobs** in a fenced **knob deck** (groups: AMP ENVELOPE / PITCH / PITCH ENV / VOICE / MASTER); mode toggles are compact in the caption row, not full-width; the **hero waveform runs full-width** (elastic band, 840×620 default preserved); the inline velocity-curve box is replaced by a **28×28 curve preview button → centered popup** with right-click node delete; voice-band controls (count / Poly-Mono / Retrig-Legato) are placed in the VOICE deck group; a **post-mixer per-sample-ramped master gain** (−∞…+24 dB, no zipper) is placed in the MASTER deck group, persisted as `masterGainLinear` — `ComponentState` envelope bumped v7→v8 (pre-v8 blobs lift to unity gain). Three new pure `src/vst/` modules landed: `knob_deck` (group-box + caption-row + knob-cell geometry, deterministic wrap, hit-test), `curve_popup` (sheet/close/box geometry + outside-sheet dismissal test), `master_gain` (dB↔linear taper math, −∞…+24 dB). **FB2 Zone-panel parity (r11, 2026-07-28) has also landed** (suite 55/55 green): the Zone param panel now uses the same knob deck + curve-preview-button/popup grammar as the Sample face — one control grammar across both surfaces of the one per-zone storage site; Zone-authoring affordances (+Add Zone / Delete, the piano-key strip, Low/High/Root legend) are preserved; VOICE and MASTER groups remain Sample-only (per-instance). `param_slider`'s linear slider rows are retired on the Zone panel (the FA4 `Knob` primitive is now the only live consumer of that half of `param_slider`). **This completes the r11 editor recomposition (Wave B / Phase S editor redesign).** A **GA post-launch DAW-fix pass** has also landed (suite 55/55 green): `pitch_shift` rewritten from dual-tap OLA (anti-phase cancellation → spectral garbage on repitched notes) to **correlation-aligned SOLA splices** with a ratio-scaled raised-cosine fade (clean pitch shift past +24 st); `Voice::start` applies a **bounded blend** (`out*(1-w) + ref*w`, w decaying from 1.0) at takeover boundaries — mono retrig/fallback, poly at-cap steal, and preview re-trigger — superseding the earlier `(1-amp)` envelope-complement gate that zeroed the compensation on Trigger/zero-attack restarts; the output bus is now **permanently stereo** (`ChannelMode` is decode-only; the dynamic mono↔stereo bus renegotiation is deleted) with channel mode **auto-defaulting from the loaded capture** via new `ComponentState` **v9** (`channelModeExplicit` flag) + a pure `channelModeFor` helper; `SetCapture` moved to drag-arm in `bank_panel` so a first straight-out drag arms correctly; and the Design/Arrange mode-toggle action now calls `bankPanelInvalidate()` so the panel footer reflects the new mode without requiring a button click. **Preview via real MIDI note path (pS, 2026-07-28):** the dedicated `PreviewCard` is RETIRED; preview now injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` (same path host MIDI uses), so it obeys polyphony/mono/voice-stealing/envelopes; the processor no longer sums a separate preview voice; the unity-Varispeed-bypass demotion (GA2 primed shifter speaks on frame 0 anyway) is removed. **Self-contained playback (pS, 2026-07-28):** `ComponentState` bumped v9→**v10** with a `SampleRefs` table — per referenced sample, the instance owns a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent); the bank/bridge is now a browser source (loading a capture copies its reference in); the reopen-heal timer + poll-to-play apparatus are removed; pre-v10 blobs lift to empty refs and re-save self-contained. **Usage-detection (pS-usage, 2026-07-28):** new pure module `sample_usage` + REAPER-facing shell `usage_scan` implement the instance-usage wire (`rsusage_` per-instance ext-state keys); each live ReaSampler 9000 instance publishes the captures it holds (from its `SampleRefs`) at `reloadInstrument` time; the extension reads every usage record at prune-scan time, liveness-folds against the live FX enumeration, and unions the surviving held paths into the prune's referenced set — so a capture held by any live instance can never be an orphan and `BANK_PRUNE_FOLDER` can never delete it; `prune_reconcile` gains `mergeReferenced`; `persist`'s `PruneReport` gains `abortedUnreadableUsage` + `offendingUsageKeys`; dry-run / orphan-set / reclaim each independently abort (delete nothing) on unreadable usage; `actions` halts and prints offending keys; `reaper_bridge` gains `writeUsageExtState` (prefix-guarded — refuses non-`rsusage_` keys); `ComponentState` bumped v10→**v11** (`instanceGuid` field; pre-v11 blobs mint guid on first publish); new `sample_usage_tests` build target (pure, no REAPER/DAW). **Phase Q (2026-07-29, landed on `phase-q`)** reorganized all of `src/` into `core/` (pure, subsystem-namespaced: `model`/`view`/`capture`/`audio`/`ui`/`reclaim`/`version`/`json`/`util`/`wire`/`instrument/{engine,map,ui}`), `shell/` (REAPER/host-facing: `capture`/`panel`/`view`/`persist`/`actions`/`instrument`/`bank_ops`), and `app/` (`main.cpp`), splitting several god-modules along the way (`bank_panel` into eight `shell/panel/` TUs, the VST3 processor/editor god-TUs, `capture.cpp`, and `persist.cpp`) under a soft ~600-line-per-TU ceiling; `src/vst/` no longer exists. +**ReaSampler** is a per-project audio sample-bank capture tool that builds two artifacts: the REAPER extension (`reaper_reasampler`) and **ReaSampler 9000**, a Windows-only VST3 sampler instrument (`reasampler_9000.vst3`, `core/instrument/` + `shell/instrument/`, second CMake target `reasampler_vst`, gated on the vendored `vendor/vst3sdk` submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout: `core/` never includes REAPER or VST3 SDK types, `shell/` is where those hosts are actually touched, `app/` is the extension entry point. Every REAPER API name cited in project docs is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. + +Per-module detail — what each file owns, its invariants — lives in the nineteen per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below. + +## Settled decisions + +- **Capture modes:** offline render (deterministic, the default) AND realtime + record (for hardware / performed FX). The two backends deliberately share no + interface — their lifecycles genuinely differ (offline is synchronous, realtime + is async begin/tick/abort) — but both produce identical bank entries. +- **Bank scope:** per-project, travels with the `.rpp`. Files live in a + project-relative subfolder; the index persists in project ext state. No absolute + paths anywhere in the index. +- **Material:** must handle full-mix/stem bounces, chops/one-shots, and + single-cycle/wavetable grabs equally. That means exact sample-accurate bounds, + explicit tail control, channel-count preservation, and loop/zero-crossing + handling all matter from day one. ## One-time submodule setup @@ -53,96 +69,82 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde ## Architecture: the load-bearing split -**Pure core (no REAPER types, unit-testable outside the DAW):** -- `json` (`core/json`) — the ONE hand-rolled JSON lexical layer (Q-W1): string/number/bool/null tokens, the scoped object `Writer`, and the bounds-checked `Reader` cursor, byte-compatible with the five pre-extraction per-module writers it replaced (`bank_model` / `bank_book` / `view_mode_model` / `owned_manifest` / `tail_control`). Domain grammars stay in the consumers; this owns lexing/emitting only. -- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory. -- `file_bytes` (`core/util`) — the ONE whole-file byte loader (Q-W1), linked by both artifacts; blocking I/O, off-audio-thread only. -- `clamp01` (`core/util`, header-only) — the ONE unit-interval clamp (Q-W1), replacing four per-module static copies; NaN passes through unchanged rather than collapsing to a bound. -- `rect` (`core/ui`, header-only) — the ONE concrete pixel rectangle (Q-W1): XYWH storage + `right()`/`bottom()`/`ltrb()`/`contains()`, replacing 12+ byte-identical role structs (`ButtonRect`/`FooterRect`/`CellRect`/`KitBox`/…) and the VST side's separate LTRB `Rect`; every prior role name survives as a `using` alias at its old site (e.g. `editor_geometry::Rect`). -- `bank_model` — `Sample` metadata struct + `BankIndex` (add/remove/query/tier/dedup-by-hash + JSON round-trip). Test it hard — it is the heart. -- `peaks` — waveform min/max bin computation from raw PCM; does not depend on REAPER's peak API. -- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, JSON round-trip. -- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell. -- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel. -- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip. -- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch. -- `slot_map` (`core/model`) — the gap-preserving display-position carrier for ONE bank (sample id → slot, ≥0), extracted from `bank_book` (Q-W1): append/remove/reorder (insert-before-and-shift)/`reconcile` against live membership, `resetDense` migration seed, JSON round-trip. Wrapped (not merged) by `bank_book`. -- `bank_book` — multi-bank registry: an ordered set of banks each wrapping a `BankIndex`. **Pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model.** Owns create/rename/reorder/delete of named banks, active-bank id, and index-only move/copy/remove of a sample between banks. The JSON round-trip lives in the sibling `bank_book_json` TU (Q-W5 split; serialize/deserialize via a private static `nameKey` seam) — one model, one codec, same public surface. -- `owned_manifest` — the set of project-relative files the capture path itself created, persisted under the `"owned_files"` ext-state key, so the prune path can distinguish the bank system's own orphans from hand-dropped files. -- `app_version` — REAPER-free version/channel identity: CMake-sourced semver constant, ext-state stamp value, and the full set of channel-derived identity accessors. All channel strings derive from one `REASAMPLER_CHANNEL_IS_BETA` bit; no scattered `#ifdef`s in the shells. -- `wav_codec` — chunk walker + layout parse + float32 build + size-field patch + content hashes; the single pure RIFF/WAV owner (`wav_trim` is retired; `wav_codec` is the sole owner). -- `capture_realtime` (`core/capture`, **renamed from `realtime_record` in Q-W3** — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matching `drag_out`/`drag_out_win`) — the M8 realtime-record pure logic: capture scope + FX-tap point → `I_RECMODE`/`I_RECMODE_FLAGS` values, wet/dry → tap point, the recorded-file → `Sample` mapping, and the async record-phase state machine. Depends on `bank_model` for the plain `Sample`/`SourceMode` types. The transport/temp-track/send recipe lives in the shell (`shell/capture/capture_realtime_shell.cpp` + `capture_realtime_finalize.cpp`). -- `provenance` — capture-recipe fingerprint: build/encode/compare a `rsprov1` fingerprint of scope, range, tail, rate/channels, track GUIDs, and FX-chain identity. **A thin reproducibility fingerprint — NOT a serialized chain to restore.** -- `prune_reconcile` — pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) − referenced`; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists. Gains `mergeReferenced(bankRefs, liveInstanceHeldPaths)` (pS-usage) — unions live instance holds into the prune referenced-set so the pure orphan computation includes them. -- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button. -- `batch_capture` — pure batch-capture planner: maps source ranges to capture units and aggregates results. -- `action_buttons` — pure action-button strip layout/hit-test. -- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag. -- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + three-accent pastel system, WCAG contrast-floor helpers. -- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers. -- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching). -- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement. -- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button. -- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons. -- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect. -- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace. -- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped. -- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns the `infoNamesFxHotspot` prefix classifier for `GetThingFromPoint` tokens. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure. -- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge. -- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW. +`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the nineteen directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth. -**REAPER-facing shells:** -- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`. -- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain). -- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + owned-manifest record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places). -- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action. -- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload. -- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26). -- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`. -- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.** -- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Maintenance → Placement via `action_bar`, short labels, More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU. -- `bank_ops` (`shell/bank_ops`) — the promptless bank-mutation verb seam (Q-W6 lift out of `panel_bank_ops`): `bankOpCreate`/`Rename`/`Delete`/`Evacuate`/`Activate`/`Transfer`/`Remove` + `persistBankOp` (the undo-batched ext-state persist), each taking a `ReaSamplerSession&` and returning whether the model accepted the mutation — no prompts, no message boxes, no panel-state reads. `shell/panel/panel_bank_ops` (menu/prompt UX) and `shell/actions/bank_actions` (bindable-action UX) both consume these as thin skins, so the mutation logic has exactly one home. -- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, `OwnedManifest` JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `prune_fs` hosts the prune dry-run / full-set orphan queries (supplying `referencedPaths()` + `owned().paths()` to the `prune_reconcile` pure core) and is **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. **pS-usage:** the prune scan unions instance usage via `usage_scan`; `PruneReport` carries `abortedUnreadableUsage` + `offendingUsageKeys`; dry-run / orphan-set / reclaim each independently abort (delete nothing) when usage state is unreadable. -- `usage_scan` — extension-side prune-scan shell (pS-usage): at prune-scan time, enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and folds with `sample_usage::foldUsageRecords` / `usageHeldPaths` to produce the set of held paths — or `abortPrune` when any record is unreadable (fail-safe: an unreadable record may protect anything, so the prune halts). Feeds `prune_reconcile::mergeReferenced`. Read-only: writes no ext-state. -- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. **Never touches master or `B_MUTE`/`I_SOLO`.** -- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys. -- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively. -- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. -- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** -- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.** -- `draw_kit` — shared LICE draw shell: `fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`, full interaction-state model, double-buffer preserved. Consumes `theme` + `component_geometry`. -- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). **pS-usage:** `BANK_PRUNE_FOLDER` halts on `abortedUnreadableUsage` and prints the offending `rsusage_*` key names with clear instructions. +| Directory | Scope | +|---|---| +| `src/app/` | REAPER extension entry point | +| `src/core/audio/` | pure audio-data math | +| `src/core/capture/` | pure logic behind the capture pillar | +| `src/core/instrument/` | pure VST3-instrument core (engine / map / ui) | +| `src/core/json/` | the hand-rolled JSON lexical layer | +| `src/core/model/` | the pure bank/sample index and its multi-bank container | +| `src/core/reclaim/` | pure prune orphan computation | +| `src/core/ui/` | pure UI geometry, palette, and interaction-decision modules | +| `src/core/util/` | small shared pure utilities | +| `src/core/version/` | version/channel identity | +| `src/core/view/` | pure Design View mode model | +| `src/core/wire/` | pure ext-state and wire-format codecs, cross-artifact contracts | +| `src/shell/actions/` | bindable REAPER actions, drag/drop shells, ingest | +| `src/shell/bank_ops/` | promptless bank-mutation verbs | +| `src/shell/capture/` | REAPER-facing capture backends and action bodies | +| `src/shell/instrument/` | ReaSampler 9000 VST3 shells | +| `src/shell/panel/` | the docked bank-panel shell + the shared LICE draw kit | +| `src/shell/persist/` | project ext-state persistence, prune filesystem I/O, usage scan | +| `src/shell/view/` | Design View mode application shell | -**VST3 instrument (`core/instrument/{engine,map,ui}`, plus shared substrate in `core/{audio,ui,wire}`) — pure core:** -- `sampler_core` — polyphonic voice engine with bounded stealing, user-parameterized voice count (1–32, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` Retrigger/Legato toggle), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots); per-zone `ZonePlayParams` (Gate/Trigger, AHDSR, pitch engine Varispeed/Preserve, AD pitch mod envelope), repitch/interpolation with loop-point-aware sustain. Preview injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` — no dedicated `PreviewCard`; preview obeys polyphony/mono/voice-stealing/envelopes. -- `sample_map` — zone payload: zones keyed by note range. **Wall-clock times stored as rate-free SECONDS, resolved against the live project rate — NO hardcoded sample rates in `src/`** (Daniel's standing ruling, load-bearing). JSON round-trip. -- `component_state_io` (`core/instrument/map`) — the `ComponentState` envelope + zones-payload binary codec (envelope v1…v11, zones-payload v1…v7), split out of `sample_map` (Q-W2v, T4-13 ≡ T2-07) so BOTH artifacts can link the codec without the extension pulling in the whole voice engine (`sampler_core`/`pitch_shift`) to serialize one preset blob — the extension's `instrument_drop` and the instrument's processor read/write the identical bytes, so the cross-artifact contract cannot drift. `zone_params.h` (`core/instrument/engine`) is the sibling header split out of `sampler_core.h` (T4-14/T4-17): the per-zone play-parameter value structs (`ZonePlayParams`/`AdsrParams`/`TriggerParams`/`PitchEnvParams`) and the per-instance mode enums (`ChannelMode`/`VoiceMode`/`MonoTrigger`) the engine, the codec, and the editor all share. -- `pitch_shift` — hand-rolled **correlation-aligned SOLA** (splice-overlap-add) pitch shifter for the Preserve playback mode: one active read tap chases the write head at the shift ratio; each splice jump is refined by a cross-correlation search so the new read point is waveform-aligned, then old and new taps are crossfaded (raised-cosine, amplitude-complementary). Replaces the prior dual-tap OLA whose fixed half-window tap offset caused anti-phase cancellation on many source frequencies. **GA2:** ring buffer **primed with the actual upcoming source** at note-on (was zero-filled) → gap-free frame-0 onset, ~25 ms Preserve onset latency eliminated (Preserve now speaks on frame 0, matching Varispeed), and real-content-bounded tail (last-window tail-truncation gone). No third-party dependencies; RT-discipline: no allocation in `process()`. -- `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects. -- `bridge_marshal` — pure marshalling helper for the REAPER VST-host bridge read: interprets the `GetProjExtState` int return against its filled buffer. -- `editor_geometry` (`core/instrument/ui`) — VST3 editor layout: aliases the shared `core::ui::Rect` (+ `contains()`) rather than defining its own; owns `EditorLayout`/`layoutEditor(w,h)`, the Tier-0/Tier-1 sample-list and keymap-editor row layout/hit-test, and — hoisted here off the former `reasampler_editor.cpp` god-TU (Q-W2v, T2-06) — the r11 Sample-face band layout (`SampleBands`/`ClusterRects`/`channelToggleRects`) and the Zone-face content/legend/deck layout, so the editor shell only draws + routes. -- `keyboard_strip` — piano-keyboard strip: MIDI-note→key rect mapping, black/white key layout, hit-test, zone highlight overlay geometry. -- `waveform_view` — waveform/marker geometry: maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap. -- `capture_browser` — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing. -- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search. -- `note_entry` — parses a raw string into a clamped MIDI note [0,127]; accepts plain decimal integers or note names (C4==60, DAW convention). -- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel. -- `trigger_seam` — pure Trigger frames↔fraction converter: owns the shared formula for converting between engine source-frame fade counts and the overlay's fractional representation, threading `startFrame` correctly through pack and unpack directions. -- `velocity_curve` — pure velocity→amp transfer curve: `VelocityCurve` evaluated by a Fritsch–Carlson monotone cubic Hermite spline (no overshoot outside [0,1]). `eval(velocity)` called once per note-on. `flat()` default (y=1, every velocity→unity) replaces the prior fixed `velocity/127` path — a deliberate non-back-compat behavior change (Daniel-approved). -- `embed_strip` — compact single-row control layout for embed mode in the track FX chain. -- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. -- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types. -- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift. -- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge. +## Directory and namespace layout -**VST3 instrument (`shell/instrument/`) — shells:** -- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. **pS-usage:** gains `writeUsageExtState` (prefix-guarded — accepts only `rsusage_`-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant. -- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded keymap via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. -- `reasampler_editor` (`shell/instrument/`: eight face-axis TUs — `editor_session` session/bridge state, `editor_controls` parameter plumbing, `editor_paint_sample`/`editor_paint_browse_zone` paint, `editor_input_sample`/`editor_input_browse_zone` input, `editor_platform` IPlugView/Win32 window plumbing, plus the pure `editor_geometry` layout hoist as the eighth axis; shared internals in `editor_internal.h`, no TU of its own — Q-W2v, T4-11 split of the former god-TU) — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; default face is the capture browser, then single-capture setup, with opt-in zones panel. Drop-onto-editor ingest is NOT shipped (deferred). -- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout/hit-test to `embed_strip`. -- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs. +The top-level split is by the pure/shell discipline: `core/` never includes REAPER or VST3 SDK +types; `shell/` is where those host types are actually touched — the discriminator is "may this +file touch a host type, REAPER *or* VST3 SDK." Subsystem directories sit beneath `core/` (see the +table above); `core/instrument/` further subdivides into `engine/` / `map/` / `ui/`. Namespaces +mirror directories — `reasampler::` for `core/`, house style for `shell/`. `app/` holds +`main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and dispatch. -## REAPER extension contract (src/main.cpp) +For the module list within any one directory — what lives there, its invariants — open that +directory's own `CLAUDE.md` rather than looking here. + +## Performance guardrails (HARD CONSTRAINT — Daniel's non-negotiable) + +The reorg must cost **zero runtime.** The two hot paths must keep their exact call/inline shape; +the following are acceptance criteria on every point: + +- **`peaks` envelope compute** (`computeEnvelope` / `lastFrameAboveThreshold` over full PCM): **NO + virtual dispatch, NO `peaks` interface, NO added header→TU indirection.** `computeEnvelope` + stays a **free function on `const std::vector&`** so it inlines as today. Relocate + + namespace only; never wrap in an abstraction. +- **Audition / preview**: `panel_audition` may be its own TU, but the call stays a **direct + call-through, not virtual.** +- **Realtime-capture tick**: keep the idle fast-path a **single pointer test**; + `realtime_lifecycle` must not change the tick's branch shape. +- **JSON extraction** is **off all hot paths** — safe to abstract freely. +- **`FxBypassGuard`** runs per-capture, not per-frame — keep it **stack RAII**; never + heap-allocate or virtualize it. + +**Net:** every recommended split falls on a cold path or preserves call/inline shape on the two +hot ones. *A split that would add a hot-path indirection is out of scope — rework it or drop it.* + +## Structural heuristics (Daniel, 2026-07-28 — acceptance criteria phase-wide) + +Three heuristics postdate the original framing and bind every wave. They **generalize** the +three-hot-path performance guardrail above — they do not replace it: + +1. **More directories is a must; more files is good; ~600-line file ceiling.** SRP applies to + namespaces, encapsulation, and file organization alike. The ceiling is the *bar*, the audit's + named seams are the *method*: a file landing over ~600 needs a responsibility seam, not an + arbitrary bisection (bisection-to-hit-the-number is rejected). A documented hot-path + exception (`sampler_core.cpp`) is legitimate; silent overshoot is not. +2. **Templates are the right tool for compile-time dedup — use them where earned.** The LE + byte-codec `putLE`/`readLE` is earned: compile-time dispatch, zero runtime cost, off + the hot paths. The rect family is NOT: the types differ in name only, so one + **concrete** `ui::Rect` — a template there would model nothing. +3. **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts *anywhere* — not just + the three named hot paths; prefer static polymorphism where the types are compile-time-known. + A by-class `sampler_core` split that would put virtual envelope `tick()`s on the + per-voice-per-sample path is exactly the blowout this forbids. + +## REAPER extension contract (src/app/main.cpp) - Exactly **one** translation unit defines `REAPERAPI_IMPLEMENT` — that is `main.cpp`. Every other `.cpp` includes `reaper_plugin_functions.h` without the define and gets `extern` declarations for the global API function pointers. - REAPER dlopen()s any `reaper_*.dll|dylib|so` found in `UserPlugins/` and calls the `ReaperPluginEntry` export (produced by `REAPER_PLUGIN_ENTRYPOINT`). `rec->GetFunc` resolves API pointers; `rec->Register` plugs extension callbacks in. @@ -154,19 +156,34 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde ## Product design docs -`docs/product/` holds the product-design reasoning behind each phase — the "why we chose this" that predates the spec. They are large and are cited by section from `CONTEXT.md` and `PLAN.md`; **grep for the cited section rather than reading a file whole**. `docs/cmake-cheatsheet.md` is a standalone build-system reference. +`docs/product/` holds the product-design reasoning behind each phase — the "why we chose this" that predates the spec. They are large; **grep for the cited section rather than reading a file whole**. `docs/cmake-cheatsheet.md` is a standalone build-system reference. Files: `capture-tail.md`, `code-organization.md`, `design-view.md`, `midi-playback.md`, `multi-bank.md`, `provenance.md`, `removal-and-prune.md`, `versioning-and-release.md`, `visual-design-language.md`. +## Project docs + +Plan-style docs live under `docs/`: +- `docs/ARCHIVE.md` — pre-1.0 history, rarely read, not a source of context for current work. +- `docs/COMPLETED.md` — 1.x landed items. +- `docs/TODO.md` — deferred follow-ups with recorded rationale. +- `docs/TODO-1.0.md` — the raw 1.x work list, not yet structured into a plan. + ## The load-bearing principle **Capture and placement are separate acts.** Capturing audio writes a file to the bank and adds an index entry. It **never** puts an item in the arrange view. Placement is a distinct, on-demand action (`insert` module / `InsertMedia`). Any code path that auto-inserts a capture into the timeline violates the purpose of the tool and **must be rejected in review**. ## Precision invariants — required before any feature ships -- **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source. Ship as a verification action. +- **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source — the tool's trust anchor. Ship as a verification action. (Verification action cut per `docs/product/provenance.md` — manual verification only.) - **Bit-identical repeats:** identical offline capture requests produce identical files. - **Non-destructive:** capture never mutates source items or tracks; the realtime backend's temp track is created and removed cleanly, and source routing is restored. - **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; channel count preserved (no silent stereo fold). - **Relative paths only** in the persisted `BankIndex`. - **Capture FX scope:** two scopes only — item = item/take FX only; track = item FX + the selected track's own track FX. There is no master scope (to capture the master, render a track instead). For both scopes, the out-of-scope chain (ancestors + master track, plus the item's own track for item scope) has its FX, gain, and pan/width/pan-law/mode neutralized to unity — the master track is bypassed as out-of-scope chain, not captured as a scope. Range (time selection or razor) is orthogonal. + +## Non-goals / guardrails + +- No auto-insertion of captures into the arrange (see the load-bearing principle). +- Do not depend on REAPER's peak API for thumbnails; compute from the captured file. +- Do not silently time-stretch on insert; conform is opt-in. +- Do not trust cited REAPER API names blindly — verify against the SDK header. diff --git a/COMPLETED.md b/COMPLETED.md deleted file mode 100644 index 1b00005..0000000 --- a/COMPLETED.md +++ /dev/null @@ -1,3258 +0,0 @@ -# COMPLETED.md — ReaSampler landed milestones - -Completed milestone entries removed from `PLAN.md`. Each entry preserves its -original Goal, Verify, and checklist points with boxes marked done. - ---- - -## Milestone 0 — Transition scaffold: reaper_mpeview → ReaSampler -**Goal:** Retire the MPE scaffold and stand up the sampler's pure core in its -place, preserving the pure-core / REAPER-shell split. -**Verify:** `cmake -B build -S .` configures clean; `cmake --build build` builds -the renamed extension target and the pure-core test target; `ctest --test-dir -build` is green with the new `bank_model` + `peaks` suites present. - -- [x] Delete `src/mpe_model.{h,cpp}` and `src/mpe_view.{h,cpp}`; remove - `tests/test_mpe_model.cpp`. -- [x] Rename the CMake `project()` and the extension MODULE target from - `reaper_mpeview` to `reaper_reasampler` (binary `OUTPUT_NAME` likewise); - update `PREFIX ""` / platform SUFFIX blocks to the new target name. -- [x] Replace the pure `mpe_model` static lib + `mpe_model_tests` executable with - `bank_model` (pure static lib) + `bank_model_tests`; keep the CTest wiring. -- [x] Repoint `src/main.cpp`: drop the `mpe_view.h` include and all `MpeView_*` - calls (toggle / IsOpen / OnTimer / Cleanup); stub the extension entry so it - loads, logs to console, and registers nothing MPE-specific. The - `command_id` / `gaccel` / `hookcommand` registration *pattern* is preserved for - reuse (CLAUDE.md §REAPER extension contract) — the MPE action string is removed. -- [x] Choose and record the persistent action-id prefix for the sampler family - (replaces `CEREBELLUM_MPEVIEW_TOGGLE`); this string is forever-stable once - shipped (CLAUDE.md §action registration). -- [x] Refresh `README.md` layout/next-step sections to the sampler module set. - (Landed-work reflection is doc-keeper's; this point exists so the stale MPE - README does not mislead the first implementer.) - ---- - -## Milestone 1 — bank_model + JSON round-trip (pure) -**Goal:** The `Sample` metadata struct and `BankIndex` (add / remove / query / -tier moves / dedup-by-hash) with JSON serialize/deserialize to `std::string`. -CONTEXT-ARCHIVE.md §Data model, §Module architecture. -**Verify:** CTest green. Round-trip is lossless (deserialize(serialize(x)) == x) -across all fields; dedup-by-hash and tier filtering asserted; **relative paths -only** invariant enforced at the model boundary (no absolute path accepted/stored). - -- [x] Define `Sample` with the full field set (id, display name, relative path, - source mode, source range in project time + PPQ, track GUID(s), wet/dry, - channels, SR, length sec + beats, capture tempo, optional key, peak/RMS/LUFS, - clip flag, tier, content hash, provenance, created ts). CONTEXT-ARCHIVE.md §Data model. -- [x] `BankIndex`: ordered collection keyed by id; add / remove / query. -- [x] Hash lookup for dedup-by-content-hash. -- [x] Tier model (scratch | archive) + tier-move + tier filtering; scratch marked - auto-prunable. -- [x] JSON serialize/deserialize to/from `std::string`. -- [x] Tests: full-field round-trip lossless; dedup collapses equal-hash adds; - tier filter/move correct; relative-path invariant rejects absolute paths; - empty-index and malformed-JSON edge cases. - ---- - -## Milestone 2 — peaks (pure) -**Goal:** Compute waveform min/max bins from raw PCM, dependency-free (not -REAPER's peak API). CONTEXT-ARCHIVE.md §Module architecture, CONTEXT.md §Non-goals. -**Verify:** CTest green. Fed a known signal (full-scale sine, ramp), asserted -min/max envelope per bin matches expected within tolerance; channel count -preserved; bin count honored for arbitrary sample lengths (incl. remainder bin). - -- [x] Min/max bin computation from interleaved PCM given a target bin count. -- [x] Multi-channel handling (per-channel envelope; no silent fold). -- [x] Tests: sine envelope ≈ ±amplitude; ramp envelope monotonic; DC/silence → - zero envelope; short-buffer and non-divisible-length edge cases. - ---- - -## Milestone 3 — Offline capture spike (REAPER shell) -**Goal:** Offline-render the time-selection master mix to a wav in the project -bank folder, add a `Sample`, log it. The render-driving spike. CONTEXT-ARCHIVE.md -§REAPER API surface (offline render), Build order 3. -**Verify (in DAW):** Render runs via `Main_OnCommand(42230)` ("Render using most -recent settings") — REAPER always shows its offline-render progress window; no -stock/header-documented fully-headless path exists. File lands in the -project-relative bank folder at **32-bit float WAV** at project rate (lossless, -dither-free → enables bit-identical/null-test). A `Sample` is added to the -in-memory `BankIndex`. Non-destructive. Unsaved-project state triggers a -Save-As prompt; capture is refused if the user cancels (no default-location -fallback). -**Bit-identical repeats:** two identical requests produce byte-identical files. -**Exact bounds:** rendered length matches the requested range (no rounding, no -added silence without an explicit tail). - -- [x] `ICaptureBackend` interface + `CaptureRequest` (source mode, time range, - wet/dry, tail, SR/bit-depth/channels, output path). CONTEXT-ARCHIVE.md §capture. -- [x] `OfflineRenderBackend`: drive `GetSetProjectInfo` render settings + - `GetSetProjectInfo_String` file/pattern/format; **verify every flag against - `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`.** -- [x] Resolve the no-dialog render command/flag on the current REAPER build - (open question) and confirm it runs headless. -- [x] Populate a `Sample` from the finished file; hand to `bank_model`; console-log. -- [x] Verify bit-identical repeats and exact-bounds by hand on a known range. - ---- - -## Milestone 4 — persist (index ↔ project ext state) -**Goal:** Write the `BankIndex` JSON to project ext state, reload on project open; -project-relative path resolution. CONTEXT-ARCHIVE.md §persist, §Persistence & paths. -**Verify (in DAW):** Index survives Save / Save As / close+reopen; **bank travels -with the .rpp**; **relative paths only** in the persisted index (Save As to a new -folder still resolves the bank). - -- [x] `SetProjExtState` / `GetProjExtState` under namespace `"reasampler"`. -- [x] Bank-folder resolution from the current project path - (`EnumProjects` / `GetProjectPathEx`); store under a project-relative subfolder. -- [x] Reload-on-open; confirm survival across Save / Save As. - -**Notes/decisions:** -- Storage: `SetProjExtState` / `GetProjExtState`, namespace `"reasampler"`, keys `bank_index` (serialized JSON) and `project_guid`; relative paths only in the persisted index. -- Project identity: keyed off a **minted GUID** stored in ext state (REAPER exposes no native per-project GUID), not the raw `ReaProject*` — a recycled pointer cannot misread a project switch as a Save-As. -- Save-As: **copy** semantics (Daniel's decision) — the `reasampler_bank/` folder is copied under the new `.rpp`; the old project's bank stays intact. Every ext-state write calls `MarkProjectDirty` so captures/GUID changes flush on the normal save. -- Save-As collision — fixed (DAW-verified): project identity is **GUID-primary** — the stored per-project GUID is the identity of record; a different stored GUID always means a different project (Load its bank), immune to REAPER recycling `ReaProject*` addresses across close/open. The `ReaProject*` pointer is a secondary signal that disambiguates the same-GUID case only: a different object with the same GUID = a Save-As fork (Load + re-GUID to diverge); the same object with the same GUID + a new path = a genuine Save-As in progress (relocate bank). This replaced two earlier iterations: GUID-only (mis-detected forks sharing a copied GUID) and pointer-primary (mis-detected reopen/new-project because it ignored the GUID on address recycling). Non-destructive preserved. - ---- - -## Milestone 5 — bank_panel (docked grid) -**Goal:** Docked LICE-drawn grid: thumbnails (from `peaks`), audition, -multi-select, keyboard navigation. Reuses the docking setup from the retired -`mpe_view.cpp`. CONTEXT-ARCHIVE.md §bank_panel. -**Verify (in DAW):** Grid docks; thumbnails render from computed peaks; audition -plays selected sample; multi-select + keyboard nav work. - -- [x] Docked window + LICE grid render loop. -- [x] Thumbnail draw from `peaks` bins. -- [x] Audition (play selected sample) + stop. -- [x] Multi-select + keyboard navigation. - -**Notes/decisions:** -- Thumbnail cache: in-memory recompute keyed by `(sampleId, drawWidth, bankGeneration)`; peak bins are NOT persisted alongside the index. Cache is discarded on bank change and rebuilt on next draw. (Closes the PLAN "thumbnail cache" open question.) -- Audition: stock `PlayPreview` / `StopPreview` API, read-only — display + select + audition only, never inserts into the arrange. Single stop-funnel ensures a leak-free preview lifecycle. Flagged undocumented assumption: `StopPreview` detaches the source before returning; mitigated by the single-funnel design. Escalation path if a runtime pop appears: switch to `StartPreviewFade` + deferred free. - ---- - -## Milestone 6 — insert (placement) -**Goal:** "Insert selected sample at edit cursor" via `InsertMedia`. CONTEXT-ARCHIVE.md -§insert, Build order 6. -**Verify (in DAW):** Selected sample inserts at the edit cursor wrapped in -`Undo_BeginBlock2` / `Undo_EndBlock2`; conform-to-tempo is an explicit flag — -**no silent time-stretch** when off. - -- [x] `InsertMedia(path, mode)` at edit cursor (verify mode bits against SDK). -- [x] Conform-to-project-tempo vs literal as an explicit flag (never silent). -- [x] Undo-block wrapping. - -**Notes/decisions:** -- Placement target: inserts the focused bank sample onto the **currently selected track(s) at the edit cursor** (Daniel's directive — not a new track). Uses `InsertMedia` base mode 0; for multiple selected tracks the sample is placed on each at the same cursor position, then the original track selection and edit-cursor position are restored — non-destructive to editing state. The entire operation is one undo block. -- No silent time-stretch: the `&4` stretch-to-time-selection bit is never set; a pure `insert_plan` test asserts this across all mode combinations. Conform-to-project-tempo is an explicit separate action (`&8`), never on the default path. -- Non-destructive to the bank: insert only adds arrange items — no bank/file/ext-state writes. - ---- - -## D1 — view_mode_model (pure) -**Goal:** REAPER-free mode registry + membership index + folder-tree-aware -visibility derivation + parking/restore planner + JSON round-trip. The heart of the -phase; mirror of `bank_model`. CONTEXT-ARCHIVE.md §Design View (Module architecture — pure). -**Verify:** CTest green. N-mode model (not a boolean); Arrange + Design seeded. -Restore-planner round-trip (snapshot → park → restore) returns every driven flag to -its captured value. Parent-derivation correct against a supplied folder tree. -JSON round-trip lossless across modes + membership + show-both + snapshots + active -mode. - -- [x] Mode registry: ordered (id, display name, ordinal); Arrange + Design seeded; - add/query more modes (prove N-mode, not binary). -- [x] Membership index: `GUID → { mode ids }` + per-track show-both flag; - add / remove / retag / query; untagged = Arrange. -- [x] Folder-tree-aware visibility derivation: given a supplied parent↔child tree + - active mode, compute the visible set (active leaves, derived-visible parents, - show-both leaves, master always in). -- [x] Parking/restore planner: emit exact (track, flag, value) op-lists for park and - restore from active mode + snapshot record. -- [x] JSON round-trip: modes + membership + show-both + snapshots + active mode. -- [x] Tests: N-mode add/query; parent follows tagged leaf (multi-mode parent); - restore-round-trip returns snapshot values (never hardcoded "on"); show-both leaf - never parked; unknown/stale GUID tolerated; JSON lossless. - ---- - -## D2 — view shell (apply flags in the DAW) -**Goal:** Read the folder tree and drive REAPER flags per the planner. -CONTEXT-ARCHIVE.md §Design View (view shell, REAPER API surface). -**Verify (in DAW):** Toggling active mode hides + parks inactive leaves -(`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline) and restores -active ones from snapshot. **Master untouched. `B_MUTE`/`I_SOLO` untouched.** -Untagged tracks untouched. Parents follow their tagged descendants. - -- [x] Build parent↔child tree from `I_FOLDERDEPTH`; feed to `view_mode_model`. -- [x] Snapshot prior flag values (`GetMediaTrackInfo_Value`) before parking. -- [x] Apply park/restore ops (`SetMediaTrackInfo_Value` for the four flags; - `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`). Verify flag names/signatures. -- [x] GUID resolution: `GetTrackGUID` / `guidToString` / `stringToGuid` (never index). -- [x] Review gate: no path touches master visibility or `B_MUTE`/`I_SOLO`, or any - untagged track's owned flags. - ---- - -## D3 — persist slice (view state ↔ project ext state) -**Goal:** Serialize the view section into the `"reasampler"` namespace alongside the -bank; reapply the active mode on project open. CONTEXT-ARCHIVE.md §Design View (persist). -**Verify (in DAW):** Membership + active mode + snapshots survive Save / Save As / -close+reopen; on open, the active mode's visibility + processing is reapplied. -Saved-while-parked project restores parked tracks from persisted snapshots (not to a -guessed "on"). - -- [x] Serialize/deserialize the view section under `"reasampler"` (shared blob, - distinct section from the bank index). -- [x] Reapply active mode on project open (rebuild tree, run the planner). -- [x] Confirm survival across Save / Save As; snapshot durability across - save-while-parked. - ---- - -## D4 — actions -**Goal:** Bindable action set for the mode workflow. CONTEXT-ARCHIVE.md §Design View -(actions). **Verify (in DAW):** Each action registered (bindable in Actions list); -toggle + mode-jumps MIDI-bindable; tag/untag acts on the current track selection. - -- [x] Toggle active mode (cycle; extensible to cycle-all for >2 modes). -- [x] Activate mode: Arrange / Activate mode: Design (direct jumps). -- [x] Tag selected tracks → Design / → Arrange; Untag selected (= → Arrange). -- [x] Show-both for selected tracks (toggle). -- [x] Register each (`command_id`/`gaccel`/`hookcommand`); toggle + jumps MIDI-bindable. - -**Notes/decisions:** -- New `src/actions.{h,cpp}` — the Design View action family registered via the - `command_id`/`gaccel`/`hookcommand` contract in `main.cpp`; MIDI-bindable. -- New `src/track_guid.{h,cpp}` — shared `MediaTrack*` → canonical GUID-string - formatter; used by both the view shell and the actions layer (single source of - truth for membership keys). -- Wiring: actions drive the D2 view shell and D3-persisted model; saved active mode - is reapplied on project load via a load-signal seam in `persist` (`loadFromProject` - raises it; `main.cpp`'s timer drains it) — `persist` stays model-only. -- A pure `nextModeId` free function added to `view_mode_model` (the N-mode cycle - decision behind "toggle"), unit-tested in the existing `view_mode_model_tests`. - ---- - -## D5 — in-window toggle affordance (UI) -**Goal:** The segmented mode switch in the ReaSampler / bank_panel window header. -CONTEXT.md §Design View (UI). **Verify (in DAW):** Segmented control shows current -mode (lit segment), one click flips modes via the D4 toggle action, per-mode -membership count visible, offlined-FX caveat surfaced as a tooltip. - -- [x] Segmented mode switch `[ Arrange | Design ]` in the window header; active lit. -- [x] Wire the switch to the toggle/activate actions from D4. -- [x] Per-mode membership count display. -- [ ] Offlined-FX re-init caveat as a tooltip on the switch. (Not built — no trace in source; did not survive the Phase L panel redesign.) - -**Notes/decisions:** -- New PURE module `src/mode_switch.{h,cpp}` — REAPER-free layout math for the - Design View mode switch: divides a header rectangle into N equal segments (one per - registered mode) and hit-tests a point to a segment. Unit-tested via a new - `mode_switch_tests` CTest target. Mirror of `bank_grid`. -- `src/bank_panel.cpp` — segmented `[ Arrange | Design ]` control drawn in the panel - header (one lit segment per registered mode, click activates that mode via - `view::applyMode`), with the grid offset below the header. - ---- - -## D2-W1 — view_mode_model lane extension (pure) -**Goal:** Extend D1's pure planner to item level for the two-canvas sub-phase: -lane↔mode mapping, a managed-vs-manual lane-ownership index, managed-only item-lane -ops in the toggle planner, the "which lanes may this toggle touch" query, the -auto-tag decision (manual-lane items exempt; pre-existing ⇒ Arrange), and JSON -round-trip of the lane index. REAPER-free, unit-tested; mirror of D1. CONTEXT.md -§Two-canvas sub-phase (Module architecture — pure). -**Verify:** CTest green; D1 behavior and tests unchanged. - -- [x] Lane↔mode mapping: which lane maps to which mode, which `C_LANEPLAYS` value - per mode. -- [x] Lane-ownership index: per (track GUID, lane) managed-which-mode vs manual; - managed-only item-lane op family alongside the existing track-flag op family. -- [x] "Which lanes may this toggle touch" query (managed only) — planner emits lane - ops for managed lanes only, never for manual lanes. -- [x] Auto-tag decision (pure): new track/item GUIDs + active mode ⇒ membership - writes; manual-lane items exempt; pre-existing ⇒ Arrange. -- [x] JSON round-trip of the lane-ownership index. -- [x] Tests: managed/manual partition; toggle-touches-managed-only; auto-tag - exemption for manual-lane items; JSON lossless; D1 behavior/tests unchanged. - ---- - -## D2-W2 — shell: lane application + new-content detection -**Goal:** The view shell applies the planner's managed-lane ops in the DAW and the -bank_panel timer detects new content and auto-tags it to the active mode. Resolves -the two flagged implementation design points (I_FIXEDLANE reorder/renumber -fragility; the auto-tag / manual-lane detection heuristic). See CONTEXT.md -§Two-canvas sub-phase (Module architecture — shell; New-content detection). -**Verify (in DAW):** Toggling a mode shows + plays only the active mode's managed -lane, hides + silences the inactive-mode lane, and **never touches a manual lane** -(its `C_LANEPLAYS` stays exactly as the user set it); new content created while a -mode is active is tagged to that mode; pre-existing content stays Arrange (no -mass-tag on the first poll after open). -**Depends on:** D2-W1. - -- [x] Apply managed-lane ops in the view shell (`I_FREEMODE`/`I_FIXEDLANE`/ - `C_LANEPLAYS` via the item/track info setters; `UpdateTimeline()` after `I_FREEMODE`); - **managed lanes only, never manual**. `B_FIXEDLANE_HIDDEN` is read-only per the SDK and - is not written — lane visibility follows from `C_LANEPLAYS`. - Verify every flag name/signature against the SDK header. -- [x] New-content detection on the bank_panel timer: diff the live track/item GUID - set against the previous poll; tag any GUID new since the last poll to the - then-active mode, with a **first-poll-after-open guard** (pre-existing ⇒ Arrange, - no mass-tag) and the **manual-lane exemption** (items in a manual lane not tagged). -- [x] Resolve the manual-lane detection heuristic (which new items are exempt) and - the `I_FIXEDLANE` lane-identity fragility (index survival across lane - reorder/renumber/deletion) — the two open design points from CONTEXT.md. -- [x] D2-W1 review polish: document the one-managed-lane-per-mode-per-track - exclusivity assumption in `laneModeState` (comment / debug-guard); clarify the - `serialize()` one-line style note; optional round-trip tests for the - last-writer-wins lane-replace contract. - -**Notes/decisions:** -- Two new pure modules added with unit tests: `guid_diff` (diffs live track/item - GUID sets between polls) and `lane_keys` (manages lane identity via durable - `P_LANENAME` rather than the renumber-prone `I_FIXEDLANE` ordinal, reconciled each - apply — the resolution to the lane-identity fragility design point). CTest green. -- **Manual-lane protection:** a single pure predicate `isOnManualLane` is the - exclusive gate; manual lanes — including REAPER's default unnamed fixed lanes — - are provably never driven or auto-tagged. -- **Track-level auto-tag and park behavior is live.** Item-lane show/hide is - correctly structured but is a provable no-op on real projects until D2-W3 mints - the `reasampler:`-prefixed named lanes. End-to-end DAW verification of item-lane - show/hide is sequenced after D2-W3 for this reason. -- W1 review polish was folded in during this wave. - ---- - -## Milestone 7 — capture action family -**Goal:** Bindable capture actions for master / selected tracks / selected items / -razor area, each with wet-dry + tail options. CONTEXT-ARCHIVE.md §actions, Build order 7. -**Verify (in DAW):** Each action registered (bindable in Actions list), routes to -the offline backend, and honors wet/dry + tail. **Load-bearing principle:** none -auto-inserts into the arrange. - -- [x] Source resolvers: master mix, selected tracks, selected items, razor area - (`GetSet_LoopTimeRange`, `P_RAZOREDITS`, `CountSelectedMediaItems`, etc.). -- [x] Register each as a bindable action (`command_id`/`gaccel`/`hookcommand`). -- [x] Wet-dry + tail options per action. -- [x] Review gate: confirm no capture path touches the timeline. - -**Notes/decisions:** -- Four **wet** bindable capture actions — master mix, selected tracks, selected items, razor area — registered under the `CEREBELLUM_REASAMPLER_CAPTURE_*` command-id prefix. Each routes to `OfflineRenderBackend` (RENDER_* snapshot/restore, dither/normalize off, 32-bit float), produces a `Sample`, adds it to the bank, persists, and calls `MarkProjectDirty`. The M3 spike action was retired. -- **Wet-only decision (Daniel):** REAPER offline render has no true pre-FX "dry" bit — the only wet/dry-adjacent lever (`&8192` pre-fader stems) is post-FX. Approximate-dry action variants were removed rather than ship a "dry" that isn't. `CaptureRequest.wetDry` is retained as the seam for true dry (M10). -- Pure `render_settings` module maps source mode → RENDER_SETTINGS bits and parses `P_RAZOREDITS` (union of track-audio areas), unit-tested. No capture path inserts into the arrange (load-bearing gate); non-destructive (selection/razor read-only). - -**Superseded / reworked (post-landing):** -- The four wet source-mode actions (master/tracks/items/razor) were replaced by **three FX-scope actions** — `capture item`, `capture track`, `capture master` — with range (razor-else-time-selection) inferred orthogonally. This fixed the defect where item captures were rendered through the parent FX chain. -- **FX-scope semantics (initial rework):** item = item/take FX only; track = item FX + the selected track's own track FX; master = full chain. For item/track, the out-of-scope chain (ancestors + master, plus the item's own track for item scope) is neutralized during the render. -- **Master scope subsequently removed:** capture is now **two scopes — item and track only**. `CAPTURE_MASTER` and `CAPTURE_MASTER_REALTIME` are retired (to capture the master, render a track instead). The master track is still neutralized as out-of-scope chain for both item and track captures; it is a bypass target, not a capture scope. The realtime backend taps the selected track (track scope only; item realtime deferred). -- **`FxBypassGuard` (RAII):** snapshot → neutralize (FX bypassed via `I_FXEN`; gain zeroed via `D_VOL`; pan/width/pan-law/mode set to unity via `D_PAN`/`D_WIDTH`/`D_PANLAW`/`I_PANMODE`) → render → restore. Non-destructive. This guard is the reusable mechanism M8 (realtime backend) and M10 (null-test / true dry) build on. - ---- - -## Milestone 8 — RealtimeRecordBackend -**Goal:** Realtime record behind the same `ICaptureBackend`, producing identical -bank entries. CONTEXT-ARCHIVE.md §capture (realtime), CONTEXT.md §Precision invariants. -**Verify (in DAW):** Hidden temp track taps each selected track's own post-fader -output via a `CreateTrackSend`; recorded file moves into the bank; **non-destructive** -— temp track (and its sends) removed cleanly, every snapshotted track arm, time -selection, and edit cursor restored unchanged on every terminal path. - -- [x] Track-scope tap: a `CreateTrackSend(source, temp)` from each selected track - into a hidden temp track (`B_MAINSEND=0`, hidden from TCP/mixer). The temp records - its own post-fader output — capturing each source track's output **after its own FX - and fader, before the parent/folder/master sums it** — chain-independent by - construction. No `FxBypassGuard` needed or used. Multiple selected tracks sum in the - temp track (matching offline track scope). Item realtime deferred (`UnsupportedMode`). - No track selected → refused. -- [x] Timer-driven async state machine (`begin`/`tick`/`abort` driven by `OnTimer`, - non-blocking — REAPER's UI stays responsive across the record). `begin()` validates, - snapshots all state, creates the temp track, routes the tap, arms, calls - `CSurf_OnRecord`, and **returns immediately**. `tick()` (called from `OnTimer`) - reads the transport via `GetPlayStateEx`/`GetPlayPositionEx` scoped to the record's - own `ReaProject*` (project-switch safe), advances the pure `advanceRecordPhase` - state machine, and on a terminal verdict stops + finalizes/restores. `abort()` is - the force-terminate path for shutdown and project switch. -- [x] `RealtimeCaptureState` snapshot + idempotent restore: snapshots cursor, - time selection, and every other track's `I_RECARM`; restore() is latched - (`restored_` flag) and safe to call from whichever terminal path fires first. - Terminal paths: normal completion, manual stop, error, second-capture reject, - project switch (project-scoped `OnStopButtonEx(proj_)`, never the global - `CSurf_OnStop`), **project close** (guarded by `ValidatePtr2(nullptr, proj_, - "ReaProject*")` — a closed project calls `dropWithoutRestore()` rather than - touching freed pointers), and extension unload. -- [x] `Finalizing` flush-wait before file move: after the transport stops, - `tick()` waits for the recorded file size to be positive and stable across a tick - before calling `finalizeRecording` (file is no longer being written by REAPER's - audio thread). A wall-clock ceiling (steady-clock, independent of the play cursor) - bounds both the total record duration and the flush wait separately. -- [x] Move recorded source into bank: `recordedFilePath` discovers the take's source - file from the temp track's first media item; `finalizeRecording` moves it into the - bank folder (cross-volume fallback: copy+remove); populates a `Sample` via - `sampleFromRecordedCapture`; clean teardown via `restore()` deletes the temp track - (which REAPER uses to automatically remove every send routed into it). -- [x] Dialog-free; realtime is inherently non-deterministic (documented, not asserted - bit-identical); saved-project gate (refuses + prompts Save-As if unsaved, matching - offline). Bindable **cancel** action registered. Master scope removed entirely — - to capture the master, render a track. - -**Notes/decisions:** -- **Track scope only this increment.** Item realtime is deferred: item scope needs - per-item take isolation on top of the track-output tap — a separate increment. -- **TAP vs. FxBypassGuard.** The `CreateTrackSend` defaults to post-fader - (`I_SENDMODE=0`) with full-stereo (`I_SRCCHAN` default): post-fader taps the source - track after its own FX and fader/pan, before the parent sums it. The parent chain - downstream of that branch is not in the tapped path at all — so there is nothing to - neutralize and `FxBypassGuard` (which mutates the live chain, altering the user's - monitoring) is deliberately not used. This also fixed the earlier silent-file bug - from the spike, which sent FROM the master INTO a temp track (a feedback loop REAPER - refuses, recording silence). A regular track→track send has no feedback. -- **Project-close guard.** `abort()` gates every REAPER call on - `ValidatePtr2(nullptr, proj_, "ReaProject*")`. A closed project already reclaimed - its temp track, arms, and transport — `dropWithoutRestore()` latches `restored_` - and clears `temp_` / `armSnaps_` without touching any REAPER pointer. -- **No undo block.** The transient mutations (temp track, sends, arm, transport) are - fully reversed by `restore()`; surfacing them as an undo point would pollute the - user's history with an internal scaffold they cannot meaningfully undo. - ---- - -## T1 — offline tail: auto (default) + manual override -**Goal:** Preserve decay tails on offline captures. **Auto**: render an 8 s-capped -tail, then auto-trim trailing silence to -72 dB via a **surgical** `RENDER_NORMALIZE` -(only the trim-end bit, `32768`) + a derived `RENDER_TRIMEND` amplitude ratio. -**Manual**: a fixed tail length clamped to the 8 s cap, no trim. **None** (default): -exact bounds, byte-identical to the pre-tail capture. See -`docs/product/capture-tail.md` §The offline path. -**Verify (in DAW):** A range ending mid-reverb + Auto tail ends at the -72 dB decay -point (not a hard 8 s, not the range end); a non-decaying signal caps at range + 8 s; -**two identical Auto requests are byte-identical** (deterministic trim); a -TailMode::None capture is byte-identical to the pre-tail exact-bounds capture; -Manual(N ms) yields range + N ms untrimmed, with N clamped to 8000; -`ScopedRenderSettings` restores `RENDER_NORMALIZE` and every touched setting on every -path. - -- [x] Pure layer (`render_settings.{h,cpp}`): named constants `kAutoTrimThresholdDb` - (-72) + derived `RENDER_TRIMEND` amplitude ratio via `autoTrimEndRatio()` (≈ - 0.00025119 for -72 dB, computed as `10^(dB/20)` — `std::pow` is not `constexpr` - before C++26 so this is a function, not a constant), `kMaxTailSeconds`/`kMaxTailMs` - (8 s); `TailMode { None, Auto, Manual }` enum; `TailRenderSettings` struct - (tailFlag/tailMs/normalize/trimEnd); `tailRenderSettingsFor(mode, manualTailMs)` - mapping (None = kTailFlagNone + kNormalizeDisableAll; Auto = kTailFlagCustomBounds - + kMaxTailMs + kNormalizeTrimEnd (32768) + autoTrimEndRatio(); Manual = - kTailFlagCustomBounds + clamped ms + kNormalizeDisableAll); unit-tested. -- [x] Wire the mapping into `OfflineRenderBackend` (`capture.cpp`): drives tail + - surgical-normalize (Auto) / disable-all (Manual/None) via `GetSetProjectInfo`; - `ScopedRenderSettings` snapshots and restores `RENDER_TRIMEND` alongside the - existing `RENDER_*` set. `RENDER_TAILFLAG = kTailFlagCustomBounds` (1) for Auto - and Manual — custom bounds is the always-applicable tail bit for offline captures. -- [x] `CaptureRequest` three-state tail contract (None/Auto/Manual(ms)); default - None (exact bounds, null-test-safe). The earlier `renderTail` bool/`tailMs` pair - was superseded. -- [x] Exposure: a **docked-panel footer toggle** (label "Tail: Off" / "Tail: Auto" / - "Tail: Manual", cycles on click via `cycleTailMode`) in `bank_panel.cpp`, backed - by the pure `tail_control` module (`TailSetting`, `cycleTailMode`, - `clampManualMs`, `tailToggleLabel` — unit-tested). Default `TailMode::None`. - `CAPTURE_ITEM` and `CAPTURE_TRACK` read the panel setting at fire time — **no - per-action tail variants shipped** (the "…with tail" variants were dropped in - favour of the toggle; null-test/verify captures use None explicitly). -- [x] DAW-confirm: `RENDER_TRIMEND` amplitude curve (0.00025119 ≈ -72 dB); trim-end-only - normalize (32768) does not engage fades/normalize/pad; trim never eats pre-`ENDPOS` - body. (See spec §Open questions / DAW-confirm.) - -**Notes/decisions:** -- **Surgical normalize.** `kNormalizeTrimEnd = 32768` sets only the trim-ending-silence - bit; every other postprocessing bit is clear. A fixed-threshold trailing-silence trim - scales and fades nothing, so two identical Auto requests trim at the identical sample - → bit-identical repeats hold (spec §surgical normalize). -- **`kNormalizeDisableAll = (4 << 16) = 262144`.** Used for None and Manual — the - same disable-all value the pre-tail exact-bounds capture used. -- **`tail_control` pure module** (`src/tail_control.{h,cpp}`): REAPER-free logic for - the panel toggle. `kDefaultManualTailMs = 2000.0` (2 s). Fine-adjust UI (scroll-wheel - in 250 ms steps) and per-project persistence landed as T1-followons (see below). -- **Follow-ons resolved:** Manual fine-adjust UI and per-project persistence of the - toggle landed as T1-followons. T2 realtime tail landed separately. - ---- - -## T2 — realtime tail (follow-on to T1) -**Goal:** The parallel tail path for the M8 realtime backend, which does not drive -`RENDER_*`: record an 8 s-capped tail window past the range end, then **trim in a -PCM decay-scan** to the -72 dB point (Manual = record fixed tail, skip the scan). -See `docs/product/capture-tail.md` §The realtime path. -**Verify (in DAW):** A realtime Auto capture of a decaying source records ≥ the range -then trims at the -72 dB decay point (± inherent realtime tolerance); realtime tail is -**not** asserted bit-identical (documented non-determinism). -**Depends on:** T1, M8. - -- [x] Record `[start, end + clamp(tail, 8 s)]` (extend the record time selection in - `capture_realtime.cpp`); Manual skips the scan, Auto proceeds to it. -- [x] Pure decay-scan helper alongside `peaks`: `lastFrameAboveThreshold(interleaved, - channels, frames, linearThreshold) -> frameIndex` (backward scan, per-frame max-abs - across channels, no fold); unit-tested with a synthetic decaying ramp. (Spec §realtime - path option (a) — recommended over bending `computeEnvelope`.) -- [x] Realtime shell: read the recorded wav PCM into a float buffer, find the trim - frame, rewrite the file truncated (new I/O the backend does not do today). - -**Notes/decisions:** -- New pure module `wav_trim` (`src/wav_trim.{h,cpp}`): 32-bit-float WAV parse + header-aware - truncate plan (RIFF/data size rewrite). Rejects WAVE_FORMAT_EXTENSIBLE with non-float - SubFormat GUID. Depends on `peaks` for the `AudioSample` float alias. Unit-tested via a - new `wav_trim_tests` CTest target. -- `peaks` gained `lastFrameAboveThreshold` (backward PCM scan, per-frame max-abs across - channels, no fold) for the Auto decay scan. -- **Auto/Manual/Off semantics.** Auto: records `[start, end + 8 s cap]`, scans backward - for the last frame above -72 dBFS, truncates the WAV header-aware at that frame. Manual: - records `[start, end + fixed tail]`, skips the scan. Off: byte-identical to the pre-tail - exact-bounds capture. -- **Realtime tail is non-deterministic by design** (inherent to the realtime backend). - Bit-identical repeats are not asserted for the realtime path; this is documented, not a defect. - ---- - -## T1-followons — Manual fine-adjust UI + per-project tail persistence -**Goal:** Close the two follow-ons deferred at T1 landing: (1) scroll-wheel fine-adjust -of the Manual tail length in the panel footer; (2) the tail setting (mode + Manual length) -persists per-project inside the `.rpp` rather than resetting on extension unload. -**Verify (in DAW):** Scroll-wheel over the footer adjusts Manual length in 250 ms steps, -clamped 0–8 s; the label reads "Tail: Manual X.Xs" (one decimal) in Manual mode; footer -click still cycles Off → Auto → Manual. The tail setting survives Save / close+reopen; -projects with no stored key fall back to Off / 2 s. -**Depends on:** T1. - -- [x] `adjustManualMs(current, notches, stepMs)` pure helper in `tail_control` (per-notch - ±`kManualStepMs` = 250 ms, clamped [0, `kMaxTailMs`]); unit-tested. -- [x] `tailToggleLabel` updated: Manual mode appends the clamped length in seconds to one - decimal, e.g. `"Tail: Manual 2.0s"`; unit-tested at boundary lengths. -- [x] Panel footer scroll-wheel handler calls `adjustManualMs` and repaints; click handler - unchanged (still cycles mode via `cycleTailMode`). -- [x] `serializeTailSetting` / `deserializeTailSetting` pure round-trip (mode + manualMs) - added to `tail_control`; unit-tested including `std::nullopt` on malformed input. -- [x] `TailSetting tail_` promoted into `ReaSamplerSession` (peer to `bank_` and `view_`); - `persist` serializes it under the forever-stable key `"tail_setting"` (namespace - `"reasampler"`) on save and reloads it on project open. Absent key → default Off / 2 s - (graceful for older/unsaved projects). -- [x] Changing the toggle marks the project dirty and commits the value to ext state; - `bankPanelTailSetting()` reads through the session (not a panel-local copy). - -**Notes/decisions:** -- `kManualStepMs = 250.0` — Daniel-set coarse-but-precise step; one wheel notch = ± 250 ms. -- Label format: `"Tail: Manual 2.0s"` (one decimal, `s` suffix) — format pinned by unit tests. -- Default fallback on absent/malformed key: `TailSetting { TailMode::None, kDefaultManualTailMs }` - (Off mode, 2 s stored length) — graceful for projects saved before this feature shipped. -- `kProjExtTailKey = "tail_setting"` is forever-stable (changing it would orphan saved choices, - falling back to the default — graceful but lossy). - ---- - -## D2-W3-A — lane minting + item→lane assignment + persist round-trip -**Goal:** The functional core that makes item-lanes appear: a pure `planLaneMinting` -decision (which tracks hold >1 mode's content, which managed lane each item lands on) -plus the shell apply path in `view.cpp` — enables fixed-lane mode, mints one managed -`reasampler:`-named lane per involved mode, assigns each item (including -pre-existing) to its mode's lane, and drives per-lane play state, all under one undo -block, triggered off the auto-tag detection tick. Reconciles the lane-ownership index -from durable lane names on project load before active-mode visibility is reapplied. -The lane-ownership index persists inside the `"reasampler"` `view_state` blob (rides -in `ViewModeModel::serialize()` / `deserialize()`). -**Verify:** CTest green (14/14). Pure decision unit-tested in `view_mode_model_tests`. -**DAW verification pending** (Daniel testing on dev): two behaviors are -REAPER-runtime-only — whether lane names stick when written on the same tick the track -flips to fixed-lane mode, and whether the leftover empty default lane 0 is silent. -**Depends on:** D2-W2. - -- [x] Pure `planLaneMinting` decision (`view_mode_model.{h,cpp}`): for each reported - track, collect the distinct modes of managed-eligible items; if < 2 modes, no split - (D1 whole-track parking still separates stances); if ≥ 2 modes, emit one - `TrackSplit`, one `LaneMint` per involved mode (durable key = `laneNameForMode(mode)`, - owned by that mode), and one `LaneAssign` per managed-eligible item — including - pre-existing items, so a track that just gained a second mode retroactively lanes all - its content. Manual-lane items (`onManualLane = true`) are exempt at the source: - never counted, never reassigned, never minted-over. -- [x] Shell apply path `applyMintPlan` in `view.cpp`: enables `I_FREEMODE` = fixed - lanes, grows `I_NUMFIXEDLANES` (never shrinks — user's manual lanes are never - deleted), stamps each managed lane's durable name via `P_LANENAME`, records - ownership in the model (`lanes().setManaged`), assigns each item to its mode's lane - via `I_FIXEDLANE` resolved from the durable key. Returns `changed` so the Undo block - is only kept when state actually changed (idempotent re-runs produce no undo point). -- [x] Per-lane play state driven immediately after minting: `planToggle` lane ops - applied via `applyLaneOps` so the freshly-minted lanes take the correct - `C_LANEPLAYS` state for the active mode without a full `applyMode` re-run (which - would re-park/restore whole tracks — not correct for a minting tick). -- [x] `mintManagedLanes` entry point in `view.cpp`: reads live track/item picture via - `readLaneTracks`, calls `planLaneMinting`, wraps the apply in one Undo block labelled - `"ReaSampler: separate cross-mode content into lanes"`, calls `UpdateTimeline()` + - `UpdateArrange()` after a fixed-lane mode change. -- [x] `reconcileManagedLanes` in `view.cpp`: on project load, reads every fixed-lane - track's `P_LANENAME` values; for each name carrying the managed prefix, records the - lane as managed-for-its-mode in the ownership index — pure read of REAPER state, no - lane created or renamed. Called from `main.cpp`'s load path before `applyMode`. -- [x] Lane-ownership index persists via `ViewModeModel::serialize()` / - `deserialize()` — the `LaneOwnershipIndex` is a member of `ViewModeModel` and - round-trips inside the `"reasampler"` `view_state` key alongside modes, membership, - snapshots, and active mode. No new persistence key required. -- [x] Detection tick integration: `mintManagedLanes` is called from the `bank_panel` - timer after the auto-tag pass, so a newly-tagged multi-mode track is split into lanes - on the same tick the content is detected. - -**Notes/decisions:** -- **Single-mode-track rule:** a track carrying content of only ONE mode is not split — - D1's whole-track parking continues to separate its stance from the other mode without - lane overhead. The lane-split only engages when a track genuinely holds ≥ 2 modes' - content. -- **Manual-lane invariant upheld at the source:** `planLaneMinting` never receives - manual-lane items as split candidates. The shell's `readLaneTracks` marks items on - manual lanes `onManualLane = true`; the pure decision skips them entirely. Managed - lanes are always appended (tail ordinals), never overwriting a user's existing lanes. -- **Idempotency:** re-reporting an already-split track produces the same plan; the - shell's ensure/assign writes are no-ops when state already matches. The Undo block is - closed with no label (discarded by REAPER) when the plan is non-empty but every write - was already satisfied, so no phantom undo points accumulate. -- **Review passed** with no Critical or Major findings. - ---- - -## D2-W3-B — item-level mode actions + W3-A polish -**Goal:** Item-level lane/mode-management actions mirroring the track-level Design -View tag family (bindable in the Actions list), plus the three code-review polish -items carried from D2-W3-A. The persist slice and lane-ownership index round-trip -were completed in D2-W3-A; this wave closes the remaining action surface and -cleans up the implementation. -See CONTEXT-ARCHIVE.md §Two-canvas sub-phase (Module architecture — persistence). -**Verify (in DAW):** Item mode actions registered and MIDI-bindable in the Actions -list; re-drive mint/apply so each item lands on its mode's managed lane; manual-lane -items exempt; one undo block per action. ctest 14/14 green. -**Depends on:** D2-W3-A. - -- [x] "Move selected items → Design" action (`CEREBELLUM_REASAMPLER_VIEW_` family): - retags selected items' membership to Design mode, re-drives the existing mint/apply - so each item lands on its mode's managed lane; manual-lane items exempt; one undo - block. -- [x] "Move selected items → Arrange" action: retags selected items' membership to - Arrange mode, re-drives mint/apply; manual-lane items exempt; one undo block. -- [x] "Untag selected items" action: removes selected items' membership, re-drives - mint/apply; manual-lane items exempt; one undo block. -- [x] All three registered (`command_id`/`gaccel`/`hookcommand`); MIDI-bindable. -- [x] W3-A polish — simplified `applyMintPlan`'s redundant `I_NUMFIXEDLANES` re-read: - single grow-and-track pass removes the second `GetMediaTrackInfo_Value` call inside - the mint loop. -- [x] W3-A polish — extracted shared item-read seam (`src/item_read.{h,cpp}`): - removes duplicated `itemGuid`/`itemLaneName` read logic from `view.cpp` and - `bank_panel.cpp`. -- [x] W3-A polish — added reconcile guard in `reconcileManagedLanes`: skips lanes - encoding an unregistered mode id (log and skip rather than silently recording an - orphaned ownership entry). - -**Notes/decisions:** -- ctest 14/14 green; review passed with no Critical or Major findings. -- **Panel UI indicator explicitly deferred** (Daniel's decision): a per-track - lane-split marker has no natural cheap home in the bank panel; the mode switch - already shows the active mode. Preserved as a deferred/backlog note in PLAN.md - Phase D2 — not silently dropped. - ---- - -# Phase V — Versioning & release - -> **New pillar, own lettered namespace.** Version scheme + beta side-channel. -> Namespaced **`V` (Versioning)** alongside `M`/`D`/`B`/`R` — a distinct concern -> (build identity + channel isolation) that touches CMake, `main.cpp`'s -> forever-stable command-id contract, and the `"reasampler"` ext-state. Product -> framing + full option analysis: `docs/product/versioning-and-release.md`. -> Deploy/CD wiring (two named artifacts per platform) hands off to dev-ops. - -## V1/V3 — app_version module: version constant, ext-state stamp, show-version action -**Goal:** Pure `app_version` module — single-source semver from CMake -`REASAMPLER_VERSION "0.9.01"` via `configure_file` → `version_generated.h`; -ext-state writing-version stamp under key `"version"` riding -`saveToActiveProject()`; absent stamp = silent pre-versioning; on-demand -`"ReaSampler: show version"` action (no startup print). New CTest target -`app_version_tests`. -**Verify:** CTest green. Stamp written under `"version"` key on every -`saveToActiveProject()` call. Absent key classifies as `PreVersioning` (silent). -Show-version action fires on demand only. - -- [x] `app_version` pure module (`src/app_version.{h,cpp}`): exports the CMake - version string constant (`appVersion()`), the ext-state stamp value - (`stampVersion()` — numeric triple only, no channel suffix), `parseVersion`, - `versionLess`, `classifyWritingVersion` (empty → `PreVersioning`; unparseable → - `Unknown`; well-formed → `Stamped`). No REAPER types; standard library only. -- [x] `configure_file` wires `REASAMPLER_VERSION` (the one CMake variable) + - `REASAMPLER_CHANNEL_IS_BETA` into `version_generated.h` in the build tree; - `app_version` reads from there — one edit re-threads the version string through - every consumer. -- [x] Writing-version stamp: `persist` calls `SetProjExtState` under - `kProjExtVersionKey` (`"version"`) with `stampVersion()` inside - `saveToActiveProject()` on every save. Absent key on load → `PreVersioning` - (silent; graceful for pre-versioning projects). -- [x] On-demand show-version action (`channelCommandId("SHOW_VERSION")` / - `channelActionName("show version")`): prints the CMake-sourced `appVersion()` - string to the console when fired. **No unconditional startup print** (no version - line added to the extension load message). -- [x] `app_version_tests` CTest target: version parse/compare/classify round-trip; - `PreVersioning` on empty; `Unknown` on malformed; `Stamped` on well-formed; - `versionLess` numeric ordering (10 > 9, not lexicographic). - -## V4 — beta-in-isolation: fully isolated coexisting binary via compile-time channel flag -**Goal:** Compile-time channel flag `-DREASAMPLER_CHANNEL=beta` → fully isolated -`reaper_reasampler_beta` binary: ext-state namespace `reasampler_beta`, FOREVER-STABLE -command-id prefix `CEREBELLUM_REASAMPLER_BETA_`, `"ReaSampler beta: "` action names, -channel-qualified dock title/ident, `0.9.01-beta` display render, bank-panel footer -version/channel readout. Stable build byte-identical to prior identity. -**Verify (in DAW):** Both binaries load simultaneously in one REAPER via the startup -dlopen. Stable produces no change to any existing action id, ext-state key, or panel -string. Beta reads/writes only `"reasampler_beta"` namespace; its actions carry -`CEREBELLUM_REASAMPLER_BETA_` prefix; its panel shows `0.9.01-beta`. No shared-state -collision path between channels. - -- [x] `app_version` extended as the single source of truth for channel identity (V4): - `channel()`, `isBeta()`, `extStateNamespace()`, `commandIdPrefix()`, - `actionDisplayPrefix()`, `binaryName()`, `dockTitle()`, `dockIdent()` — all derived - from the one `REASAMPLER_CHANNEL_IS_BETA` bit. Stable values byte-identical to - pre-V4 build. -- [x] `channelCommandId(suffix)` / `channelActionName(phrase)` composition helpers: - every action-registering shell funnels through these so no shell re-implements the - channel-qualified concatenation. FOREVER-STABLE per channel. -- [x] `configure_file` threads `REASAMPLER_CHANNEL_IS_BETA` (0 for the default build, - 1 for `-DREASAMPLER_CHANNEL=beta`) alongside the version string. Beta binary name, - namespace, prefix, and display suffix all derive from this one bit. -- [x] All shells (`main.cpp`, `actions.cpp`, `bank_panel.cpp`, `persist.cpp`) updated - to compose ids/names via `channelCommandId`/`channelActionName` and read - `extStateNamespace()` — no scattered `#ifdef` forks in the shells. -- [x] Bank-panel footer version/channel readout: displays `appVersion()` (stable: - `"0.9.01"`, beta: `"0.9.01-beta"`). -- [x] The lane-name `reasampler:` prefix is deliberately NOT channel-qualified (shared - naming convention; ownership isolated by namespace). -- [x] Stable build: byte-identical to pre-V4 identity on every string that was - previously shipped. - -**Notes/decisions:** -- The stamp value (`stampVersion()`) is the numeric triple only on BOTH channels — - no `-beta` suffix in the stamp. The channel is carried by the isolated namespace - (`extStateNamespace()`), not baked into the stamp, so the stamp parses as `Stamped` - on read-back and stable's stamp is byte-identical regardless of channel build. -- Two permanent commitments accepted: a second forever-stable command-id prefix - (`CEREBELLUM_REASAMPLER_BETA_`) and a second ext-state namespace - (`"reasampler_beta"`). Beta keybindings are a distinct forever-family from stable's. -- Isolation semantics (accepted, not a bug): a channel reads/writes only its own - namespace — a stable project looks empty/default when opened in beta, and vice versa. - No cross-namespace read, migration, or fallback. -- Deploy implication (dev-ops hand-off): two named artifacts per platform - (`reaper_reasampler` + `reaper_reasampler_beta`), built by toggling - `-DREASAMPLER_CHANNEL`. - ---- - -# Phase B — Multi-bank (parallel to the M0–M11 capture roadmap and Phase D) - -> **Separate phase namespace.** The M-numbers belong to the capture pillar -> (M0–M11); the D-letters belong to Design View. Multi-bank is a third orthogonal -> pillar — generalizing the single bank into a pool + named banks — so it takes its -> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D, -> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth -> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md -> §Multi-bank**. Product framing: `docs/product/multi-bank.md`. - -## B1 — bank_book (pure) -**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded + -privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a -sample between banks, JSON round-trip + legacy-migration. The heart of the phase; -mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**. -CONTEXT-ARCHIVE.md §Multi-bank (Module architecture — pure). -**Verify:** CTest green. Pool always present, un-deletable, un-renamable, -un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is -index-only (source loses entry, destination gains it) and observes destination -collapse-by-hash; copy leaves source intact. Delete drops member index entries. -Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip -lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals + -active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks. - -- [x] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool - seeded with fixed id + fixed name; create / rename / reorder / delete named banks - (delete drops the bank's member index entries). -- [x] Pool-privilege rules enforced in-model: reject delete-pool, reject - rename-pool, reject evacuate-pool, never allow zero banks. -- [x] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`. -- [x] Move sample between banks (index-only; destination collapse-by-hash observed; - source entry removed). -- [x] Copy sample between banks (index-only; source entry retained; destination - collapse-by-hash observed). -- [x] Evacuate bank: move every member to the pool (index-only; destination - collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated. -- [x] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank - indices + ordinals + active id. -- [x] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with - zero named banks (one-way, lossless; blob authoritative thereafter). -- [x] Tests: pool privileges (delete/rename/evacuate rejected); move - source-loses/dest-gains; copy source-retained; evacuate empties source into pool - with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a - bank already holding the hash; JSON lossless; legacy migration. - -> **Phase-B-wide undo (fork R-B, settled 2026-07-24 — batched REAPER undo points).** -> Every index verb across B1–B5 (create/rename/reorder/delete-bank, move, copy, -> evacuate, remove) wraps its bank/index mutation in a **batched REAPER undo point** -> (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. This is -> a cross-cutting decision that retro-touches B1–B4, not a B5-local one; the -> per-verb points above inherit it. **Must-verify before build:** confirm against -> `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly -> in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on -> it. See CONTEXT.md §Sample removal (Guardrails) + product notes §Fork R-B. - -**Notes/decisions (R-B — Phase-B-wide undo, landed):** -- Every bank index verb — bindable action AND panel gesture (menu/drag/Delete key) — - is one batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, - `UNDO_STATE_MISCCFG`); ext-state participates in undo via `UNDO_STATE_MISCCFG` - ("extensions!"), SDK-verified. A `projectconfig` hook - (`BeginLoadProjectState(isUndo)`) triggers a deferred session reload so Ctrl-Z/redo - visibly restores book/view/tail/manifest in-session. Rejected/no-op ops open no - undo point; unsaved-project ops discard the empty block. - ---- - -## B-cap — owned-file manifest seam (capture writes; prune consumes in Phase R) -**Goal:** Capture writes each file it creates into an **owned-file manifest** -persisted in the `"reasampler"` ext-state, so Phase R prune can later distinguish -the bank system's own orphans from hand-dropped files. Consumed only in Phase R -(R1/R2) — landed early here because reconstructing the manifest retroactively is a -backfill cliff (fork R-D, settled 2026-07-24: *defer the feature, design the seam*). -CONTEXT.md §Prune (Settled decisions — orphan attribution) + product notes §Fork R-D. -**Verify:** every file the capture path creates is recorded in the owned-file -manifest; the manifest round-trips through the `"reasampler"` ext-state (Save / Save -As / reopen); relative-paths-only preserved. Prune's consumption of it is Phase R. -**Depends on:** the capture add-path (M7) + persist blob machinery (M4 / B2). - -- [x] Capture records each created file into an owned-file manifest (the set of - files the book has created), persisted in the `"reasampler"` ext-state (sibling - `owned_files` key — persistence shape resolved at build time: sibling key, not - folded into the `banks` blob). -- [x] Manifest round-trips: survives Save / Save As / reopen via the M4 blob - machinery; relative-paths-only. (Consumed by Phase R R1/R2 — not consumed here.) - -**Notes/decisions:** -- Pure `owned_manifest` module (`src/owned_manifest.{h,cpp}`): relative paths, dedup, - JSON round-trip. Deliberately decoupled from `bank_book` — tracks files created, not - index membership; sample-remove is not manifest-remove. Unit-tested via new - `owned_manifest_tests` CTest target. Persisted under the `"owned_files"` ext-state - key. Both capture commit paths (offline + realtime) record created files. Joins the - undo-reload set. - ---- - -## B2 — persist slice (banks ↔ project ext state) -**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the -existing sections, with the pool folded in as bank-zero; migrate a legacy -`bank_index` key into the pool on first load and retire the legacy key; reload-on-open -and Save-As survival via the existing M4 machinery. CONTEXT-ARCHIVE.md §Multi-bank (persist). -**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive -Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`; -a project saved before this phase (legacy `bank_index` only) loads as pool + zero -named banks with no sample loss, and after save carries `banks` with no `bank_index` -written. -**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the -`banks` blob, legacy key retired after one-way migration.) - -- [x] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside - the blob; distinct section from `view_state`; no `bank_index` key written going - forward). -- [x] Legacy-migration path on load: absent `banks` + present `bank_index` → promote - into pool, mint the blob, treat blob as authoritative (legacy key retired). -- [x] Session exposes the book; the active bank's `BankIndex` is the capture add - target (route the M7 capture family through it — additive to M7, no M7 rewrite). -- [x] Confirm survival across Save / Save As; confirm legacy-project load path. - ---- - -## B3 — actions -**Goal:** Bindable action set for the multi-bank workflow. CONTEXT-ARCHIVE.md §Multi-bank -(actions). **Verify (in DAW):** Each action registered (bindable in Actions list); -bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate -drive the B1 model via the B2-persisted session. -**Depends on:** B1, B2. - -- [x] Create bank / rename bank / delete bank (delete drops member index entries; - confirm-on-non-empty offered at the UI layer in B4). -- [x] Evacuate bank → pool (move all members back to the pool; refuses on the pool). -- [x] Activate bank (direct-by-id + cycle). -- [x] Move selected samples → bank / copy selected samples → bank (move is default). -- [x] Pool full-height / banks full-height toggles. -- [x] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy - + evacuate MIDI-bindable. - ---- - -## B4 — bank_panel vertical split (UI) -**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region -below, full-height toggles — extending the M5 docked grid. CONTEXT-ARCHIVE.md §Multi-bank -(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip -below (empty when no named banks, one tab per named bank); active-bank **unmistakably** -indicated; both full-height toggles collapse the split correctly; sample move/copy -affordance works; non-empty delete confirms and offers evacuate; the Design View mode -switch in the header is unaffected. -**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 — -settled 2026-07-23: LICE-drawn tabs + both move affordances; see Phase B open questions -and product notes → *Fork 5 — settled*.) - -- [x] Vertical split: pool grid region (top) + named-banks tab-page region (bottom). -- [x] Named-banks tab strip: **LICE-drawn** (matching the M5 grid + Design View - segmented switch, not SWELL-native — fork 5a); one tab per named bank; empty state - when none. -- [x] Tab-strip overflow/scroll affordance (fork 5a): scroll/chevron overflow shipped - with the strip. -- [x] Pool full-height / banks full-height toggle affordances wired to B3. -- [x] Active-bank indicator — **visually unmistakable** (settled constraint). -- [x] Create / rename / delete / activate / evacuate affordances driving B3 actions. -- [x] Delete confirms on a non-empty bank, naming the evacuate alternative. -- [x] Sample move affordance — **both** (fork 5b): a "move to bank" menu on the current - selection (bindable front-end for the B3 move action) **and** drag-between-regions. - Copy is the deliberate secondary act, offered on the menu. -- [x] Drag mis-drop mitigation (fork 5b): clear drop-target highlighting on the - destination region/tab during a drag. - -**Notes/decisions:** -- New pure module `src/tab_strip.{h,cpp}`: named-banks tab-strip geometry (B4) — - strip rect + N tabs + scroll offset → per-tab rects (overflow-clipped), overflow - chevron reservation + maxScroll, and point → tab/chevron hit-test. Unit-tested via - `tab_strip_tests` CTest target. Mirror of `mode_switch`. -- Active-bank indicator placement (the open polish detail from the Phase B open - questions) was resolved at build time in the panel implementation. -- Post-landing: m11's console-chatter policy applied to Phase B messages (successes - silent, failures kept). - ---- - -## B5 — sample-remove (the missing sample-level verb) -**Goal:** Drop an individual `Sample`'s index entry from a bank or the pool — -the sample-level companion to move/copy/evacuate/delete-bank. Index-only, -non-destructive to the file; exposes the `BankIndex::remove` primitive that -`bank_model` already has (wires it, does not add it). CONTEXT.md §Sample removal. -Product framing + open forks: `docs/product/removal-and-prune.md` §Sample-remove. -**Verify (in DAW):** Remove drops the selected sample's entry from the target -bank; a same-hash entry in another bank is untouched (no cross-bank dedup); -pool *contents* are removable while pool-container privileges hold; removing the -last index reference to a file leaves that file on disk (orphaned until prune — -never deleted by remove); non-destructive (index + ext-state only, no file, no -timeline item). -**Depends on:** B1, B2, B3 (action set), B4 (panel affordance). - -- [x] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a - bank's index; pool contents removable, pool-container privileges unchanged. -- [x] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`), - MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A, settled - 2026-07-24: **this-bank** is the default and only surfaced affordance; all-banks - stays a latent seam-only parameter, not shipped). -- [x] `bank_panel` remove affordance on the current selection (reuse M5 selection - model, as move/copy do). -- [x] Silent remove: no confirm dialog; recoverability via batched REAPER undo - (R-B) — one Ctrl-Z restores the index entry; files are never deleted by remove. -- [x] Tests: remove drops the target entry; same-hash entry in another bank - survives; remove-from-pool allowed; last-reference remove leaves an orphan (file - untouched); non-destructive (no file/timeline mutation). - -**Notes/decisions (B5 forks — settled 2026-07-24):** -- **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in - view only; the `scope: this-bank | all-banks` seam stays in the action signature - but all-banks is a latent parameter, not a surfaced verb. -- `hashReferencedElsewhere` cross-bank reference query in `bank_book` retained as - a tested model API for Phase R prune; the remove shells no longer call it. -- Both `bank_panel` context menu ("Remove selected sample(s)") and Delete key - affordance wired; panel gesture is also one batched undo point (R-B applies). - ---- - -## Milestone 10 — provenance (re-capture from source) -**Goal:** Populate `Sample.provenance` (parent sample id + a capture-recipe -fingerprint) on resample-from-sample, and ship a **"re-capture from source"** -action that regenerates a sample from its recorded source. Reconciled with the -dual-canvas (Phase D2) model. CONTEXT-ARCHIVE.md §Data model, §capture; product framing + -the settled reconciliation in `docs/product/provenance.md`. -**Verify (in DAW):** A sample resampled from a bank sample carries its parent id + -recipe fingerprint; "re-capture from source" regenerates the file into the bank -(never auto-inserting into the timeline — load-bearing principle); re-capture with -an unchanged source + request is byte-identical to the original (bit-identical -repeats); non-destructive to source items/tracks. - -> **Reshaped from the old "provenance + null-test verify" M10.** **Cut (fixed by -> Daniel):** the null-test verification *action* and the true-pre-FX-dry *mechanism* -> the old note required — both dropped, see `docs/product/provenance.md` §What was -> cut. **Kept:** provenance + re-capture. The `Sample.provenance` struct and its JSON -> round-trip **already exist** (M1) — M10 populates and consumes the field, it does -> not add it. Fork picks settled by Daniel (2026-07-23): **P1=a thin fingerprint, -> P2=a bank-only re-capture**; P3/P4 moot under P2=a. - -- [x] Populate `Sample.provenance` on resample-from-sample: `parentSampleId` (the - bank sample the capture derived from) + `fxChainSnapshot` as a **thin capture-recipe - fingerprint** (scope + source FX-chain identity/hash at capture time — a drift/repro - fingerprint, NOT a serialized pre-FX-dry chain to restore; P1=a settled). -- [x] "Re-capture from source" action (`RECAPTURE_FROM_SOURCE`, channel-composed): - regenerate a provenanced sample by re-running its recorded capture request against - the source's **current** state; update the bank file + Sample in place - (`BankIndex::updateInPlace` / `BankBook::updateSampleInPlace` — order-preserving, - id-stable; old file becomes a Phase R orphan). **Bank-only — never inserts/re-places - into the timeline** (load-bearing principle). Reports drift if the source changed - since capture. -- [x] Verify: re-capture of an unchanged source is byte-identical to the original - capture (bit-identical repeats); non-destructive (`FxBypassGuard` snapshot/restore - as M7); relative-paths-only preserved. - -**Dual-canvas reconciliation (settled — `docs/product/provenance.md`):** With -bank-only re-capture (P2=a), provenance is **pure per-sample bank metadata**, -`bank_model` and `view_mode_model` **stay decoupled**, and M10 touches **no** canvas -code. Dual-canvas compliance is satisfied by staying on the right side of the -capture-never-places line — not by any new coupling. Forks P3 (canvas/lane memory in -provenance) and P4 (re-capture auto-tag interaction) were only live under -re-capture-and-replace (P2=b) and are **closed as moot**; if the user manually -re-places a regenerated sample, the existing D2 mode-aware placement rule governs. - -**Notes/decisions:** -- Pure `provenance` module: `rsprov1` length-prefixed encoding; captures scope, exact - range, tail, rate/channels, track GUIDs, order-sensitive FX-chain identity; - parse/compare for drift detection. NOT a serialized chain to restore. -- `provenance_shell`: FX-chain identity queries via `TrackFX_*` / `TakeFX_*`; - source-item path collection; parent-detection inputs. Item scope fingerprints take - FX via `TakeFX_*`; track scope fingerprints track FX. Stamps `Sample.provenance` - when every resolving source item maps by exact normalized path (case-folded on - Windows) to exactly one bank sample — ambiguous/mixed cases conservatively record - nothing. -- **Cut items (correct per spec, not built):** the null-test verification action and - the pre-FX dry path. Both were explicitly removed at M10 reshaping - (`docs/product/provenance.md` §What was cut). -- New CTest target `provenance_tests`. - ---- - -## Phase B open questions — all resolved - -All five forks settled by Daniel (2026-07-23): persistence key = fold pool into `banks`, -retire legacy key (1a); delete drops members + add evacuate verb (2); move is the -default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator (4); -LICE-drawn tabs + overflow, and both move affordances with drop-highlighting (5). -Active-bank indicator placement (the one residual polish detail) was resolved at -build time. B5 forks R-A and R-B settled 2026-07-24 (see B5 and B1 notes above). -Both in `docs/product/removal-and-prune.md` §Fork R-A / §Fork R-B. - ---- - -# Phase R — Reclaim (file lifecycle: the prune path) - -> **New pillar, own lettered namespace.** Prune is the file-lifecycle path the -> capture and multi-bank specs forward-reference throughout ("files persist on disk -> until prune") but that had no phase, module, or point. It is the **only** operation -> in ReaSampler that deletes bytes off disk. Namespaced **`R` (Reclaim)** alongside -> `M`/`D`/`B` because it is a distinct pillar — it serves *every* orphan-producing -> path (delete-bank, sample-remove B5, potentially M10 re-capture), not just -> Multi-bank, and it carries a new risk class (file deletion) with its own -> invariants. Authoritative spec: **CONTEXT.md §Prune — file-lifecycle spec**. -> Product framing + phase-placement justification + forks: -> `docs/product/removal-and-prune.md` §Prune. -> -> **Boundary (load-bearing):** *remove creates orphans; prune reclaims them.* No -> operation other than prune deletes a file; prune deletes only files no index -> references. A bank op that deletes a file is still a bug. -> -> **Depends on:** B1, B2 (needs the multi-bank book to union the referenced-set -> across all banks) and B5 conceptually (sample-remove is a primary orphan-producer, -> so remove-then-prune is the coherent pair — mirror of evacuate-then-delete). Does -> **not** depend on the B3/B4 UI. - -## R1 — prune-reconcile core (pure) -**Goal:** REAPER-free, filesystem-free reconciler — given the files present in the -bank folder, the files referenced by the book (unioned across all banks, pool -included), and the **owned-file manifest** (fork R-D, written from capture onward by -B-cap), compute the orphan set `(owned ∩ present) − referenced`. The mirror of -`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs). -CONTEXT-ARCHIVE.md §Prune (Module architecture — pure). -**Verify:** CTest green. **Prune null test:** a folder whose every file is -referenced deletes nothing; prune returns exactly `(owned ∩ present) − referenced` -and nothing else. Referenced-set unioned across every bank (a file referenced by any -bank — including via a copy — is never an orphan); a present-but-not-owned file (a -hand-dropped file) is never an orphan. - -- [x] Prune-reconcile pure function: `(present, referenced, owned) → orphans`, - computing `(owned ∩ present) − referenced`; referenced unioned across the whole - book (copies keep a file alive). -- [x] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)− - referenced; a copied file referenced by a second bank survives; a present-but- - unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty - manifest edge cases. - -**Notes/decisions:** -- New pure module `src/prune_reconcile.{h,cpp}`: exports `pruneOrphans(present, - referenced, owned)` (the safety-critical set algebra), `buildPruneReport` - (count/bytes/display-capped list, unit-testable), and `pruneDeletePlan` (the R3 - confirm-time staleness intersection — `confirmed ∩ freshOrphans` in confirm order). - Exact-string path match throughout (no case-folding, no separator normalization). - `BankBook::referencedPaths()` additive const union query (all banks incl. pool, - de-duped) added to `bank_book`. New `prune_reconcile_tests` CTest target. - ---- - -## R2 — prune shell + persist wiring (filesystem I/O, thin) -**Goal:** Enumerate the current project bank folder (M4 project-relative resolution), -supply the referenced-set and the **owned-file manifest** (from B-cap) from the -session, feed the pure core, and produce a dry-run manifest. No deletion in this -wave — the report path only. CONTEXT-ARCHIVE.md §Prune (persist / prune shell). -**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list -for a small set) against the resolved current bank folder; resolves paths the same -way the index does (survives a Save-As relocation); deletes nothing. -**Depends on:** R1, B1, B2. - -- [x] Prune shell: enumerate the resolved current bank folder; feed the pure core. -- [x] Session supplies the referenced-set (union across the book) **and the - owned-file manifest** (written by B-cap); resolve the bank folder via the M4 - project-relative machinery. -- [x] Dry-run manifest: orphan count + reclaimed size (+ files for a small set); - **no deletion in this wave.** - -**Notes/decisions:** -- `ReaSamplerSession::pruneDryRun()` (read-only, non-throwing) enumerates the - resolved current bank folder, unioning `book().referencedPaths()` and - `owned().paths()`, feeds `pruneOrphans`, and calls `buildPruneReport` with a 64-file - display cap. Pure `bankRelativeForName` (`capture_paths`) normalizes the folder- - enumeration spelling to match the index convention so the pure core's exact-string - match lines up. Forever-stable `BANK_PRUNE_FOLDER` action registered (dry-run report - to console in R2; deletion wired in R3 behind the same action id). - ---- - -## R3 — deletion + action (the destructive step, guarded) -**Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and -a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the -orphan set — via OS trash where portably available (fork R-C), else unlink. -CONTEXT.md §Prune (guardrails) + CONTEXT-ARCHIVE.md §Prune (API). -**Verify (in DAW):** "Prune bank folder" (action or panel button) reports first, -deletes only on explicit confirm, and reclaims exactly the orphan set — never a -referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety -holds; deletions route to OS trash where available; non-bank and capture invariants -untouched. -**Depends on:** R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24. - -- [x] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`), - dry-run-first, confirm-to-delete. -- [x] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder" - action through the existing command-id contract — the panel affordance alongside - the bindable action; split: button hit-test/layout is pure (mirror of - `mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell. -- [x] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash - where a portable move-to-trash is verified available, else unlink behind the - dry-run/confirm guardrail. -- [x] Orphan attribution (fork R-D, settled owned-file manifest): reclaim only - `(owned ∩ present) − referenced` — the bank system's own leavings, never a - hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.) - -**Notes/decisions:** -- Deletion is guarded: dry-run → REAPER `ShowMessageBox` confirm (count+bytes+files) - → `pruneDeletePlan` staleness intersection (confirmed ∩ fresh pure-core output) → - delete exactly the plan. Zero ext-state writes, no undo point (file deletion is not - REAPER-undoable by design). -- **Windows:** routes to Recycle Bin via `SHFileOperationW` + `FOF_ALLOWUNDO` - (verified against SDK 10.0.26100). **macOS / Linux:** no portable SWELL trash - surface; falls back to `unlink` behind the dry-run/confirm guardrail. -- Manifest entries are deliberately NOT removed on deletion (the owned-file manifest - algebra self-cleans: a deleted file will drop from `present` on the next prune scan, - and `pruneOrphans` returns `(owned ∩ present) − referenced` — the absent file - contributes nothing regardless). -- New pure module `src/prune_button.{h,cpp}`: layout (`computePruneButton`) and - hit-test (`hitTestPruneButton`) for the footer prune button, right-anchored, - suppressed gracefully when the footer is too narrow. Mirror of `mode_switch` / - `tab_strip`. New `prune_button_tests` CTest target. -- `bank_panel` footer button dispatches `BANK_PRUNE_FOLDER` via `Main_OnCommand` - through the registered command id (the same action as the bindable menu entry — - no duplicate logic). - ---- - -## Milestone 11 — polish (wave 1) -**Goal:** Batch capture (per selected item / per razor area), action trigger buttons -+ keybinding help labels, conform-on-insert. CONTEXT-ARCHIVE.md Build order 11. -**Verify (gates 23/23 both configs):** CTest green on new pure targets; each -in-panel action fires through the command-id contract without regressing precision -invariants. - -- [x] **Batch capture** (`batch_capture` pure module + `CAPTURE_BATCH_ITEMS` / - `CAPTURE_BATCH_RAZOR` actions): plan source ranges → ordinal units; mixed-result - aggregation; actions fire one bank sample per selected item / per razor area; - transient per-unit selection with RAII restore; per-unit invariants + provenance; - single persist per batch; one summary line. `RunCapture` internals extracted to a - shared `captureAndIndexOne` helper (behavior identical). New CTest target - `batch_capture_tests`. -- [x] **Action trigger buttons + keybinding help labels** (`action_buttons` pure - module + bank_panel strip): strip layout/hit-test with min-width overflow-hiding - and label formatting with "(unbound)" fallback; struct `ActionButtonRect`; a 28px - LICE button strip in bank_panel between the split body and tail footer fires - capture item/track, realtime start/cancel, insert native/conform, re-capture-from- - source via `NamedCommandLookup` + `Main_OnCommand`; labels show live bindings via - `kbd_getTextFromCmd`. Coexists with Phase R's prune button (footer). New CTest - target `action_buttons_tests`. -- [x] **Conform-on-insert** — verified already shipped (both insert variants were - registered actions since the insert milestone); no new code. Closes as - verified-extant. -- ~~[ ] **Resample-and-mute-source** — Cut (fixed by Daniel, 2026-07-26).~~ Rationale: - the Design View mode projection (park/hide inactive-mode content) supersedes the - mute-after-capture workflow; a mute action would be redundant with the dual-canvas - architecture. Mirror of the null-test cut precedent ("Cut (fixed by Daniel)"). - -## Milestone 11 — polish (wave 2 / completion) -**Goal:** Native OS drag-out — the final M11 polish item. CONTEXT-ARCHIVE.md Build order 11, -§Non-goals (drag-out deferred to last). -**Verify (gates 24/24 both configs):** Drag-out places a valid file in the OS target -without regressing the precision invariants; copy-only semantics throughout (no source -deletion on drop); internal move/copy drag unchanged. - -- [x] **`drag_out` pure module** (gesture-boundary decision): internal drag becomes - OS-bound when the pointer leaves the panel client rect; path-list assembly with - dedupe and missing-file skip. No REAPER types at the boundary. New CTest target - `drag_out_tests`. -- [x] **`drag_out_win` shell** (Windows): OLE `DoDragDrop` / `CF_HDROP`. Copy-only - structurally — `DROPEFFECT_MOVE` is not offered and no source-deletion path exists; - prune remains the sole file-deleter. macOS/Linux via `SWELL_InitiateDragDropOfFileList` - with a documented copy-semantics caveat (SWELL does not expose a drop-effect query). -- [x] `bank_panel` additive hook only — internal move/copy drag unchanged. - -**Notes/decisions:** -- Copy-only is structural, not a policy flag: `DROPEFFECT_MOVE` is never offered on - Windows, so the OS never signals a move. The SWELL path cannot query drop-effect; - copy semantics are documented as a known caveat for macOS/Linux. -- Prune remains the sole authority for deleting files off disk; drag-out does not - remove the source file or any bank index entry. -- This wave completes **Milestone 11** in full. - -## Phase R forks — settled 2026-07-24 -- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.** - Route to OS trash where a portable move-to-trash is available (recoverable), else - unlink behind strong dry-run/confirm. Per-platform trash surface verified at build: - Windows `SHFileOperationW` + `FOF_ALLOWUNDO` (SDK 10.0.26100); macOS/Linux no - portable SWELL trash surface → unlink fallback. Folded into R3. -- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, - `(owned ∩ present) − referenced`; folder-sweep rejected as unsafe. **Seam lands - early** — the manifest is written from capture onward (new **B-cap** point in - Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape - (sibling `"reasampler"` key vs. `banks` blob) resolved at build time: sibling - `"owned_files"` key. -- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**, - dry-run-first, confirm-to-delete. No background sweep. The earlier optional - delete-time "…and prune now" convenience was **not** selected — out of scope. - Folded into R3. - -Both docs of record: `docs/product/removal-and-prune.md` §Fork R-C/R-D/R-E and -CONTEXT.md §Prune (Settled forks). - ---- - -# Phase L — Look-and-feel (system-wide visual design language) - -> Separate phase namespace. Namespaced **`L` (Look-and-feel)**, orthogonal to and ungated -> by the M/D/B/R/V/S pillars. Authoritative spec: **CONTEXT.md §Phase L**. Product -> framing + settled decisions (DS-1/DS-2/DS-3): `docs/product/visual-design-language.md`. - -> **New pillar, own lettered namespace, taken up by a parallel team.** Phase L is the -> whole-system look-and-feel effort: a shared LICE drawing kit and the surfaces that -> adopt it, so ReaSampler and ReaSampler 9000 shed the flat "temple os" drawing for a -> modern, sleek 2026 dark synth look. It answers Daniel's post-DAW-test verdict on the -> instrument ("this looks like temple os… the VST is dogshit… scope it for the whole -> system… does Cockos have a toolkit?"). Namespaced **`L` (Look-and-feel)** so it is -> orthogonal to and ungated by the M/D/B/R/V/S pillars — a parallel team owns it while -> Phase S feature work proceeds independently. Authoritative spec: **CONTEXT.md §Phase L -> — visual design language (design-system spec)**. Product framing, the settled decision -> record (DS-1/DS-2/DS-3 all SETTLED 2026-07-26), palette, and the three visual -> directions: `docs/product/visual-design-language.md`. -> -> **L1 (shared LICE drawing kit — the foundation), L2 (dock-panel layout redesign), L3 -> (VST editor + embed-strip restyle), L4 (dock-panel button layout enhancement), L5 -> (dock-panel button refinements), L6 (toolbar polish), and L7 (capture ordering, card -> metadata, and selection styling) have all landed** — -> `theme`/palette module, `component_geometry` geometry/hit-test helpers, `draw_kit` shell, GDI -> `DrawText` retirement in `bank_panel` (L1); `action_bar` pure task-grouped layout module, full -> M11-aware button inventory placed by task cluster, `bank_panel` redesigned through the L1 kit -> (L2); VST editor (`reasampler_editor.cpp`) + embed strip (`reasampler_embed.cpp`) restyled -> through the L1 kit — REAPER-grey neutrals + three pastel accents, pastel spectral keyboard -> strip + zone bars, hover/pressed/drag states, local `kCol*` forest-green palette retired (L3); -> three-zone layout (top capture/placement/maintenance toolbar, bottom Design-View toolbar, -> footer toggle + Tail button + Prune), `footer_bar` pure module, `ActionCluster::Tagging`/ -> `Switching` in `action_bar` (L4); top-bar overflow menu (`overflow_menu` pure module), -> custom LICE-kit hover-delay tooltips (`tooltip` pure module), opposite-mode Item/Track tag -> buttons + Show Both, Toggle + Activate-Arrange/Design buttons removed, grouping spacing -> widened (`mode_enable` pure module) (L5); single-row button faces, keybinding in tooltip, -> Cancel RT moved to overflow, top-bar cluster order tidied (L6); per-bank `SlotMap` -> (id→slot) in `bank_book`, sparse-grid rendering, `card_drag` + `card_meta` pure modules, -> `captureTimeSigNum`/`captureTimeSigDenom` on `Sample`, tertiary-border selection (L7). See -> individual entries above. **Phase L is complete.** -> -> **This section is self-contained for a team without Phase S context.** Where a point -> touches a Phase S surface (the VST editor, the embed strip, the keyboard strip), the -> gate is stated explicitly so the team does not chase files that are not on dev yet. -> -> **Settled decisions (Daniel, 2026-07-26 — see `docs/product/visual-design-language.md` -> §6):** -> - **DS-1 — toolkit: LICE + WDL free game, no external frameworks.** Draw the modern -> look with LICE directly; reuse any useful WDL/vwnd piece (skin/image helpers, draw -> idioms, a control like the scroll listbox) where it beats re-deriving — "don't -> reinvent the wheel." Reject iPlug2 / JUCE / VSTGUI (external frameworks re-opening the -> settled bare-SDK+LICE build shape). Keep hit-test geometry in pure CTest-covered -> modules — do not import vwnd's retained-mode object model wholesale. -> - **DS-2 — visual direction: Direction B ("Neon Console") + Direction C's spectral -> keyboard strip. SETTLED 2026-07-26, REVISED 2026-07-26 (Daniel) — palette-only.** -> *Neutral surfaces (revised):* the neutral ladder moved **from near-black up into REAPER's -> mid-grey theme family** so the dock reads as part of REAPER, not a black slab — `bg/base` -> ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈ `#3a3a3a`, `line/hairline` ≈ `#4a4a4a`, -> `text/primary` ≈ `#dcdcdc`, `text/dim` ≈ `~#a0a0a0`+ (elevation-ladder discipline -> unchanged). *Accent layer (revised):* now a **three-accent pastel system** — -> `accent/primary` pastel lime green (live/active/selected), `accent/secondary` pastel teal -> + `accent/tertiary` pastel purple (categorical distinctions) — replacing the original -> single electric cyan. The spectral keyboard strip is a **pastel** sweep anchored on the -> three accents. *Tight WCAG pairs to re-verify against the grey ladder:* `text/dim`-on-grey -> (mid-grey-on-mid-grey, AA 4.5:1) and the three pastels-as-indicators on `bg/cell` (shrunk -> from ~15:1 to ~6:1–7:1). **A stylish/bundled-font upgrade was considered and DECLINED -> (Daniel):** no font bundling/redistribution — the kit keeps its current cached-font face, -> no new typeface. The kit palette stays abstract (roles, one constants block — three accent -> roles + grey neutrals), so the direction is a single-file change; final hex is locked -> against the theme WCAG tests within the pastel intent. Detail: -> `docs/product/visual-design-language.md` §2.1/§4/§6 + CONTEXT.md §Phase L. -> - **DS-3 — dock-panel scope: a thorough layout redesign, not a light re-skin.** L2 lays -> out the full button inventory (including M11's action-button additions) intuitively, -> uncluttered, and useful — then applies the kit. Sequenced after M11 merges. - -## Phase L — sequencing -``` -L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED] - ├─────────► L3 (VST editor + embed-strip restyle) [LANDED] - └─────────► L4 (dock-panel button layout enhancement) [LANDED] - └────► L5 (dock-panel button refinements; ungated, after L4) [LANDED] - └────► L6 (toolbar polish; ungated, after L5) [LANDED] - └────► L7 (capture ordering + card metadata + selection styling; ungated, after L6) [LANDED] -``` -L1, L2, L3, L4, L5, L6, and L7 have all landed. **Phase L is complete.** - -## Phase L — must-verify-before-build -- **LICE design-kit surfaces (L1)** — `LICE_GradRect`, `LICE_RoundRect`, AA - `LICE_Line`/`LICE_FLine`/`LICE_ThickFLine`/`LICE_Circle`/`LICE_FillCircle`/ - `LICE_DrawCBezier`, `LICE_FillTriangle`/`FillTrapezoid`/`FillConvexPolygon`, and the - `LICE_CachedFont`/`LICE_IFont` font engine (`SetFromHFont`, AA `DrawText`, shadow/outline/ - glow FX flags). Verified *present* in `vendor/WDL/WDL/lice/lice.h` + `lice_text.h` - (design-language doc §1.1); **confirm exact signatures + the `LICE_CachedFont`↔`HFONT` - lifecycle at build.** -- **WDL/vwnd reuse assessment (DS-1)** — at build time, evaluate whether a vwnd piece beats - re-deriving it: `virtwnd-slider.cpp` / `vwnd_slider_drawknobstack` as the slider/knob - drawing reference, `virtwnd-listbox.cpp` as a candidate scroll listbox, `virtwnd-controls.h` - for `WDL_STYLE_*` gradient hooks, `virtwnd-skin.h` for image-skin helpers. Reuse where - useful; keep hit-test geometry pure regardless. -- **M11 button inventory (L2)** — resolved at L2 build: inventory taken against dev after - M11 merged; all buttons placed by task cluster in the landed `action_bar` module. -- **L4 re-home surface (L4)** — resolved at L4 build: three-zone layout confirmed against the - post-palette-revision `bank_panel`; `action_bar` extended with `ActionCluster::Tagging` + - `Switching`; new pure `footer_bar` module covers footer layout/hit-test. -- **L5 refinement surface (L5)** — resolved at L5 build: `TrackPopupMenu` overflow menu - confirmed; tooltip mechanism resolved as **custom LICE-kit hover-delay tooltip** (`tooltip` - pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move - and track-tag action ids confirmed; active-mode read confirmed via the same - `view().activeModeId()` the footer toggle uses. -- **L7 model + tempo surface (L7)** — **RESOLVED at build (L7 landed).** Forks F1/F2/F3 all - confirmed: `TimeMap_GetTimeSigAtTime` confirmed at build for the meter stamp; SWELL stock - cursors chosen for drop-result cues (Reorder→IDC_SIZEALL, Move→IDC_HAND, Copy→IDC_UPARROW, - Replace→IDC_SIZEWE); existing pool-privilege guard reused as-is for Alt-replace. Gap navigation - in the grid = skip gaps (arrow keys skip empty slots). See individual entries §L7 above. - -## L1 — shared LICE drawing kit (the foundation) -**Goal:** Stand up the shared LICE-based drawing kit that every Phase L surface (L2 dock -panel, L3 VST editor) consumes — palette/theme module, pure component geometry/hit-test -helpers, LICE draw shell, and retirement of the GDI `DrawText` path in `bank_panel`. -CONTEXT-ARCHIVE.md §Phase L (Kit architecture). DS-1 (LICE + WDL, no external frameworks) and -DS-2 (Direction B Neon Console + Direction C spectral) are the governing settled decisions. -**Verify:** CTest green (`theme_tests`, `component_geometry_tests`). Each text-on-surface -pair in the palette clears its WCAG floor (tested). `bank_panel` text routes through -cached-font `text()` — GDI `DrawText` path retired. Double-buffer discipline preserved. - -- [x] **`theme`/palette module** (`src/theme.{h,cpp}`): role→color mapping via one - constants block (DS-2 revised — REAPER-grey neutral ladder + three-accent pastel - system: `bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a` / `line/hairline - #4a4a4a` / `text/primary #dcdcdc` / `text/dim #a8a8a8`; `accent/primary` pastel lime - `#B0E098` = live/active/selected, `accent/secondary` pastel teal `#84D6D0` + - `accent/tertiary` pastel purple `#C2AAE8` = categorical distinctions); `Role` enum - carries the three accent roles; `roleColor`/`roleColorState` updated (Active/Dragging/ - Focus → primary accent); `spectralColor` is a pastel three-stop sweep anchored on the - three accents (lime → teal → purple); WCAG contrast-floor helpers + tests (text/dim-on-grey - AA body; three pastels on bg/cell + bg/panel at the 3:1 floor); interaction-state color - model. Pure; no LICE types. New CTest target `theme_tests`. -- [x] **`component_geometry` module** (`src/component_geometry.{h,cpp}`): button/slider/ - list-row geometry + hover hit-test. Pure; no LICE or REAPER types. New CTest target - `component_geometry_tests`. -- [x] **`draw_kit` shell** (`src/draw_kit.{h,cpp}`): LICE draw layer — `fillSurface` - (micro-gradient + inner highlight/shadow), `drawButton`/`drawSlider`/`drawListRow`/ - `drawWaveform`, cached-font `text()` over four `LICE_CachedFont`s (kit-owned lifecycle), - full interaction-state model, double-buffer preserved. -- [x] **GDI `DrawText` retirement in `bank_panel`**: all panel text now routes through the - kit's cached-font `text()`; raw GDI `DrawText` path retired (the single biggest - "temple os → modern" lever). - ---- - -## L2 — dock-panel layout redesign -**Goal:** Full layout redesign of the `bank_panel` dock window — task-grouped action bar, -full M11-aware button inventory placed by cluster, entire panel drawn through the L1 kit. -CONTEXT-ARCHIVE.md §Phase L (L2 scope). DS-3 (thorough layout redesign, not a light re-skin) is the -governing settled decision; sequenced after M11 merged. -**Verify:** CTest green (`action_bar_tests`). The panel renders in the settled B+spectral -language through the L1 kit — chrome, buttons, tabs, grid cells, dividers all by palette -role with hover on interactive elements; remaining GDI text retired; single -`KitColor→LICE_pixel` boundary via the kit's `toLice` (exposed in `draw_kit.h`). Prune -button remains footer-set-apart + `warn`-colored; grid stays the centerpiece. - -- [x] **`action_bar` pure module** (`src/action_bar.{h,cpp}`, `tests/test_action_bar.cpp`, - CTest target `action_bar_tests`): task-grouped action-bar layout — clusters (Capture / - Placement / Maintenance), per-button label + keybinding micro sub-rects, whole-trailing- - button overflow, point→index hit-test. Pure; no LICE or REAPER types. Mirror of - `mode_switch`/`bank_grid`/`action_buttons`. -- [x] **`bank_panel` redesigned**: full M11-aware button inventory placed and grouped by - task cluster (Capture: capture item/track, batch items, batch razor, realtime; Placement: - insert + insert-conform; Maintenance: re-capture, cancel-realtime); prune remains footer- - set-apart + `warn`-colored; grid stays the centerpiece. Entire panel draws through the L1 - kit (chrome, buttons, tabs, grid cells, dividers) by palette role with hover on - interactive elements; remaining GDI text retired; single `KitColor→LICE_pixel` boundary - via the kit's `toLice` (now exposed in `draw_kit.h`). - ---- - -## L4 — dock-panel button layout enhancement -**Goal:** Re-home the `bank_panel`'s L2 button inventory around *frequency and intent* — -three-zone structure: top toolbar (capture + placement + maintenance), bottom toolbar (Design -View tagging + switching), footer (narrow mode toggle · Tail button · Prune). A layout -re-home of buttons that fire existing actions; no new actions, no capture/placement behavior -change, no touching the "capture ≠ placement" principle. Independent of L3. -CONTEXT-ARCHIVE.md §Phase L (L4 dock-panel button layout). Product framing: -`CONTEXT-ARCHIVE.md` §L4 dock-panel button layout. -**Verify (in DAW):** the top toolbar fires every capture + placement + maintenance action; the -bottom toolbar tags/untags selected tracks and switches/toggles Arrange/Design/show-both; the -footer shows a narrow `[Arrange|Design]` toggle at the left, a Tail button that cycles tail -on click with button states, and the Prune button set apart at the right in `warn`; every -surface draws through the L1 kit in the DS-2 grey+pastel palette; capture and placement still -never auto-insert (the buttons only fire the existing, unchanged actions). - -- [x] **Top toolbar**: capture cluster (capture item, capture track, batch items, batch razor, - capture RT) + placement cluster (insert, insert-conform) + maintenance cluster (re-capture, - cancel-realtime) moved from the L2 bottom bar to a top toolbar via `action_bar` row layout; - buttons fire existing actions unchanged — no auto-insert. -- [x] **Bottom toolbar**: Design View action family as buttons (tag / untag selected for mode, - activate Arrange, activate Design, toggle active mode, show-both) via `action_bar` - `ActionCluster::Tagging` + `ActionCluster::Switching`; fires existing registered Design View - actions. -- [x] **Footer toggle**: `[Arrange|Design]` segmented toggle shrunk to fit-its-text width and - moved to the footer left of Prune; per-mode count as a compact adjacent label. -- [x] **Footer Tail button**: Tail click-zone converted to a proper kit button (rest/hover/ - pressed states; click still cycles the tail setting). -- [x] **Footer order** (`[Arrange|Design]` · Tail · … · Prune `warn` set apart at the right); - pure footer-strip layout in new `footer_bar` module covered by CTest (`footer_bar_tests`). - -**Notes/decisions:** -- **Maintenance cluster restored to top toolbar** (Daniel's directive during L4 build): the - initial L4 spec described the top toolbar as "capture + placement" only; the landed - implementation includes re-capture and cancel-realtime in a Maintenance cluster on the same - top toolbar. CLAUDE.md updated to reflect the actual layout. -- New pure module `src/footer_bar.{h,cpp}`: footer layout/hit-test (narrow mode toggle + - Tail button + Prune); no LICE or REAPER types. New CTest target `footer_bar_tests`. -- `action_bar` gained `ActionCluster::Tagging` and `ActionCluster::Switching` for the - bottom-toolbar Design View verb groups. - ---- - -## L5 — dock-panel button refinements -**Goal:** Refine the L4 three-zone toolbar so the button faces read cleanly and group -legibly — an overflow menu for the rare capture variants, short faces with full-name -tooltips (no `ReaSampler:` prefix), an opposite-mode tag-button set, removal of the now- -redundant Toggle and Activate-Arrange/Design buttons, and semantic-grouping spacing. -**No new capture/placement behavior**; every button fires an existing registered action -(the "capture ≠ placement" principle is untouched). Ungated by Phase S; sequences after L4. -**Verify (in DAW):** the top bar shows only frequent capture/placement/maintenance buttons + -a right-anchored More (⋯) menu that fires Batch Items / Batch Razor / Capture RT; hover -tooltip shows the full action name with `ReaSampler:` prefix stripped; the bottom bar shows -four Item/Track × Arrange/Design tag buttons with only the opposite-mode pair live (disabled -pair visibly greyed via the kit `Disabled` state) and no Toggle button; cluster groups read -as groups. All buttons fire the same actions their keybindings do. - -- [x] Top-toolbar overflow: Batch Items / Batch Razor / Capture RT pulled off the visible bar - into a right-anchored **More (⋯) menu button** (kit-drawn button + `TrackPopupMenu` popup); - each entry fires its existing command id. Pure layout owns the menu-button rect + hit-test - (`overflow_menu` pure module); the popup + dispatch is shell. -- [x] Short faces + drop `ReaSampler:` prefix on the button *face*; keep the keybinding micro - sub-row. -- [x] **Hover tooltip carrying the full action name (prefix stripped)** via a **custom LICE-kit - hover-delay tooltip** (`tooltip` pure module): sourced from the registered action phrase - (not `kbd_getTextFromCmd`); `ReaSampler:` prefix stripped at draw time; tooltip box width - clamped to the client so it never overhangs a narrow dock. The keybinding sub-row still uses - the live binding from `kbd_getTextFromCmd`. -- [x] Bottom-toolbar four tag buttons: **Item: Arrange / Item: Design / Track: Arrange / - Track: Design**, wired to the existing item-move (`VIEW_MOVE_ITEMS_ARRANGE` / - `VIEW_MOVE_ITEMS_DESIGN`) + track-tag (`VIEW_TAG_ARRANGE` / `VIEW_TAG_DESIGN`) actions. -- [x] **Opposite-mode enablement:** a button is live iff its target mode ≠ the active mode; - otherwise drawn `Disabled` (kit disabled state, `TextDim`) and its click is a no-op. Pure - predicate (`mode_enable` pure module) unit-tested; shell reads `view().activeModeId()` once - per draw and applies. -- [x] **Activate-Arrange / Activate-Design / Toggle buttons removed** from the bottom toolbar - (all three actions stay registered; footer toggle owns mode switching). **Show Both** kept - as a set-apart button on the bottom toolbar. -- [x] Semantic-grouping spacing widened: `clusterGap` 16→24 (`buttonGap` remains 4; 6:1 ratio) - on both toolbars so clusters read as groups. - -**Notes/decisions:** -- **Activate-Arrange / Activate-Design / Toggle FORK resolved (Daniel):** all three removed - from the bottom toolbar. The footer `[Arrange|Design]` toggle is the single mode-switch - affordance; the bottom bar is tagging + Show Both only. -- **Tooltip mechanism resolved as custom LICE-kit hover-delay tooltip** (DS-1 "keep drawing in - the kit"): avoids attaching a SWELL tooltip control to non-child LICE rects. SWELL is the - Win32-emulation layer for macOS/Linux and is not the mechanism used here; on Windows the - path is native, and the chosen implementation is a custom kit-drawn tooltip. -- New pure modules: `src/overflow_menu.{h,cpp}` (menu-button geometry/reserve/hit-test), - `src/mode_enable.{h,cpp}` (opposite-mode enablement predicate), `src/tooltip.{h,cpp}` - (placement + prefix-strip). New CTest targets `overflow_menu_tests`, `mode_enable_tests`, - `tooltip_tests`. - ---- - -## L6 — toolbar polish (in-DAW feedback refinement on L5) -**Goal:** Polish pass on the L5 dock-panel state based on Daniel's in-DAW feedback — -single-row button faces, keybinding surfaced in the hover tooltip, Cancel RT moved into -the overflow menu, and visible top-bar cluster order tidied. No new modules, no new test -targets, no capture/placement behavior change. - -- [x] **Single-row button faces:** keybinding micro sub-row removed from `ActionBarSlot` - (and `bindingHeight`/`minSplitHeight` removed from `ActionBarSpec`); buttons now show - only the short label. Toolbar height 40→28 px. -- [x] **Keybinding in hover tooltip:** tooltip now renders "`phrase — binding`" when the - action is bound, bare phrase when unbound (live binding via `kbd_getTextFromCmd`). -- [x] **Cancel RT moved into overflow menu:** the More (⋯) popup now lists four entries — - Batch Items / Batch Razor / Capture RT / Cancel RT. The visible top bar no longer has a - Cancel RT button. -- [x] **Visible top-bar cluster order:** Capture Item · Capture Track · Re-capture · Insert · - Insert Conform (cluster order: Capture → Maintenance → Placement). - -**Notes/decisions:** -- Icons were considered and deferred (not implemented in this pass). - ---- - -## L7 — capture ordering, card metadata, and selection styling -**Goal:** Three grid-facing improvements to the dock panel, drawn through the L1 kit in the -settled DS-2 palette. (1) **Persisted deterministic capture order + drag-drop reorder + sparse -placement:** each bank (and the pool) carries an explicit, persisted per-sample order via a -per-`Bank` id→slot `SlotMap` in `bank_book`; a card may sit in a slot that leaves earlier slots -empty (gaps preserved; trailing empty tail trimmed for scroll extent). (2) **Decorative metadata -over the peaks:** each card overlays capture length as **bars.beats.subdivisions (bottom-left)** -and **seconds.ms (bottom-right)** in the kit's micro / value-mono type class, `text/dim`. (3) -**Selection restyle:** a selected card drops the inverted accent-fill and instead draws the -*normal* cell + an **`accent/tertiary` (pastel purple `#C2AAE8`) border**. No new capture / -placement behavior; the "capture ≠ placement" principle is untouched. - -- [x] **`SlotMap` in `bank_book`:** gap-preserving per-`Bank` id→slot map — persisted - deterministic display order; interior gaps preserved / trailing tail trimmed; JSON rides - inside the existing `"banks"` blob; pre-L7 migration seeds dense insertion order via - `reconcileSlots()` on the load path. `bank_model` / `Sample` untouched by position (position - is a per-bank display concern). -- [x] **`BankBook` mutators:** `reorderSample` (insert-before-shift; same-slot = no-op), - `replaceSample` (occupant index-removal via the standard remove path + pool-guard inheritance; - no-op on reject), `orderedSampleIds`, `reconcileSlots`. All gap-preserving; deterministic; - CTest-covered. -- [x] **`Sample` meter stamp (ONE sanctioned `Sample` change):** `captureTimeSigNum` / - `captureTimeSigDenom` added to `Sample` + JSON round-trip; stamped on both capture paths and - refreshed on re-capture via `TimeMap_GetTimeSigAtTime` (API confirmed at build). Old samples - with no stamp fall back gracefully (blank musical read-out). -- [x] **`card_drag` pure module:** gesture precedence — leave-client → OS drag-out; - other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace. Cursor-cue map - returned as the pure resolved gesture. Sparse `computeSlotRects` / `hitTestSlot`. SWELL stock - cursors chosen at build: Reorder→`IDC_SIZEALL`, Move→`IDC_HAND`, Copy→`IDC_UPARROW`, - Replace→`IDC_SIZEWE`. New CTest target `card_drag_tests`. -- [x] **`card_meta` pure module:** bars.beats.subdivisions from the stamped tempo + meter; - seconds.milliseconds rounded. Both blank when the sample is unstamped. New CTest target - `card_meta_tests`. -- [x] **`bank_panel` sparse-grid render:** grid renders in sparse slot order; decorative - empty-gap cells drawn for unoccupied slots; every cell↔sample consumer remapped to - id-based occupied-ordinal space (selection, keyboard nav — arrows skip gaps, audition, - multi-select, delete / re-capture resolution, drags). -- [x] **`bank_panel` drop dispatch:** reorder / Alt-replace / move-copy drop with one-Ctrl-Z - undo via the existing batched undo pattern; per-slot drop highlight (`accent/hot`, doubled - outline for replace); SWELL stock cursor cues via `SetCursor` per the pure resolved gesture. -- [x] **`bank_panel` metadata overlay:** bars.beats bottom-left (Micro), s.ms bottom-right - (ValueMono), `TextDim`, decorative / non-interactive. -- [x] **Selection restyle:** normal cell + `accent/tertiary` purple border; inversion removed; - focus ring distinct from the selection border. - -**Notes/decisions:** -- **M9 overlap (awareness note — unchanged intent).** M9 (capture-to-slot-N / insert-slot-N, - MIDI-bindable, MPC-style) remains **explicitly deferred (Daniel, 2026-07-26).** The - interchangeable-slot substrate L7 builds still eases a future M9 revival but L7 adds **no** - slot-numbered capture/insert actions and **no** MIDI bindings. The "plain vs. M9-shaped" - sub-fork is closed: plain gap-preserving substrate (F2). -- **Build-time choices confirmed:** `TimeMap_GetTimeSigAtTime` confirmed at build for the - meter stamp; SWELL stock cursors chosen (no custom cursor load/synthesis required); - gap navigation = skip gaps (arrow keys skip empty slots); same-slot reorder = no-op. -- New CTest targets `card_drag_tests`, `card_meta_tests`. - ---- - -## L3 — VST editor + embed-strip restyle -**Goal:** Bring the ReaSampler 9000 VST editor (`IPlugView` LICE surface, -`reasampler_editor.cpp`) and the S6 embed strip (`reasampler_embed.cpp`) up to the -settled-and-revised **B + three-accent pastel** look via the L1 kit: kit cached-font -`text()` (the kit's current face — no font change), kit component draws, the -**REAPER-grey neutrals** (`bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a`) -with the **three pastel accents** (primary lime / secondary teal / tertiary purple), the -**pastel spectral keyboard strip + zone bars** (active zone lifts to `accent/primary` + -a static glow) as the signature surface, and hover/pressed/drag interaction states -throughout. Merged as `c53683e`. **L3 was the last remaining Phase L point — Phase L -is now complete (L1, L2, L3, L4, L5, L6, L7 all landed).** -**Verify (in DAW):** the VST editor + embed strip render in the settled B + three-accent -pastel language through the L1 kit — kit AA cached-font text, gradient/rounded kit -components, the pastel spectral keyboard strip, working hover/pressed/drag; the VST3 -class UID is unchanged (a visual refresh is not a compat event). - -- [x] Route the VST editor's + embed strip's text through the kit's cached-font - `text()`; retire their raw GDI `DrawTextA` path (retired in both shells). -- [x] Retire the shells' **local pre-L1 palette** — the `kColBackground`/`kColCardBg`/ - `kColThumb`/… forest-green-on-charcoal constants block in `reasampler_editor.cpp` - (and the mirrored constants in `reasampler_embed.cpp`) — and draw every surface - through the L1 `theme` roles. One kit, one look; two palettes collapsed to one. -- [x] Restyle the editor + embed components through the kit (capture-first browser - search/tabs/thumbnails, channel toggles, ADSR + pitch sliders, Varispeed/Preserve mode - toggles, zone bars, list rows, waveform + start/loop markers, segmented controls) in - the B (Neon Console) palette with hover/pressed/drag states throughout. -- [x] Apply **Direction C's pastel spectral treatment** to the keyboard strip + zone bars: - hue-mapped zones as a pastel sweep anchored on the three accents; active zone lifts to - `accent/primary` + a static glow (no animation); waveform + loop/start markers drawn - through `drawWaveform` + `warn`/accent marker roles; VST3 class UID unchanged. - -**Notes/decisions:** -- **Full restyle, not a born-in-kit no-op.** The Phase S surfaces (`reasampler_editor.cpp` - + `reasampler_embed.cpp`) arrived on dev drawing flat `LICE_FillRect` blocks + raw GDI - `DrawTextA`, off a local pre-L1 forest-green palette (`kColBackground` etc.) — the - coordination contract's "born in the kit" branch did not occur. L3 performed the full - restyle and reconciled the two palettes into one. -- **Shell-side only.** All `src/vst/` UI geometry modules (`editor_geometry`, - `keyboard_strip`, `waveform_view`, `capture_browser`, `param_slider`, `browser_scroll`, - `embed_strip`) are pure geometry/hit-test — zero LICE, zero draw. L3 touched only the - two draw shells; no geometry rework was needed. -- **Beta title band — textual-only distinction (Daniel, 2026-07-27).** The S18 beta - channel title band gets no distinct visual accent; L3 restyles it in the standard B - pastel palette and the beta-vs-stable distinction stays purely textual (the - channel-derived plugin name via `app_version`, as before). No channel-specific accent - color. Closes the one open fork from the L3 readiness review. -- **VST3 class UID unchanged.** A visual refresh is not a compat event; RT/`process` - path untouched; pure geometry modules remained pure throughout. - ---- - -# Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact) - -> **Landed on dev (merged 2026-07-27); DAW verification pending Daniel's smoke test.** -> S1–S18 are all on dev. The cross-artifact ingest relay (one S13 bullet) was explicitly -> DEGRADED and remains deferred in `PLAN.md`. Authoritative spec: **CONTEXT.md -> §MIDI-playback instrument — additive phase spec (Phase S)**. Product framing: -> `docs/product/midi-playback.md`. - ---- - -## S3 — pure sampler core (voice engine / envelope / keymap / repitch) -**Goal:** The REAPER-free **and** VST3-free sampler core — voice allocation/polyphony, -amplitude envelope (ADSR), key→sample and velocity→sample mapping (the keymap), -repitch/interpolation from root note, keymap resolution — unit-tested in CTest against -known signals. **The heart of the phase (D3); the mirror of -`bank_model`/`peaks`/`view_mode_model`/`bank_book`; test it hard.** The core is -invariant under the build-shape choice — no VST3 or REAPER type at its boundary. -CONTEXT-ARCHIVE.md §Phase S (pure core, module architecture). -**Verify:** CTest green. Voice allocation is correct under polyphony (note-on/off, -voice stealing where bounded); ADSR shape asserted against a known signal (mirror of -`peaks`); repitch from root note produces the expected pitch ratio; keymap resolution -maps a (note, velocity) to the correct sample/zone; the core takes and returns only -plain data (no VST3/REAPER types) — enforced by the test target linking neither SDK. -**Depends on:** S2 (consumes `rootNote` / loop points as core inputs). - -- [x] Voice engine: polyphonic voice allocation (note-on/off, bounded voice stealing), - per-voice state, mono-and-basic-polyphony sufficient for Tier 0. -- [x] Amplitude envelope (ADSR) math — asserted against a known signal. -- [x] Repitch/interpolation from root note (chromatic pitch ratio across the - keyboard); loop-point-aware sustain for held notes. -- [x] Keymap model + resolution: key ranges/zones (Tier-1 shape) and the - (note, velocity) → sample/zone query; Tier-0 chromatic-from-single-root as the - degenerate case. -- [x] Tests: voice allocation under polyphony + stealing; ADSR envelope shape; - repitch pitch-ratio correctness; keymap resolution (single-root chromatic + zoned); - core boundary is plain-data-only (no VST3/REAPER types). - ---- - -## S4 — Tier 0: "the bank plays" (single sample, chromatic) -**Goal:** The honest MVP — one bank sample mapped chromatically across the keyboard -from its root note, basic polyphony, a simple amp envelope, velocity→volume. Wire the -S3 core into the S1 VST3 shell over the live-state seam (bridge-read bank + audio via -the M4 project-relative path machinery). Editor deferrable behind a parameters-only -default view. CONTEXT.md §Phase S (Tier 0, seams). **Delivers the core promise.** -**Verify (in DAW):** on an instrument track, the VST3 plays a chosen bank sample -MIDI-triggered, repitched chromatically from its root note, with basic polyphony, -an amp envelope, and velocity→volume; it reads the live `"reasampler"` bank via the -bridge and resolves the WAV audio the same project-relative way `persist` does; -following the active project works; it never captures and never inserts into the -arrange (read-only over the bank). -**Depends on:** S1, S2, S3. - -- [x] VST3 `process` marshalling: read MIDI note-on/off/velocity off the event bus, - drive the S3 core, write per-voice audio to the output bus. (Block-granular event - timing at Tier 0; sample-accurate offset scheduling is a later tier.) -- [x] Live-state seam: read the bank index + selected sample's root note from - `"reasampler"` ext-state via the bridge; resolve the WAV audio path the M4 - project-relative way (shared convention with `persist`, not re-implemented — the - parent-of-.rpp derivation is extracted to `capture_paths::projectDirOfRpp`, which both - `persist` and the bridge call). Bank JSON parsed via the shared `bank_book` path (the - spike string-scan reader retired); ext-state key names shared via pure `ext_keys.h`. -- [x] Sample selection UI (minimal, in the `IPlugView` LICE editor): a clickable list - of the bank's samples; the pick is the instance's own VST3 component state - (setState/getState), never written back to the bank. -- [x] Tier-0 playback: chromatic-from-root, basic polyphony (16 voices), amp envelope, - velocity→volume — plays in REAPER's routing/record/render path like any VSTi. Sample - load / decode / keymap build happen off the audio thread and hand to `process` via a - lock-free atomic pointer swap (graveyard-reclaim); `process` never allocates. - ---- - -## S5 — Tier 1: "a keymap" (zoned multisamples, per-sample root notes) -**Goal:** Multiple bank samples zoned across the keyboard (key ranges), each with its -own root note — a captured *kit* (one-shots) or a *multisampled instrument* (same -instrument sampled at several pitches) plays correctly. One sample per key-region. -CONTEXT.md §Phase S (Tier 1). **Where the root-note + key-range seam fields earn -their place.** -**Verify (in DAW):** a keymap of several bank samples plays correctly zoned across -the keyboard, each repitched from its own root note within its range; a captured kit -and a multisampled instrument both play as expected; the keymap is authored in the -instrument (performance map) while root notes come from the bank intrinsics (S2); -editing the keymap does not touch the bank. -**Depends on:** S4. - -- [x] Keymap editor in the `IPlugView` LICE editor: assign bank samples to key ranges - (low/high note per sample), each with its own root note (from S2 intrinsics, - overridable in the performance map). -- [x] Tier-1 playback: zoned resolution — a note picks its zone's sample and repitches - from that sample's root note; one sample per key-region. -- [x] Performance-map persistence: the keymap (zones, per-sample assignment) is the - instrument's own state — held in the instrument as VST3 component state (setState/getState) - per D-B's data-ownership split; the live `"reasampler"` seam is read-only (bank + - intrinsics in, nothing written back), never written back as a bank intrinsic. - ---- - -## S6 — embedded TCP/MCP UI (D-D — scheduled in-phase, after the editor) -**Goal:** Render a compact keymap/level strip **inline in the track/mixer control -panel** via `reaper_plugin_fx_embed.h` (`IReaperUIEmbedInterface`) — the same -Cockos surface REAPER's own embedded FX use — so the instrument draws inline, not only -in its own window. Composes with the S1/S5 LICE editor path (same LICE-class drawing). -**Scheduled, not deferred (D-D settled 2026-07-26):** a real later point, sequenced -last because it is polish over a Tier-0 need — but on the roadmap. CONTEXT.md §Phase S -(embedded UI, D-D). -**Verify (in DAW):** the instrument draws a compact inline strip in the TCP/MCP (not -only its own editor window); the inline surface reflects and (where offered) edits the -keymap/levels; the embed lifecycle is clean (open/close/resize); the same LICE drawing -as the main editor is reused. -**Depends on:** S5 (composes over the existing LICE editor). **Must-verify before -build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle against -`vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`. - -- [x] Implement `IReaperUIEmbedInterface` on the VST3; draw a compact keymap/level - strip inline in the TCP/MCP using the same LICE surface as the editor. -- [x] Embed lifecycle (open/close/resize/hit-test inline) handled cleanly; reflects - the live keymap/levels. - ---- - -## S7 — stereo channel mode (mono | stereo; core channel dimension + bus negotiation) -**Goal:** Give the instrument a per-instance **channel-mode toggle — 1 (mono) or 2 -(stereo)** — that "works with the REAPER audio bus automatically." Mono keeps today's -downmix path; stereo grows the S3 core a **channel dimension** (2-channel sample data, -per-voice stereo render, stereo interp/loop) and negotiates the VST3 output bus so -mono/stereo just works in REAPER's routing. **This is an S3-core extension, not a shell -hack** — it touches the engine Daniel smoke-tests, so it sequences first after the -editor/embed work. CONTEXT.md §Phase S (channel mode, D-E). **Decided direction -(2026-07-26); leans below are build-time residuals, not open forks.** -**Verify (in DAW):** an instance set to stereo plays a stereo capture in true stereo, -its VST3 output bus negotiated to 2 channels via `setBusArrangements` so REAPER routes it -without manual channel wiring; an instance set to mono plays the existing downmix path; a -mono source in stereo mode plays dual-mono (centered); a stereo source in mono mode -downmixes (existing policy); the mode is per-instance state that survives project -save/reopen (component state, like the selected sample); the pure core's stereo render is -asserted against a known two-channel signal (mirror of `peaks`), and mono behavior is -unchanged (regression). -**Depends on:** S3 (extends the core), S4 (extends the process/bus shell). Independent of -S8/S9. - -- [x] Core channel dimension (pure, S3 extension): `SampleData` carries 1- or 2-channel - decoded PCM (`frames` + optional length-matched `framesR`; `channelCount()`); - `Voice::renderFrameStereo` + a `VoiceEngine::render(left,right,n)` overload produce a - per-channel frame sharing one read head + one envelope tick; stereo linear interpolation + - loop read per channel. Mono stays the degenerate case (`renderFrame` reads channel 0 only, - byte-identical). Tests: stereo render asserted against a known 2-channel signal; dual-mono; - per-channel repitch + additive mix; mono render unchanged (regression) — sampler_core_tests. -- [x] Channel-mode toggle as per-instance state: `ChannelMode {Mono,Stereo}` in the - instrument's own component state (v4 = v3 + a channel-mode byte; setState/getState); - default mono. Cross-mode policy in `decodeChannels`: **mono source + stereo mode → - dual-mono**; **stereo source + mono mode → downmix** (existing decode-side policy). The - toggle lives in the instrument, never written to the bank (D-B). v1/v2/v3 blobs lift to v4 - with mono default; round-trip + lift tests — sample_map_tests. -- [x] Shell: `decodeRelative` fills 1- or 2-channel `DecodedZonePcm` per the active mode - (source channel count from the WAV layout); the process path renders the host's negotiated - output channel count (stereo into ch0/ch1, mono into ch0) — RT discipline unchanged. -- [x] VST3 bus negotiation: `setBusArrangements` accepts only the mode's arrangement - (kMono/kStereo), else rejects (kResultFalse) but keeps a valid mode arrangement so - `getBusArrangement` (base default) reports it; a runtime mode change repoints the output bus - + calls `restartComponent(kIoChanged)` so REAPER re-negotiates. **Verified** against the - vendored Steinberg SDK (`ivstaudioprocessor.h` contract, `vstsinglecomponenteffect.cpp` - base impl, `ivsteditcontroller.h` kIoChanged); see handoff notes. - ---- - -## S8 — ingest through the bank (one gesture: capture/import into bank + assign to instance) -**Goal:** Loading a sample into the sampler is **one gesture** — capture/import-into-bank -**and** auto-assign to the active sampler instance. **The extension owns ingest** (it has -arrange access, media-explorer access, and drop-target surface on its own panels); the -instrument stays a **read-only bank consumer**. This lives in the *extension* codebase -(actions + bank_panel + capture/insert), routing through the existing capture add-path and -the live `"reasampler"` seam the instrument already reads. CONTEXT.md §Phase S -(ingest-through-bank contract). **Decided direction "option 1" (2026-07-26).** -**Verify (in DAW):** a one-click "capture selected item / time-selection into the bank and -assign to the active instance" action captures via the existing capture path (never -auto-inserting into the arrange — load-bearing principle intact) and the target instance -plays the new sample on its next reload; a Media Explorer file imports into the bank and -assigns the same way; a file dropped onto a ReaSampler panel surface ingests into the bank -and assigns; the instrument never captures or imports (read-only over the bank throughout). -**Depends on:** S4 (an instance to assign to), M7 capture add-path, B2 (active-bank add -target). Best paired with S9 so assignment refreshes hands-free; functional without it -(assign triggers a reload on the target instance directly). - -- [ ] "Capture selected item / time-selection into bank + assign to active instance" - action (`command_id`/`gaccel`/`hookcommand`, MIDI-bindable): reuse the existing capture - request path (`CountSelectedMediaItems`/`GetSelectedMediaItem` + `GetSet_LoopTimeRange` - as the capture inputs), add the resulting `Sample` to the active bank, then assign its - id to the target instance. **Never inserts a timeline item** (capture/placement stay - separate — the assignment is a bank-index + instance-selection act, not a placement). -- [ ] Media Explorer import → bank → assign: read the Media Explorer's current selection - via `MediaExplorerGetLastPlayedFileInfo` (path + selection range), import the file into - the bank (existing import/capture add-path), assign to the target instance. **Honest SDK - limit (verified against the vendored headers):** the Media-Explorer surface is thin — - `OpenMediaExplorer` (open/select) + `MediaExplorerGetLastPlayedFileInfo` (read the *one* - last-played/selected file + its range) are the whole contract; there is **no** - enumerate-selected-files and **no** register-a-drop-handler-on-the-Media-Explorer API. - So ME import is *single-file, pull-on-action* (an action the user fires while a file is - selected in the ME), not a push/drop from inside the Media Explorer. **Spike:** confirm - `MediaExplorerGetLastPlayedFileInfo` returns a usable path+range for a merely-*selected* - (not-yet-played) file, or whether a play is required first. -- [ ] Drag-and-drop onto ReaSampler surfaces: accept an OS file drop onto the docked - `bank_panel` (and its bank/tab regions) → ingest into the bank → assign. **Honest SDK - limit (verified):** REAPER exposes **no** drag-drop registration API; drop handling is on - ReaSampler's *own* HWNDs via SWELL/Win32 (`WM_DROPFILES` / an `IDropTarget` on the panel - HWND), the same surface the panel already owns. **Assess-and-flag (spike, do not promise - here):** a drop *onto the VST3 editor window* — whether the `IPlugView` HWND can accept an - OS file drop and relay it to the extension as a bank-ingest request (the instrument does - **not** ingest; it forwards a request to the extension over an agreed seam). Reported - honestly as a spike because it crosses the two-artifact boundary and the relay mechanism - is unproven; if it proves gnarly, drop-onto-panel is the shipped path and drop-onto-editor - is deferred. -- [ ] "Assign to instance" seam: how the ingest action names the target instance and hands - it the new sample id. Lean (build-time residual, not a fork): the active/last-focused - instance is the target, discovered via the host context the bridge already resolves; the - assignment is the same instance-owned selection state S4 already persists, so a reload - picks it up. If the change-detection seam (S9) exists, assignment refreshes hands-free; - without it, the ingest action pokes the target instance's reload directly. - ---- - -## S9 — bank-generation change-detection (recapture / ingest refreshes instances hands-free) -**Goal:** Because instances reference sample **ids**, a **recapture** (M10) landing under -the same id — or an **ingest** (S8) touching the active bank — should refresh playing -instances **hands-free**, without the user re-opening each editor. Add a **bank-generation -counter** to `"reasampler"` ext-state that the extension bumps on any bank-content -mutation, and that the instrument polls off the audio thread on a safe cadence, calling its -existing `reloadFromBank()` when the generation changes. CONTEXT.md §Phase S -(bank-generation seam). **Closes the missing change-detection trigger the recapture -auto-update story needs.** -**Verify (in DAW):** a recapture that regenerates a sample already assigned to a live -instance refreshes that instance's playback within a bounded cadence, no editor re-open; an -ingest (S8) that updates the active bank likewise refreshes assigned instances; the poll -runs off the audio thread (never in `process`) and triggers the existing off-thread reload -path; instances not referencing a changed sample do not audibly glitch (reload is atomic — -the S4 graveyard-reclaim handoff); a project with no generation stamp (pre-S9) defaults -cleanly (treated as generation 0; first bump refreshes). -**Depends on:** S4 (the off-thread `reloadFromBank` + atomic handoff this drives). Writer -side is extension-only and independent of S8; consumed by S8 and M10 recapture. Best landed -alongside S8. - -- [x] Writer (extension): a monotonic **bank-generation counter** stamped into - `"reasampler"` ext-state (new `ext_keys.h` constant — forever-stable spelling), bumped - on every bank-content mutation that changes what an instance would play (capture add, - recapture-in-place, sample-remove, move/copy affecting the active bank). Additive to the - persist blob; defaults to 0 for projects saved before the stamp exists. -- [x] Reader (instrument): poll the generation over the bridge on a safe **off-audio-thread - cadence** (a UI/timer tick, not `process`), compare to the last-seen value, and call the - existing `reloadFromBank()` on change — reusing S4's atomic pointer-swap handoff so a - refresh mid-play does not glitch. No new audio-thread work; no allocation in `process`. -- [x] Cadence + coalescing: pick a poll interval that is responsive but cheap (build-time - residual — a low-frequency UI timer, coalescing multiple bumps between polls into one - reload). **Must-verify before build:** that a bridge ext-state read on the instrument's - UI/timer thread is safe against a concurrent extension write (the read already tolerates a - stale value by design — it reloads on the *next* poll; confirm no torn-read hazard for the - single integer generation key). - ---- - -## S10 — capture-first editor: browser + guided single-capture setup ("ReaSampler 9000" UX overhaul, part 1) -**Goal (REVISED 2026-07-26 — workflow-first reframe, Daniel):** Rebuild the editor's -default face around the **primary flow = one capture, fast**, not a keymap. A giant list of -"item" blocks is visually useless; most instances play a *single capture*, and zones are a -nice-to-have. So the default view is a **capture browser** (scannable cards with peak -thumbnails, name, root/key badge; **bank filter**) feeding a **guided single-capture setup** -(root note, play-mode basics, level) — and the keyboard strip serves the *single-capture* -case first (shows where the capture sits / its root). **Time-to-first-note is the metric.** -Multi-zone keymap editing is **demoted to an opt-in "Zones" panel** (S10-Z below), not the -default. The keyboard-strip drag machinery is still built here, but in service of the -capture-first layout. All layout/hit-test math is **pure geometry** (new `keyboard_strip` + -a `capture_browser` layout module — mirrors of `mode_switch`/`editor_geometry`); the LICE -draw + drag-state machine is the editor shell. RT discipline untouched (edits commit -off-thread via `commitAndReload`); the instrument stays a **read-only bank consumer**. -CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX — capture-first editor). - -**Policy reversal — fresh instance is SILENT, nothing auto-selected (was S4).** The S4 -"first sample plays" fallback is **removed**: on open with no stored selection, the -instrument plays **nothing** and the editor shows a clear **empty state** ("pick a capture") -— it does not auto-play sample #1. Retires the `selectSample` first-sample fallback -(`sample_map.cpp` "No stored id → fall back to the FIRST sample") and the processor's -Tier-0 fallback that resolved it; an empty stored id now resolves to silence. A capture is -loaded when the user picks one (or via S13 drop-to-load / S8 ingest). This is a deliberate -reversal of the S4 convenience default, not a regression. - -**Verify (in DAW):** a fresh instance plays **nothing** and shows the "pick a capture" -empty state (no auto-play of sample #1); the capture browser draws **peak thumbnails** -(the `Sample` peaks bank_model already carries — same data the dock panel thumbnails use), -name, and a root/key badge where present, and is **filterable by bank** (bank_book named -banks); picking a capture loads it, shows it (waveform/peaks + its root on the keyboard -strip), and it plays repitched from its root; time-to-first-note is a pick-then-play, not a -list-scroll; the keyboard strip shows the single capture's root and is draggable to set it; -the pure geometry modules are CTest-green (browser card/grid layout + hit-test; strip -edge-grab/body-move/key→note) with no host types at their boundary; the ±1 nudge-button row -is gone. -**Depends on:** S4 (the selection state + reload path this reverses the fallback on), S1 -(the LICE `IPlugView` drag/event routing — extends the click-only `wndProc` to -`WM_MOUSEMOVE`/`WM_LBUTTONUP`), S5 (the `PerformanceMap`/zone model the opt-in Zones panel -edits — but the default face does not require a keymap). **Adopts the Phase L kit when -available — not gated on Phase L.** S10 builds its browser cards + keyboard strip with the -current LICE drawing; when Phase L's L1 kit lands on `dev`, this surface adopts it (the one -source of drawing). The drag machine's `WM_MOUSEMOVE` tracking also lights the kit's -**hover** states at near-zero marginal cost once the kit is present. - -- [ ] No-auto-select + empty state (the policy reversal): remove the `selectSample` - first-sample fallback (`sample_map.cpp`) and the processor's Tier-0 fallback that - consumed it — an empty stored selection resolves to **silence**, not sample #1. The - editor draws a clear **empty state** ("pick a capture" affordance) when nothing is - selected. Pure change is testable (empty id → `nullopt`); the empty-state draw is shell. -- [ ] Capture browser (pure layout + shell draw): grow `SampleChoice` to carry the - **peak thumbnail data** (from the `Sample` peaks bank_model already stores — the same - peaks the dock panel draws), the **root/key badge** (S2 `rootNote` intrinsic / the - optional musical key), and its bank. A new pure `capture_browser` module lays out - scannable **cards/rows** (card rect grid, thumbnail rect, hit-test a point → card) — no - host types at the boundary, unit-tested. The shell draws each card's peak thumbnail + - name + badge in LICE (house palette) and routes a click to select. -- [ ] Bank filter (pure + shell): a filter/tab strip over the browser that narrows the - drawn cards to a chosen bank_book bank (or "all"). Filter-tab layout + hit-test pure - (mirror of `mode_switch`); the active-filter state is transient UI state; the shell draws - the tabs and applies the filter to the card list. (Type-to-filter search folds in from - S12 — see S12's boundary note; a name-substring filter over the same card list.) -- [ ] Guided single-capture setup (the fast path): once a capture is picked, a prominent, - self-explanatory setup surface — **root note** (settable on the keyboard strip / typed), - **play-mode basics**, **level** — sized for the single-capture case, not a zone table. - Graphic and descriptive; the point is to get from pick → set → play with no hunting. -- [ ] Pure `keyboard_strip` geometry module (serves the single-capture case first): map a - MIDI key span across a strip width (128 keys → pixels, reusing the S6 `embed_strip` - key-span idiom); a **root marker** for the loaded capture; `pixel→note` and a `keyAtPoint` - for click-to-set-root; a drag-delta resolver `(grabbedField, startNote, dxPixels) → - newNote`; **per-zone bar rect** + **edge-grab hit regions** (resize handles vs. body - move-handle) for the opt-in Zones panel. No VST3/REAPER/LICE types at the boundary; - unit-tested (root marker, edge grabs, body-move delta, key mapping, clamps low≤high, - boundary rounding). Mirror of `mode_switch`/`editor_geometry`. -- [ ] Editor shell drag-state machine: `WM_LBUTTONDOWN` grabs a card / a key / a zone - edge-or-body, `WM_MOUSEMOVE` updates the in-flight edit against the pure resolver, - `WM_LBUTTONUP` commits via the existing `commitAndReload` (off-thread reload; RT path - untouched). Live visual feedback while dragging; a single undo-coherent edit on release. - -### S10-Z — Zones panel (opt-in multi-zone keymap editing; demoted from the default face) -The multi-zone keymap editor is now an **opt-in view/panel** ("Zones" toggle), not the -default. It reuses the same `keyboard_strip` geometry and drag-state machine: each zone a -bar over the keys it covers; **drag an edge** → low/high note; **drag the bar body** → move -the zone (span preserved); **click a key** → set/relocate the zone's root. This is the -capability RS5K structurally lacks (multi-zone in one instrument), kept as a *nice-to-have* -per Daniel's hierarchy — "most of the time the zones won't be used." Add/select/delete a -zone; overlapping zones render legibly and resolve first-match. The seven ±1 nudge/delete -mini-buttons are retired everywhere; delete is one affordance (a small × on the bar or a -keystroke). The `zoneHitTest`/±1 nudge path in `editor_geometry` is retired (a numeric -fallback for accessibility is a build-time residual, not a fork). -**Verify (in DAW):** the Zones panel is reachable via an explicit toggle (default view is -the capture browser + single-capture setup, not this); a zone's range is set by **dragging -edges** (not nudge clicks); body-drag moves the span; click-a-key sets the root (audible on -the next held note); zone add/select/delete work; the ±1 nudge row is gone. - -- [ ] "Zones" panel toggle (opt-in): the default editor face is the capture browser + - single-capture setup; a toggle reveals the multi-zone keymap editor. Toggle state is - transient UI state (or per-instance component state if it should persist — build-time - residual). -- [ ] Zone edit via the shared strip: draw the keyboard strip + zone bars in LICE, drive - the shared drag-state machine (edge = resize, body = move, key = root), commit via - `commitAndReload`. Zone add/select/delete as single affordances; ±1 nudge row gone. - ---- - -## S11 — waveform view with draggable loop points (UX overhaul, part 2) -**Goal:** Give each sample/zone a **waveform display** with **draggable start/end/loop -markers** — the S2 loop-point intrinsics and the S5 performance map already carry the data; -today there is no way to *see* a sample or *set* its loop by eye. Selecting a zone (or a -bank sample) shows its waveform (peaks via the existing `peaks` module, fed the decoded -PCM the shell already loads); drag the **loop-start / loop-end** markers to set the sustain -loop, snapping to zero-crossings (the S2 spec's zero-crossing-aware requirement). Loop -points are a **performance-map override on the zone** where set, seeded from the bank -intrinsic (D-B split: the bank carries the file-fact default; the instrument's drag is the -performance choice). All marker/waveform layout + hit-test is pure geometry; peaks compute -reuses `peaks`; the draw + drag is the shell. CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX — -waveform view). -**Verify (in DAW):** selecting a zone shows its sample's waveform; dragging the loop-start -and loop-end markers sets the sustain loop and a held note audibly loops that region; -markers snap to the nearest zero-crossing; a sample with no loop shows the "no loop" state -and a held note past the end goes silent (existing core behavior); the waveform peaks match -the audio (mirror of the `peaks` envelope assertion); the marker geometry module is -CTest-green (px↔frame mapping, marker grab regions, clamp start≤end). -**Depends on:** S2 (loop-point intrinsics), S3 (loop-aware sustain the markers drive), S5 -(the zone the loop attaches to), S10 (shares the editor's drag-state machine + shell). The -zero-crossing snap is a small pure helper over the decoded PCM. - -> **Boundary note (S10 reframe, 2026-07-26):** the waveform view is now **central to the -> single-capture fast path**, not just per-zone. Selecting a capture in S10's browser shows -> its waveform (this is "see it" in pick → see it → play it); the loop-marker drag here -> extends that same waveform surface. S11's waveform draw is the same one S10's picked- -> capture view uses — build it once, S10 shows it read-only for the single capture, S11 adds -> the draggable loop markers. No renumber; S11 stays the loop-editing point. - -- [ ] Pure waveform/marker geometry: `frame↔pixel` mapping across the waveform rect, marker - x-position from a frame index, marker grab regions (start/end/loop-start/loop-end), - drag-delta `(grabbedMarker, dxPixels) → newFrame` with clamps (start≤end, in-bounds). A - **zero-crossing snap** helper: nearest sign-change frame to a target (pure, over the - decoded mono PCM). No host types; unit-tested. -- [ ] Waveform draw: compute peaks with the existing `peaks` module from the shell's already- - decoded PCM (no new decode path, no new WAV reader); draw the envelope in LICE in the - house style; draw the loop markers over it. Reuses the S10 drag-state machine. -- [ ] Loop-point edit → performance-map override: a dragged loop writes a per-zone loop - override (seeded from the S2 bank intrinsic, D-B), committed off-thread via - `commitAndReload`; the bank intrinsic is never written back (instrument is a read-only - bank consumer). Extends `PerformanceZone` with an optional loop override (additive, same - shape as `rootOverride`) + its component-state (de)serialize (version bump, back-compat - with S5's v2 map blob — a truncated/older blob defaults the override absent). - ---- - -## S12 — editor scale + ergonomics (UX overhaul, part 3; scrollable/searchable list, direct entry) -**Goal:** Make the editor usable **at bank scale** and close the remaining RS5K-parity -gaps: the sample list **scrolls** (today a long bank's rows run off the panel with no way -to reach them) and has a **type-to-filter search**; add **direct numeric entry** for a -zone's low/high/root (a click-to-type field over the strip, for precision the drag can't -hit) and an **ADSR control** for the amp envelope (S3 already has the ADSR math; today it -is fixed — expose attack/decay/sustain/release as draggable sliders, per-instance state). -This is the "sensible list handling + direct manipulation of the parameters that exist" -tier. All slider/scroll/search-box layout + hit-test is pure geometry; the shell draws + -routes; ADSR/scroll/filter state is instrument-owned (component state / transient UI -state). CONTEXT-ARCHIVE.md §Phase S (ReaSampler 9000 UX — scale + ergonomics). -**Verify (in DAW):** a bank with more samples than fit **scrolls** (wheel + drag) and every -sample is reachable; typing filters the list to matching names; a zone's low/high/root can -be **typed** (not only dragged) via a click-to-edit field; the amp envelope's ADSR is -**adjustable** (four draggable controls) and the change is audible + persists across project -save/reopen (component state); the scroll/search/slider geometry is CTest-green. -**Depends on:** S10 (the editor shell + drag-state machine + the **capture browser** the -scroll/search now apply to), S3 (the `AdsrParams` the ADSR sliders drive — already wired -into the voice engine; today they are fixed defaults), S5 (the map the numeric fields edit). - -> **Boundary note (S10 reframe, 2026-07-26):** the "sample list" S12 originally scrolled and -> searched **is now S10's capture browser** (cards with peak thumbnails, bank filter). What -> pulled INTO S10: the browser layout itself, the peak thumbnails, and the **bank filter** -> (a bank_book tab, distinct from name search). What stays in S12 and applies **to S10's -> browser**: (a) **scroll** for a bank longer than the panel, and (b) **type-to-filter -> search** (a name-substring narrow over the same cards, composing with S10's bank filter — -> bank filter picks the bank, search narrows within it). The scroll/search geometry is pure, -> layered over the `capture_browser` module S10 builds. Net: S12 = scroll + search over the -> S10 browser + numeric entry + ADSR; the browser *card* work is S10's. **S12 now also -> carries the S15/S16 control surfaces** (per-zone Gate/Trigger mode toggle, AHDSR hold -> control, Trigger %-length/fade controls, Varispeed/Preserve engine toggle, and the AD pitch -> envelope depth/shape controls) — deferred here from S15 and S16 per spec. - -- [x] Scrollable, searchable capture browser: a scroll offset (wheel + scrollbar drag) so a - bank longer than the panel is fully reachable; a **type-to-filter search** that narrows - the drawn cards to matching display names, **composing with S10's bank filter** (bank - filter selects the bank; search narrows within it). Scroll/search layout + hit-test is - pure geometry (visible-card window, scrollbar thumb rect, search-box rect), layered over - S10's `capture_browser` module; filter/scroll state is transient UI state. **Landed:** - pure `browser_scroll` module (`browser_scroll_tests`) — scrollContentHeight / max / clamp, - visibleCardRange window, scrolledCardCellRect, scrollThumbRect + thumbDragToOffset inverse, - searchBoxRect, nameMatchesQuery + filterNameIndices. Editor shell wires wheel (`WM_MOUSEWHEEL`), - thumb-drag (`DragKind::kScrollThumb`), and the search box (`WM_CHAR` -> `onSearchChar`, - composed into `rebuildVisible`). Scroll/search are transient (never persisted). -- [x] Direct numeric entry for zone low/high/root: a click-to-edit field over the strip - (LICE text-entry idiom) so a precise note can be typed, not only dragged. Commits via - `commitAndReload` like every other edit. **Landed:** pure `note_entry` module - (`note_entry_tests`) — `parseNoteEntry` accepts a decimal integer OR a note name (C4==60), - clamps to [0,127], rejects garbage. Editor shell hosts three focusable fields (low/high/root) - on the Zones legend, committing on Enter through `commitAndReload`. -- [x] ADSR/AHDSR editor + S15/S16 control surfaces: draggable sliders over the S3 `AdsrParams` - (attack/**hold**/decay/sustain/release — hold is the S15 addition) plus the deferred S15/S16 - controls — per-zone Gate|Trigger mode toggle, Trigger %-length/fade-in/fade-out, Varispeed| - Preserve engine toggle, and the AD pitch-envelope enable/attack/decay/±semitone depth. All - edit the SELECTED zone's `ZonePlayParams` (instrument-owned, D-B; never the bank), round-trip - through the existing v3 component-state blob (no new persistence — the S15/S16 payload already - landed in the core pass), and commit off-thread via `commitAndReload`. **Landed:** pure - `param_slider` module (`param_slider_tests`) — control-panel stack layout, toggle-segment - split + hit-test, slider value<->pixel round-trip + clamping, point->control routing. The - shell owns the control-id -> engine-param binding + the value DOMAIN mapping (frames/fraction/ - semitones); the module stays engine-free. - -**Notes/decisions:** -- **Wall-clock envelope times stored as rate-free SECONDS (zones payload v5)**, resolved to - frames at keymap build against the live project rate — no hardcoded sample rates anywhere in - `src/`. Daniel's standing ruling; enforced throughout the voice engine and verified at S12. - ---- - -## S13 — drop-to-load (partial landing; relay deferred) -**Goal:** Make "load a sample into the sampler" **one gesture from the editor** via an -OS file drop onto the editor window. The cross-artifact relay was a spike — the -instrument's REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; the relay is -DEGRADED and deferred. The two landed items are the drop-accept surface and the UX degrade -path. The deferred relay item remains in `PLAN.md`. CONTEXT-ARCHIVE.md §Phase S (drop-to-load). - -**Spike verdict (ps-w12, 2026-07-27): DEGRADED.** The relay would require (a) a new -instrument WRITE seam into ext-state and (b) an extension-side timer poller + claim/clear -nonce — the same cross-process handshake race the S17 spec rejected. The shipped ingest -gesture stays drop-onto-docked-panel (S8). The relay is a future wave when the design is -ready. - -- [x] Editor-window drop target: accept `WM_DROPFILES`/`IDropTarget` on the editor child - HWND (the same SWELL/Win32 surface `bank_panel` owns), extracting the dropped file - path(s). Windows-only (D5). This is the *acceptance* half; the ingest is the extension's. - **Landed:** the editor child window calls `DragAcceptFiles(TRUE)` on attach and handles - `WM_DROPFILES` (`reasampler_editor.cpp`). Windows-only (D5). -- [ ] Cross-artifact ingest relay (the S8-flagged spike): **DEFERRED** — relay mechanism - proved load-bearing to redesign. Remains in `PLAN.md` §S13. -- [x] UX degrade path: when the relay is unavailable/unproven, the editor shows a clear - "drop files on the ReaSampler panel to add" affordance rather than silently swallowing the - drop — the shipped ingest gesture stays discoverable either way. **Landed:** the editor - ACCEPTS the drop and flashes a transient banner ("drop files onto the ReaSampler bank panel - to add them") that decays over a few sync ticks, plus a persistent affordance line in the - empty state ("drop a file onto the ReaSampler bank panel"). No file is ingested; NO timeline - item is ever inserted (the hard invariant — the editor only displays guidance). - ---- - -## S15 — sampling modes: Trigger vs Gate (per-sample play-mode; core + editor) -**Goal:** Give each played sample a **play mode** — **Gate** (classic held note) or -**Trigger** (one-shot) — a per-sample/per-zone performance choice (D-B, instrument-owned). -**Gate** is today's behavior grown from ADSR to **AHDSR** (adds a Hold stage): note-on → -attack/hold/decay/sustain, note-off → release, sustain **loop points apply** (S11's -draggable loop UI is Gate-mode UI). **Trigger** is a one-shot drum-pad: note-on fires -playback of a defined **% of sample length** with a **fade-in** and **fade-out** ramp, -**ignores note-off**, and uses **no sustain loop**. **Both** modes carry a **modifiable -start point** (playback begins at an offset into the sample, not always frame 0). This is -an **S3-core extension** (the engine Daniel smoke-tests) plus editor surfacing — the mode + -its parameters are instrument performance-map state, never a bank fact. CONTEXT.md §Phase S -(Sampling modes — Trigger vs Gate). **Daniel's feature set is settled.** -**Verify (in DAW):** a sample in **Gate** mode plays held with the AHDSR envelope (hold -stage audible between attack and decay), releases on note-off, and loops its sustain region -if loop points are set; a sample in **Trigger** mode fires a fixed % of its length on -note-on with audible fade-in/out, **plays through to completion regardless of note-off**, -and never sustain-loops; the **start point** offsets playback in both modes (a note starts -partway into the sample); the mode + parameters are per-instance component state that -survive save/reopen; the pure core's Trigger envelope (fade-in → hold → fade-out over -%-length frames) and the AHDSR hold stage are asserted against known signals; existing -Gate/ADSR behavior is unchanged when hold=0 (regression). -**Depends on:** S3 (extends the envelope + voice read-position machinery), S5 (the -`PerformanceZone` the mode + params attach to), S11 (Gate loop-point UI; Trigger's waveform -shows start + %-length + fades on the same waveform surface). Independent of S7. - -- [x] Core: `PlayMode { Gate, Trigger }` on the voice + the envelope split. **Gate**: `AdsrParams` - gains `holdFrames` in place (AHDSR hold stage between attack and decay; `holdFrames == 0` is - the exact pre-S15 ADSR — back-compat; no type rename). **Trigger** is a distinct envelope: play `[start, - start + lengthFraction·(frames−start))` with a **fade-in** ramp (0→1 over `fadeInFrames`) - and a **fade-out** ramp (1→0 over `fadeOutFrames` ending at the play-length end), - **ignoring note-off** (release is a no-op in Trigger). Fade curve default **equal-power** - (constant-power `sin`/`cos`, click-free on one-shots); pure, unit-tested against a known - signal. -- [x] Core: **modifiable start point** — the voice's initial `readPos_` is `startFrame` - (frame offset), applied in both modes; the existing per-frame `readPos_ += ratio_` read - and loop/interp machinery is otherwise unchanged. Clamp `0 ≤ startFrame < frames`. -- [x] Core: **% length → frames + fade mapping** for Trigger. `lengthFraction ∈ (0,1]` - resolves to `playEnd = start + round(lengthFraction·(frames − start))`; `fadeInFrames` / - `fadeOutFrames` clamp so their sum ≤ play length (fade-out anchored to `playEnd`). Note-off - in Trigger does nothing; the voice frees when `readPos_ ≥ playEnd`. **Choke on note-off is - NOT in scope** (fork S15-F1, held). -- [x] Parameter ownership (per-sample/per-zone, instrument-owned): the play mode + its - params (Gate: AHDSR; Trigger: %-length, fade-in, fade-out; both: start point) attach to - the **capture selection / zone**, stored in the **performance map** (D-B). Additive/ - version-bumped component state; back-compat — a truncated/older blob defaults to **Gate**, - hold=0, start=0, no fades = exactly today's behavior. -- [x] Editor (S11 waveform surface, mode-aware): **Gate** shows draggable **start + loop - markers** (S11's loop UI); **Trigger** shows **start + %-length end + fade-in/out** - handles on the same waveform. A **mode toggle** per capture/zone in the guided setup - (S10) / Zones panel (S10-Z). Marker/handle geometry is pure; commits off-thread via - `commitAndReload`. The instrument stays a **read-only bank consumer**. **Editor control - surface deferred to S12 tier (spec-sanctioned).** - ---- - -## S16 — pitch engine modes (Varispeed vs Preserve) + pitch envelope (per-voice) -**Goal:** Give the sampler **two pitch behaviors** and a pitch envelope that rides whichever -is chosen. **Varispeed** — resampling that couples pitch and duration (classic sampler / -RS5K default). **Preserve** — duration-preserving repitch, where a transposed note keeps -its original length. A **per-zone/per-capture pitch-engine mode**. On top of either engine -rides a per-voice **AD pitch envelope**, **off by default** — a short attack-decay pitch -modulation. Per-instance performance-map state (D-B). CONTEXT.md §Phase S (Pitch engine -modes + pitch envelope). -**Verify (in DAW):** Varispeed — a note an octave up plays half as long as the root note; -Preserve — a note an octave up plays at the same duration as the root note; pitch envelope -off (default) under either engine: no pitch modulation applied (regression); pitch envelope -on: an AD envelope makes a note start offset in pitch and glide to the zone's base pitch -over attack+decay; CPU stays within budget at polyphony cap. -**Depends on:** S3, S5, S15. Independent of S7. - -- [x] Core: **pitch-engine mode on the voice/zone** — `PitchEngine { Varispeed, Preserve }`. - **Varispeed** = `readPos_ += ratio_` (today's path, pitch and duration coupled). **Preserve** - = duration-preserving: the read advances at the **source** rate while a pitch shifter - transposes the output. Mode is per-`PerformanceZone` performance state (D-B), additive/ - version-bumped; absent/older blob → engine default. Pure where possible: Varispeed math and - duration-invariance contract unit-tested. -- [x] Core: **Preserve engine implementation** — hand-rolled pure OLA `pitch_shift` module - (house pattern — CTest-testable, no WDL/REAPER/VST3 type at the boundary). Pre-allocated, - no locks, no `process` allocation; pre-warmed at voice allocation. `pitch_shift_tests` - CTest target. WDL_SimplePitchShifter excluded by include-chain (windows.h); held as a - quality/latency swap alternative (`PitchEngine::Preserve` contract identical behind the seam). -- [x] Core: a per-voice **AD pitch envelope**, engine-aware — `PitchEnvParams { enabled=false, - int64 attackFrames, int64 decayFrames, double peakSemitones }`. Off by default (`enabled=false` - → offset always 0). Under **Varispeed** the offset multiplies `ratio_`; under **Preserve** - the offset is **added to the shifter's shift amount**. Pure, unit-tested. -- [x] Parameter ownership + editor: pitch-engine mode + pitch envelope are per-zone - instrument performance-map state (D-B), additive/version-bumped. Editor exposure deferred - to S12 tier (spec-sanctioned). The instrument stays a **read-only bank consumer**. - -**Notes/decisions:** -- **S16-F1 (default engine):** default is **Preserve** (Daniel's directive: "I want - duration-preserving repitching"). Per-zone toggle prominent and cheap to flip for - drum/one-shot zones that want Varispeed character. -- **S16-F2 (Preserve engine):** hand-rolled pure OLA `pitch_shift` module chosen over - `WDL_SimplePitchShifter` (excluded by include-chain). Both share the same - `PitchEngine::Preserve` contract; `WDL_SimplePitchShifter` is held as the quality/latency - swap (the HELD item in `PLAN.md`). - ---- - -## S17 — drop-and-load: drag a capture onto a track's FX button → instantiate ReaSampler 9000 with the capture loaded -**Goal:** Turn a bank capture into a playable instrument in one gesture. While a capture -is dragged from the `bank_panel`, a track's TCP **FX button** lights as a drop zone, -and dropping there instantiates a **ReaSampler 9000** on that track with the dragged -capture **already loaded and selected** for playback. A third `DragGesture` — -`InstrumentDrop` — added to the `drag_out` pure module. Mechanism: `TrackFX_AddByName` + -VST3 component-state injection via a Steinberg-format `.vstpreset` file built by -`instrument_drop` (applied via `TrackFX_SetPreset`). The `instrument_drop` pure module -constructs the `.vstpreset` image from the instrument's own -`sample_map::serializeComponentState` — one serializer called from both artifacts, so the -byte format cannot drift. CONTEXT-ARCHIVE.md §Phase S (drop-and-load). (`vst_chunk` write -was the original planned mechanism but is silently unappliable for VST3 — replaced by -`.vstpreset` + `TrackFX_SetPreset`, GA DAW-fix pass 2026-07-28.) -**Verify (in DAW):** dragging a single capture from the dock over a track's FX button -highlights it; dropping instantiates ReaSampler 9000 on that track with the dragged -capture loaded, selected, and MIDI-playable immediately; OS drag-out to Explorer still works -unchanged; internal bank-to-bank drag still works unchanged; no media item ever inserted -into the arrange. **VERIFIED (GA DAW-fix pass 2026-07-28).** -**Depends on:** M11 (`drag_out` gesture machinery), Phase S S4, the load-capture seam -(VST3 component-state injection — the shared blob contract). - -- [x] Extend the `drag_out` pure module with the third gesture: `DragGesture::InstrumentDrop` - when a drag armed with a **single** capture is over REAPER's UI outside the panel client - rect; `OsDrag` only when it has left REAPER entirely; `Internal`/`OsDrag`/`None` otherwise - unchanged. `DragState` gains two defaulted fields (`singleCapture`, `overReaperUi`); M11 - callers filling only `{dragging, hasArmedSamples}` get byte-identical M11 behavior — the - existing tests are the non-regression proof. **OPEN QUESTION RESOLVED (multi-capture over - FX button): REJECT** — only `singleCapture` arms InstrumentDrop; multi-payload over - REAPER UI falls through to `OsDrag`. -- [x] Shell (extension): hover-track the pointer over REAPER's UI during the drag; resolve - the hovered track + its FX button via `GetThingFromPoint` (verified present; its info string - reports `"fx_chain"`/`"fx_N"` for the FX region); highlight it as a drop target; on release - drive the drop. FX HOTSPOT resolved — drop target is the FX region (info prefix `"fx_"`), - not home-grown geometry. `instrument_drop_win::resolveFxDropTarget` implements this. - **Landed in `src/instrument_drop_win.cpp`.** -- [x] Shell (extension): on drop, `TrackFX_AddByName(track, "VST3:" + app_version::vstPluginName(), - false, negative)` to always add a fresh instance; capture the returned FX index; invoke the - load-capture seam. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`). - NEVER inserts a timeline item. **Landed: `instrument_drop_win::performInstrumentDrop`.** -- [x] **ReaSampler 9000 load-capture seam:** the instrument's existing `setState`/`getState` - already round-trip the full `ComponentState` via - `sample_map::serializeComponentState`/`deserializeComponentState` (S10). The extension - REUSES that exact serializer through the new pure `instrument_drop` module (builds a - Steinberg-format `.vstpreset` image: 'VST3' header + 'Comp' chunk = serialized component - state, addressed to `vstClassIdHex()`; applied via `TrackFX_SetPreset`). The shared-writer - requirement is met STRUCTURALLY — the blob format cannot drift. Pure round-trip test decodes - back through the instrument's own reader and asserts the capture is selected. - **`instrument_drop` lives at `src/instrument_drop.{h,cpp}`** (extension-side pure module); - CTest target `instrument_drop_tests`. (`TrackFX_SetNamedConfigParm` "vst_chunk" was the - original mechanism but is silently unappliable for VST3 — REAPER's wrapper cannot apply - unframed raw component-state bytes; replaced by the `.vstpreset` path, S-GA-DropFX.) -- [x] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across - single- and multi-capture payloads; M11 OS drag-out and internal bank-to-bank drag both - unchanged. `test_drag_out.cpp` adds the InstrumentDrop cases with M11 cases retained as - the non-regression guard; `test_instrument_drop.cpp` covers the `.vstpreset` image - round-trip (structure, class-ID, chunk offset/size). FX hit resolution is REAPER-API-bound - (`GetThingFromPoint`) — DAW-verified in the shell, not pure-tested (noted honestly). - -**Notes/decisions:** -- `resetDragState()` consolidation in `bank_panel` (prior drift cleaned up this wave). -- Copy-only / no-auto-insert / no-source-deletion invariants all hold: NEVER inserts a - timeline item; the bank file is not deleted; prune remains the sole file-deleter. - ---- - -## S18 — VST3 channel isolation: a beta ReaSampler 9000 that pairs with the beta extension only -**Goal:** Extend Phase V's beta/stable channel split (V4) to the **ReaSampler 9000 VST3 -instrument**, so a beta-built VST is a distinct plugin that pairs only with the beta -extension, and a stable VST pairs only with stable — installable side-by-side in one REAPER -with no collision. **One channel per binary; all identity derives from the ONE -`REASAMPLER_CHANNEL_IS_BETA` bit via `app_version`, no scattered `#ifdef`s.** Mirrors V4's -philosophy exactly. Two forever-stable VST3 class UIDs committed (the existing stable UID + -a new beta UID). CONTEXT.md §Phase S (VST3 channel identity — the UID-pair invariant). -**Verify (in DAW):** stable VST3 (`reasampler_9000.vst3`) and beta VST3 -(`reasampler_9000_beta.vst3`) install side-by-side in one REAPER as distinct plugins; -a beta instance reads only the beta extension's banks; a stable instance reads only stable's; -save/reopen rebinds by the correct UID; nothing plays differently (identity/pairing wave only). -**Depends on:** V4 (`app_version` channel-identity single-source), S1 (VST3 factory identity). - -- [x] **Beta VST3 class UID (the permanent commitment).** Second FOREVER-STABLE class UID - (`REASAMPLER_PROC_UID_BETA_1..4`) alongside the existing stable UID in `reasampler_vst.h`. - A `#if REASAMPLER_CHANNEL_IS_BETA` block selects `REASAMPLER_ACTIVE_UID_*`, which feeds the - single `kReaSamplerProcessorUID` — no separate beta-named constant. The channel bit selects - which UID the factory registers (`DEF_CLASS2`) — compile-time, one class per binary. Both UIDs - frozen forever. -- [x] **Channel-derived binary + display identity (no scattered `#ifdef`s).** Binary name: - CMake VST3 target `OUTPUT_NAME` forks by channel — `reasampler_9000` (stable) / - `reasampler_9000_beta` (beta) — via `REASAMPLER_VST_OUTPUT_NAME`. Display name: factory - `DEF_CLASS2` plug-in display string sourced from `app_version::vstPluginName()` — "ReaSampler - 9000" / "ReaSampler 9000 beta". Editor title band + S6 embed-strip label channel-aware from - the same accessor. -- [x] **Factory vendor/version strings channel-aware** where V4 does the equivalent. Version - display carries the `-beta` render (`appVersion()` yields `"0.9.01-beta"` on beta). -- [x] **Pairing-surface invariant recorded** (no new code — a documented guarantee): channel - isolation is structural — all wire keys live under the channel-derived `kProjExtNamespace()`, - so a future wire key that forgets to isolate is impossible by construction. The invariant - is recorded in CONTEXT.md. -- [x] **DAW-verify contract (the acceptance gate, no unit test — identity is a shell fact).** - Both channels installed side-by-side: each browser sees only its channel's banks; a - project saved with a beta instance reopens rebinding to the beta VST and restores its state. - ---- - -## S-VIEW-BUG-1 — drop-to-FX bug fix (Wave 1) -**Goal:** Dropping a capture onto a track's FX chain (TCP FX button or FX-chain window) must -instantiate + init ReaSampler 9000, not fall through to arrange-as-audio. -**Root cause (diagnosed and fixed in Wave 1):** the FX-hotspot classifier in the pure -`instrument_drop` module only matched FX-chain and floating-window hotspot strings (`fx_*`); -a drop on the TCP FX button (`tcp.fx`) or MCP FX button (`mcp.fx`) was not matched and fell -through to `OsDrag` (arrange-as-audio). -**Fix:** widened then narrowed the predicate in `instrument_drop` to match `fx_*` / `tcp.fx` / -`mcp.fx`; unit-tested in `instrument_drop_tests` at the boundary. The shell (`instrument_drop_win`) -calls the updated pure predicate unchanged. -**Verify (in DAW):** DAW-confirmation pending Daniel's post-merge smoke test — drop → a playing -instance on the track, one Ctrl-Z removes it. - -- [x] Pure `instrument_drop` predicate widened: `infoNamesFxHotspot` now matches `fx_*` / `tcp.fx` / - `mcp.fx`; unit-tested at each matched and unmatched hotspot string. -- [x] Shell unchanged — calls the updated pure predicate. - ---- - -## S-VIEW-SIZE-1 — 1080p default window size (Wave 1, interim) -**Goal:** The editor opens too small (`ViewRect(0,0,560,400)`); set a larger default sized for -the three-band Sample face on 1080p. -**What landed (interim):** default `ViewRect` bumped to **840×560** and a `checkSizeConstraint` -minimum floor added. The mechanism (`getSize`/`setRect`/`checkSizeConstraint`/`onSize`/`canResize` -in `vendor/vst3sdk/public.sdk/source/common/pluginview.h`) was verified correct at this point. -**NOTE:** Wave 2 (T-SHELL) will re-tune the final numbers to the three concrete Sample-face band -heights once the Sample view layout is built; 840×560 is the correct starting point, not the -final tuned value. -**Verify (in DAW):** opens at 840×560 showing more of the editor surface than before; cannot -shrink below the floor. Final tuning deferred to T-SHELL. - -- [x] Default `ViewRect` bumped to 840×560. -- [x] `checkSizeConstraint` minimum floor enforced. - ---- - -# Phase S — editor view-model redesign (three views: Sample / Browse / Zone) - -> **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Merged to dev (Wave 1 -> 2026-07-27, Wave 2 2026-07-27 — `Merge pS-w2-t1-shell` + `Merge pS-w2-t2-velcurve`, -> Wave 3 2026-07-27 — `Merge pS-w3-velcurve-ui`). Integrated suite green. -> **S-VIEW complete — all ten points landed.** - -## S-VIEW-1 — three-view navigation model -**Goal:** Retire the flat Browser|Zones toggle; introduce Sample (home/default), Browse (modal -overlay over Sample, select+confirm), Zone (dedicated surface, own button). Empty state surfaces -Browse as the dominant call-to-action. Fresh instance stays silent (S10 reversal). Fork -S-VIEW-F3 SETTLED — full-window overlay: Browse renders as a full-window modal over Sample -(not a centered sheet). See CONTEXT.md §S-VIEW navigation contract. - -- [x] Flat Browser|Zones toggle retired; three-view model: Sample home, Browse full-window modal - over Sample, Zone dedicated surface with its own button. -- [x] Empty state surfaces Browse as the dominant call-to-action; fresh instance is silent - (S10 reversal preserved). -- [x] S-VIEW-F3 settled and implemented: Browse is a full-window modal overlay over Sample. - ---- - -## S-VIEW-2 — Sample view (the new main face) -**Goal:** Compose the home face: enlarged **hero waveform** with the S11 markers (moved from -Browse), a **fenced root affordance**, the **Mono/Stereo toggle** (moved from Browse), and the -**"Modes-and-down" control strip** (Mode / Pitch engine / AHDSR|Trigger / AD pitch env — moved -from Zone's param panel, single-capture one-zone storage per S15-F2). Reference grammar: -Simpler / Phase Plant (labelled value-strip under a hero waveform). - -- [x] Hero waveform with S11 markers (start/loop start/loop end) moved to Sample face. -- [x] Fenced root affordance on the Sample face. -- [x] Mono/Stereo toggle moved from Browse to Sample. -- [x] "Modes-and-down" control strip (Mode / Pitch engine / AHDSR|Trigger / AD pitch env) moved - from Zone's param panel to the Sample face (single-capture one-zone storage per S15-F2). - ---- - -## S-VIEW-3 — envelope overlay + draggable nodes -**Goal:** Draw the amp envelope (AHDSR for Gate, fade/%-length for Trigger) as a curve over the -Sample waveform at accurate wall-clock time. Pure `envelope_overlay` module (params + frame-length -→ polyline; unit-tested); shell traces it in an accent hue. Breakpoints are draggable handles -(S-VIEW-F2 SETTLED): X → segment time, Y → level on level-breakpoint nodes (sustain drags both -axes), monotonic-in-time + range-clamped. Pure `envelope_edit` module (node hit-test + -pixel-delta→clamped-param inverse map; mirror of `card_drag`; unit-tested). Both surfaces -read/write the same `PerformanceZone` envelope fields. Wave 1 landed the pure modules; -Wave 2 wired them in `reasampler_editor.cpp`. The Trigger frames↔fraction conversion was -extracted to a new pure unit-tested `trigger_seam` module (startFrame threaded correctly). - -- [x] **Wave 1:** `envelope_overlay` + `envelope_edit` pure modules landed and unit-tested. -- [x] **Wave 2:** `reasampler_editor.cpp` traces the overlay and wires draggable node handles. -- [x] **Wave 2:** `trigger_seam` pure module — Trigger frames↔fraction converter (pack and unpack - directions, unit-tested); `startFrame` threaded correctly through the seam. - ---- - -## S-VIEW-4 — preview-trigger + velocity knob -**Goal:** A button firing the sampler at the loaded capture's root note through the live voice -engine (off the audio-thread commit path — no MIDI controller needed) + an adjacent velocity -knob. Preview velocity PERSISTS (S-VIEW-F1 SETTLED): `previewVelocity` on `ComponentState` via -an envelope bump to v6 (`src/vst/sample_map.h`), round-tripped through `getState`/`setState` -over `IBStream`. Zones payload untouched; older blobs lift to a mid default. Wave 1 landed the -field; Wave 2 landed the preview button + velocity knob UI. - -- [x] **Wave 1:** `previewVelocity` field on `ComponentState`, envelope v5→v6 bump, processor - `getState`/`setState` round-trip, clamped 1..127. -- [x] **Wave 2:** preview button + velocity knob UI in `reasampler_editor.cpp`; RT-safe - off-thread preview-note mailbox on the processor. - ---- - -## S-VIEW-5 — Browse reduced to choosing (modal over Sample) -**Goal:** Keep search + bank tabs + captures grid + scroll + selection; add confirm/cancel -(double-click loads). Remove the large waveform preview, Mono/Stereo toggle, root keyboard-strip -(all moved to Sample), and the loop-point labels + track-root message (cut). Render as a -full-window modal overlay (S-VIEW-F3 SETTLED — implemented here and in S-VIEW-1). - -- [x] Search + bank tabs + captures grid + scroll + selection retained. -- [x] Confirm/cancel affordance added; double-click loads. -- [x] Large waveform preview, Mono/Stereo toggle, root keyboard-strip, loop-point labels, and - track-root message removed from Browse. -- [x] Browse renders as a full-window modal overlay over Sample (S-VIEW-F3 settled and shipped). - ---- - -## S-VIEW-6 — key-tracking parameter -**Goal:** Per-`PerformanceZone` scalar on keyboard pitch tracking around the root (100% = 12-tone-ET, -0% = no tracking, 200% = double). Additive/version-bumped, defaults 100% (bit-identical). Key-track -math in the pure sampler core (unit-tested note/root/keyTrack → ratio), applied in both Varispeed -and Preserve repitch. Surfaces as a control on the Zone param panel + Sample control strip. -Wave 1 landed the field/math/apply; Wave 2 landed the UI control. - -- [x] **Wave 1:** per-`PerformanceZone` `keyTrack` field (zones payload v6, default 100% - bit-identical), pure ratio math, applied in both Varispeed and Preserve engines. -- [x] **Wave 2:** UI control on the Zone param panel + Sample control strip. - ---- - -## S-VIEW-7 — piano-key pattern on the keyboard strip -**Goal:** Overlay the actual alternating white/black (bright/dark per palette) key pattern over -the pastel spectral fill so the strip reads as a keyboard. Pure `keyboard_strip` gains a -natural/accidental predicate (12-tone, unit-tested); shell draws the two-tone overlay. Shared -by the Zone strip + Sample root affordance. Wave 1 landed `isNaturalKey`; Wave 2 drew the overlay. - -- [x] **Wave 1:** pure `isNaturalKey` predicate on `keyboard_strip`, unit-tested. -- [x] **Wave 2:** `reasampler_editor.cpp` draws the two-tone overlay over the spectral fill. - ---- - -## S-VIEW-8 — Zone view retained + wired -**Goal:** Keep +Add Zone / Delete, the per-zone keyboard strip (now with the piano pattern), the -Low/High/Root numeric-entry legend, and the per-zone param panel; add the key-tracking control. -Nothing from today's Zones view dropped. - -- [x] +Add Zone / Delete retained. -- [x] Per-zone keyboard strip with piano-key pattern. -- [x] Low/High/Root numeric-entry legend retained. -- [x] Per-zone param panel retained. -- [x] Key-tracking control added to Zone view. - ---- - -## S-VIEW-9 — velocity→amp transfer curve (pure core + engine application) -**Goal:** New pure `velocity_curve` module — eval (monotone cubic Hermite spline, -Fritsch–Carlson slope limiting) + control-point editing (add/move/delete x-ordered + -box-clamped) + hit-test + pixel-delta→clamped-point inverse map; unit-tested at eval + -clamp/order boundaries. Additive `velocityCurve` field on `PerformanceZone` (instrument-owned, -D-B), zones-payload version axis bumped to **v7**; default = **flat y=1** (R10-F1 SETTLED — -Option A, Daniel 2026-07-27: "any velocity plays at full level"); older ≤v6 blobs lift to flat -y=1. Applied at `Voice::start()` — replaces `velocityGain_ = velocity / 127.0` with -`velocityGain_ = curve.eval(velocity)`, off the per-frame path (no new RT work). -**Default is a deliberate non-back-compat behavior change:** the prior engine used linear -`velocity/127`; existing zones' soft hits will play at full level after upgrade. Flagged and -accepted by Daniel. - -- [x] Pure `velocity_curve` module: `eval(velocity 0–127)→amp 0–1` via Fritsch–Carlson monotone - cubic Hermite spline (no overshoot outside [0,1]; collinear points reduce to exact linear ramp). -- [x] Control-point editing: `addPoint`, `movePoint` (x-clamped between neighbours, endpoints - x-pinned), `deletePoint` (endpoints not deletable); `fromPoints` repair-on-deserialize. -- [x] Hit-test + inverse map: `pointAtPixel`, `resolvePointDrag` (pure; mirror of `envelope_edit`). -- [x] Additive `velocityCurve` on `PerformanceZone`, zones-payload v7; pre-v7 blobs lift to - flat y=1; `velocity_curve_tests` CTest target. -- [x] Applied at `Voice::start()` — `velocityGain_` now set from `curve.eval(velocity)`. -- [x] Default flat y=1 — back-compat caveat documented and accepted. - ---- - -## S-VIEW-10 — velocity-curve editor UI (shell, Sample + Zone views) -**Goal:** Draggable transfer-curve editor rendered through the L1 kit in BOTH the Sample view (a -curve box beside the hero band) and the Zone per-zone param panel: the velocity→amp spline drawn -as the `eval` polyline over a 0–127 × 0–1 box, with draggable control points — add on empty-click -(inside the mapping box), move, Alt-click delete interior, drag-off-box delete with a warn-state -affordance. All coordinate math in the pure `velocity_curve` module (two new tested helpers -`pixelFromPoint`/`pointFromPixel`); reads/writes the existing per-`PerformanceZone` -`velocityCurve` field (zones payload v7 — no schema change). Mirrors the S-VIEW-3 envelope-node -interaction grammar (snapshot-at-grab → off-audio-thread commit). Shell: `reasampler_editor.cpp`. - -- [x] `pixelFromPoint` / `pointFromPixel` helpers added to `velocity_curve` — coordinate mapping - between curve-point space (velocity 0–127, amp 0–1) and pixel box; unit-tested in - `velocity_curve_tests`. -- [x] Curve editor drawn in Sample view (curve box beside hero waveform band): `eval` polyline in - accent hue over 0–127 × 0–1 grid, draggable node markers per control point. -- [x] Add on empty-space click inside the box; move on drag; Alt-click to delete interior points; - drag-off-box delete with warn-state affordance. -- [x] Same editor in Zone per-zone param panel (one curve per zone, same interaction grammar). -- [x] Snapshot-at-grab → off-audio-thread commit, matching S-VIEW-3 envelope-node pattern. -- [x] Reads/writes existing `velocityCurve` on `PerformanceZone`; zones payload remains v7 — no - schema change. - ---- - -## Phase S — product name (ReaSampler 9000) -The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26, -on DAW-testing the S1–S6 instrument). The extension remains **ReaSampler**; the instrument -is **ReaSampler 9000**. Framing + propagation surfaces: -`docs/product/midi-playback.md` §Product name. - -- [x] Propagate the display name **ReaSampler 9000** across user-visible surfaces: the VST3 - class **display name** string (in the factory registration), the `IPlugView` editor title - band (currently "ReaSampler Instrument"), the S6 embed-strip label, and the Phase S docs. - **Do NOT change the VST3 class UID** — instances in already-saved projects key off it; a - UID change orphans every existing instance. -- [x] **Rename the binary filename too (S-NAME-1 SETTLED, Daniel 2026-07-26):** rename the - built VST3 module (CMake `OUTPUT_NAME` / target artifact — e.g. `reasampler_9000.vst3`) - alongside the display strings, so the on-disk name matches the product name. Record the - full rename surface: **CMake output name** (the second VST3 target's artifact name), the - **factory vendor/name strings**, the **`IPlugView` editor title**, and the **S6 embed - label**. Do NOT touch the **VST3 class UID** (unchanged — the compat anchor). - ---- - -## Phase S — S-VIEW-1 through S-VIEW-10 (rolled-up) - -- [x] **S-VIEW-1 through S-VIEW-10** — all landed (Wave 1 core + Wave 2 shell + Wave 3 velocity-curve editor UI). See entries §S-VIEW-1 through §S-VIEW-10 above for the full entry set. - ---- - -# Phase D2 — Two-canvas (item-level mode projection; additive to D1) - -> **Design View sub-phase.** Extends D1's track-level mode projection to **item -> level** via REAPER 7 fixed lanes: on a track present in both stances, each mode -> owns a fixed lane — the active mode's lane shows and plays, the inactive mode's is -> hidden and silenced — so a Design take and an Arrange take can share the same -> track and time position without colliding on the view. Nothing in D1 changes. -> Runtime floor rises to **REAPER 7** for this sub-phase (no version-gate branch; -> below v7 it is simply unavailable). Authoritative spec: **CONTEXT.md §Two-canvas -> sub-phase (Phase D2 / Phase E)** and the surrounding §Design View — additive phase -> spec. Product framing: `docs/product/design-view.md` §Two-canvas direction. -> -> **D2-W1 (pure lane extension), D2-W2 (shell: lane application + new-content -> detection), D2-W3-A (lane minting + item→lane assignment + persist round-trip), -> and D2-W3-B (item-level mode actions + W3-A polish) have all landed** — see -> individual entries above. **Phase D2 is functionally complete.** -> -> **Deferred:** panel UI indicator for per-track lane/mode state (a per-track -> lane-split marker). The mode switch already shows the active mode; no natural -> cheap home for a per-track indicator was found in the bank panel. Explicitly -> deferred — not silently dropped. Can be picked up later if wanted. - ---- - -## Milestone 9 — slots (MPC-style) - -> **Abandoned (Daniel, 2026-07-27) — will not be built.** - -**Goal:** "Capture to slot N" / "insert slot N", MIDI-bindable. CONTEXT-ARCHIVE.md -Build order 9. -**Verify (in DAW):** Slot capture and slot insert fire from MIDI bindings; slot -state persists via the index. - -Slot model + slot↔sample assignment. -"Capture to slot N" / "insert slot N" actions, MIDI-bindable. - ---- - -## Post-S-VIEW DAW-fix pass + voice-system redesign (2026-07-27) - -> **Merged to dev 2026-07-27. Integrated suite 52/52 green.** These items were found during -> Daniel's post-S-VIEW DAW testing (fixes) and a subsequent Daniel directive (voice redesign) — -> not tracked as PLAN points. PLAN.md has no corresponding entries to move. - -**Envelope node editing (DAW-fix):** Every Gate stage (A/H/D/S/R incl. a visible in-bounds -Release) and Trigger's zero-fade-out node are now fully grabbable in both modes. `envelope_edit` -uses param-domain schematic scaling (`kGateNodeSepPx = 8` minimum node separation enforced by the -forward map in `envelope_overlay`); all nodes clamped in-canvas. Previously only a subset of nodes -was editable. - -**Gap-free waveform render (DAW-fix):** `columnMinMax` moved to `peaks`; `waveformColumnCount` -moved to `component_geometry`. The dock panel and VST editor now share one gap-free per-column -min/max algorithm via `draw_kit::drawWaveform`. - -**Radial Knob primitive (DAW-fix):** `param_slider` gained a `Knob` control kind (7→5 o'clock arc, -needle, vertical-drag) as the foundation for the Wave B control deck. `ControlKind::Knob` added -to the enum; `computeKnob`/`knobHitTest`/`knobValueAngleDeg` pure helpers added. - -**Zone-bleed fix 3a (DAW-fix):** `reconcileSingleCaptureZones` added to `sample_map` — fixes -stale full-range zone shadowing so the engine plays the zone the editor draws. Called on load -(`setState`), on bank-assign, and on capture-confirm in the editor. - -**Voice-system redesign (Daniel directive):** -- `sampler_core` — user-parameterized voice count (1–32, default 16 via `kDefaultVoiceCount`); - `VoiceMode` enum (Poly/Mono: last-note held-note stack, `MonoTrigger` Retrigger/Legato toggle); - isolated `PreviewCard` class (dedicated preview voice outside the MIDI pool — never steals - from/into it; unity-Preserve zero-latency bypass scoped to it); two-tier panic (`allNotesOff` - = CC 123 release, `allSoundsOff` = CC 120 immediate hard-stop including Trigger one-shots). -- Processor — `process()` sums `PreviewCard` alongside the engine + drain; `retireIdleDrain()` - retires fully-idle drain snapshots on the UI-timer cadence (idle-drain retirement); voice-param - setters (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the - already-decoded keymap via the drain-slot swap — no bank re-read, no WAV re-decode, ringing - tails not cut. -- `ComponentState` envelope bumped **v6→v7**: three voice bytes added (voiceCount 1–32 / voiceMode - Poly|Mono / monoTrigger Retrigger|Legato). Pre-v7 blobs lift to `{16, Poly, Retrigger}` — - reproduces pre-Phase-S behavior exactly. Zones-payload axis untouched. - -- [x] `envelope_edit` + `envelope_overlay`: all Gate and Trigger nodes draggable; `kGateNodeSepPx` - separation enforced; param-domain schematic scaling; in-canvas clamp. -- [x] `columnMinMax` homed in `peaks`; `waveformColumnCount` homed in `component_geometry`; - `draw_kit::drawWaveform` shared by dock panel and VST editor — gap-free at any bins-to-pixels ratio. -- [x] `param_slider` `Knob` control kind: `computeKnob`/`knobHitTest`/`knobValueAngleDeg` pure - helpers; 7→5 o'clock arc; vertical-drag pixel mapping. -- [x] `reconcileSingleCaptureZones` in `sample_map`: stale full-range zone shadowing fixed; called - on `setState`, bank-assign, and editor capture-confirm. -- [x] `VoiceMode` / `MonoTrigger` enums and `PreviewCard` class in `sampler_core`. -- [x] `VoiceEngine` constructible with user voice count; `allNotesOff` (CC 123) / `allSoundsOff` - (CC 120) two-tier panic. -- [x] Processor: `PreviewCard` summed in `process()`; `retireIdleDrain()` on UI-timer cadence; - voice-param rebuild via drain-slot swap (no disk I/O). -- [x] `ComponentState` envelope v6→v7; `voiceCount`/`voiceMode`/`monoTrigger` on `ComponentState`; - back-compat lift in `deserializeComponentState`. - ---- - -## FB1 — Sample-view recomposition + master gain (r11; 2026-07-27) - -> **Merged to dev 2026-07-27. Integrated suite 55/55 green.** S-VIEW-11 + S-VIEW-12 from the -> Wave B plan. No corresponding PLAN.md points remain for these two; S-VIEW-13 (Zone-panel -> parity) stays open in PLAN.md. - -**Goal:** Recompose the S-VIEW Sample face after Daniel's post-landing DAW pass — radial knobs -in fenced groups, compact mode toggles, full-width elastic hero, curve preview button → popup. -Also adds a post-mixer master gain control (MASTER group, −∞…+24 dB) as a new persisted field. -**Verify:** Integrated suite 55/55 green. Sample face renders knob deck with correct group -geometry; hero runs full-width (elastic, 840×620 preserved); curve popup opens/dismisses; right- -click on a node in the popup deletes it (endpoint-guarded); master gain knob adjusts output level -and persists across project save/reopen; pre-v8 blobs lift to unity gain. - -- [x] **`knob_deck` pure module** (`src/vst/knob_deck.{h,cpp}`): group-box / caption-row / - compact-toggle / knob-cell geometry; deterministic whole-group wrap (a group that doesn't fit - the remaining row width starts a new deck row); `DeckLayout` / `DeckHit` structs; blank - reserved cells (id −1 for AMP ENVELOPE's 5-cell stability). Mirror of `action_bar`/ - `param_slider`; no LICE or REAPER types. New CTest target `knob_deck_tests`. -- [x] **`curve_popup` pure module** (`src/vst/curve_popup.{h,cpp}`): centered sheet geometry — - width `clamp(60% window, 360..520)`, height `clamp(55% window, 260..380)`; title row + 18×18 - Close button rect; curve-box rect (border rect; shell applies `curveBoxFromRect` inset); - outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types. New CTest - target `curve_popup_tests`. -- [x] **`master_gain` pure module** (`src/vst/master_gain.{h,cpp}`): dB↔linear taper math for - the −∞…+24 dB master gain knob — `masterGainLinearFromNorm`, `masterGainNormFromLinear`, - `masterGainDbFromNorm`, `masterGainNormFromDb`, `formatMasterGainLabel`; norm 0 = true silence - (exact 0.0); unity ≈ 0.714 normalized. Shared by the editor knob and the processor multiply - so needle, persisted value, and audio multiply cannot drift. No LICE or REAPER types. New - CTest target `master_gain_tests`. -- [x] **Knob deck shell** (`reasampler_editor.cpp`): Sample control strip rebuilt as five fenced - groups — **AMP ENVELOPE** (Gate|Trigger caption toggle; 5-cell width reserved; Gate: Attack · - Hold · Decay · Sustain · Release; Trigger: Fade In · Length % · Fade Out), **PITCH** - (Varisp|Preserve caption toggle; Key Track knob), **PITCH ENV** (Off|On caption toggle; - P.Attack · P.Decay · P.Depth; Disabled-not-hidden when Off), **VOICE** (Voices knob + - Poly|Mono caption toggle + Retrig|Legato row toggle), **MASTER** (Gain knob). Slider rows + - full-width toggles retired from the Sample face. Hero band recomputed as elastic (absorbs - window height minus fixed bands; floor 150px); deck band bottom-anchored at fixed height via - `deckHeight()`. -- [x] **Full-width hero** (S-VIEW-12): velocity-curve carve-out removed from hero rect; hero - now runs edge-to-edge between the kPad margins. S-VIEW markers (start/loop) + envelope overlay - unchanged. -- [x] **Curve preview button + popup** (S-VIEW-12): 28×28 miniature curve preview button placed - immediately right of the Vel knob in the cluster band; left-click opens the `curve_popup`- - geometry centered sheet; **right-click on a node in the popup deletes it** (endpoint-guarded, - commits via the same path as Alt-click — `deletePoint` guard makes endpoint right-clicks a safe - no-op). Dismiss: Close ×, outside-click (no drag in flight), Esc. All landed curve-editor - interactions preserved (drag, click-add, Alt-click delete, drag-off delete). -- [x] **Post-mixer master gain** (new): `masterGainLinear` field added to `ComponentState`; - `kComponentStateVersion` bumped **v7→v8**; pre-v8 blobs lift to `masterGainLinear = 1.0` - (unity). Processor applies the gain as a per-sample ramp over the summed output — no zipper - noise. `master_gain` pure module is the shared math seam between the editor knob and the - processor multiply. - -**Notes/decisions:** -- **Persistence is NOT zero-change.** The CONTEXT.md r11 spec described r11 as a pure view - recomposition; master gain added `ComponentState` v8. CONTEXT.md §S-VIEW r11 intro updated to - record this correctly (the "zero component-state change, no new persisted fields" clause - replaced with a factual note on the v7→v8 bump and the master gain field). -- **R11-F1 (hero height) settled at build:** elastic hero, 840×620 default preserved — the - hero grows with the window, floor 150px. No window-size change required. -- **R11-F2 (Zone-panel parity) deferred to FB2:** S-VIEW-13 remains open in PLAN.md. -- New CTest targets: `knob_deck_tests`, `curve_popup_tests`, `master_gain_tests`. - ---- - -## FB2 — Zone-panel parity (r11; 2026-07-28) - -> **Merged to dev 2026-07-28. Integrated suite 55/55 green.** S-VIEW-13 from the Wave B -> plan. Closes R11-F2. **Completes the r11 editor recomposition (Phase S Wave B).** - -**Goal:** Bring the Zone param panel to the same knob deck + curve-preview-button/popup -grammar as the Sample face (FB1), retiring `param_slider`'s linear slider rows on the Zone -panel so one control grammar renders on both surfaces of the one per-zone storage site. -Zone-authoring affordances (+Add Zone / Delete, the per-zone piano-key strip, Low/High/Root -numeric-entry legend) are preserved. VOICE and MASTER groups stay Sample-only (per-instance). -**Verify:** Integrated suite 55/55 green. Zone panel renders the knob deck with correct group -geometry; curve preview button opens the popup (right-click deletes a node, endpoint-guarded); -Zone-authoring affordances intact; no regression on the Sample face. - -- [x] **S-VIEW-13 — Zone-panel parity.** Zone param panel rebuilt using the `knob_deck` - geometry module and the `curve_popup` module — same group-box / caption-row / knob-cell - layout as the Sample face. `param_slider` linear slider rows retired on the Zone panel. - The per-zone storage site is shared; the control grammar is now uniform across both surfaces. -- [x] **Zone-authoring affordances preserved.** +Add Zone / Delete buttons, the per-zone piano- - key strip (with the real piano black-key pattern), and the Low/High/Root numeric-entry legend - are unchanged by the recomposition. -- [x] **VOICE and MASTER groups remain Sample-only.** Per-instance controls are not placed on - the Zone panel, which is per-zone. - -**Notes/decisions:** -- **R11-F2 SETTLED:** Zone panel adopts knob deck + curve popup. `param_slider`'s slider/toggle - half is now dormant — the FA4 `Knob` primitive (`ControlKind::Knob`) is the only live consumer - of that side of `param_slider`. No new pure modules required: `knob_deck` and `curve_popup` - (landed in FB1) are consumed directly. -- **Phase S editor Wave B complete.** S-VIEW-11 (knob deck + curve popup, FB1), S-VIEW-12 - (full-width hero + master gain, FB1), and S-VIEW-13 (Zone-panel parity, FB2) are all landed. - No open r11 or S-VIEW Wave B items remain in PLAN.md. - ---- - -## GA post-launch DAW-fix pass (2026-07-28) - -> **Merged to dev 2026-07-28. Integrated suite 55/55 green.** Six fixes found during -> GA DAW testing — not tracked as PLAN points. PLAN.md has no corresponding entries to move. - -**Preserve pitch engine (SOLA rewrite):** `pitch_shift` rewritten from a dual-tap OLA (taps hard-locked -half a window apart — fixed relative phase causes anti-phase cancellation on many source frequencies, -producing spectral garbage on repitched notes) to **correlation-aligned SOLA splices**: one active read -tap chases the write head at the shift ratio; each splice jump is refined by a cross-correlation search -so the new read point is waveform-aligned, then old and new taps are crossfaded with a raised-cosine, -amplitude-complementary fade. Clean pitch shift past +24 st; a repitched pure sine stays a single tone. - -**Voice takeover declick:** `Voice::start` gained a `takeoverDeclick` parameter (gated on the envelope -complement, applied when the flag is true) to remove the click at mono retrig/fallback and poly at-cap -steal boundaries. The "steal-all" symptom observed in DAW was diagnosed as no-loop sample exhaustion -under Preserve (voices played to silence, not stolen); the engine steals exactly one voice per note-on -as designed. - -**Stereo bus pin + channel-mode auto-default:** The output bus is now **permanently stereo** — the -dynamic mono↔stereo bus renegotiation that hard-panned dual-mono is deleted. `ChannelMode` is -decode-only (controls downmix vs. dual-mono on WAV decode). Channel mode **auto-defaults from the -loaded capture's channel count** via `channelModeFor()` (pure helper in `sample_map`) when the user -has not explicitly toggled it; explicit toggle latches the preference. `ComponentState` bumped **v8→v9**: -`channelModeExplicit` bool added; pre-v9 blobs treat the stored mode byte as an explicit preference -(no auto-override). - -**Drop-to-FX injection fix:** Injection switched from `TrackFX_SetNamedConfigParm(..., "vst_chunk", -)` (silently unappliable for VST3 — REAPER's wrapper cannot accept unframed component-state -bytes) to a **Steinberg-format `.vstpreset` file** whose 'Comp' chunk is the serialized component -state, applied via `TrackFX_SetPreset`. The `instrument_drop` pure module builds the `.vstpreset` -image; `instrument_drop_win` writes a transient temp file and calls `TrackFX_SetPreset`. FX hotspot -now prefix-matches `tcp.fx*`/`mcp.fx*`/`fx_*` (embed strip tokens excluded). `reasampler_uid.h` -split out of `reasampler_vst.h` as an SDK-free header so `instrument_drop` can derive the class-ID -hex string without the VST3 SDK. - -**Drag-out arm fix:** `SetCapture` moved to drag-arm (the `dragArmed` branch of `handleClick` in -`bank_panel`) so a first straight-out drag — pointer leaving the client rect before a second click -— correctly arms and fires the OS drag-out. Previously `SetCapture` was called only after the drag -threshold was crossed inside the panel, so a fast straight-out drag on the first attempt received no -`WM_MOUSEMOVE` messages outside the client rect and the gesture never transitioned to the OS drag. - -**Panel mode-toggle repaint:** The Design/Arrange mode-toggle actions (`doToggleMode` / -`doActivateMode` in `actions.cpp`) now call `bankPanelInvalidate()` after applying the mode switch. -Previously the panel footer `[Arrange|Design]` toggle only repainted on a button click inside the -panel, not when the mode was changed via the registered action (e.g. from the Actions list or a -keybinding). - -- [x] `pitch_shift`: dual-tap OLA replaced by correlation-aligned SOLA; CTest `pitch_shift_tests` - updated to assert single-tone output on repitched sine. -- [x] `sampler_core` `Voice::start`: `takeoverDeclick` parameter added; mono retrig/fallback + poly - at-cap steal pass `true`; `PreviewCard` and standard voices pass the flag correctly. -- [x] `reasampler_processor`: output bus permanently stereo; `channelModeExplicit_` member added; - `channelModeFor()` called on `reloadFromBank` when not explicit; `ComponentState` v8→v9 - (`channelModeExplicit` byte); pre-v9 lift treats stored mode as explicit. -- [x] `instrument_drop`: `.vstpreset` image builder replacing `vst_chunk` base64 path; `vstClassIdHex()` - sourced from `reasampler_uid.h`; `instrument_drop_tests` covers preset image structure round-trip. -- [x] `reasampler_uid.h`: new SDK-free header in `src/vst/` owning the frozen class-UID macros; - `reasampler_vst.h` and `instrument_drop` both source from it. -- [x] `instrument_drop_win`: FX hotspot prefix-set extended (`tcp.fx*`/`mcp.fx*`/`fx_*`); transient - `.vstpreset` write + `TrackFX_SetPreset` call replaces `TrackFX_SetNamedConfigParm` "vst_chunk". -- [x] `bank_panel`: `SetCapture` moved to drag-arm branch; `bankPanelInvalidate()` called from - `actions.cpp` after mode-toggle actions. - ---- - -## GA2 fix pass (2026-07-28) - -> **Merged to dev 2026-07-28. Integrated suite 55/55 green.** Two fixes found during -> GA DAW testing — not tracked as PLAN points. PLAN.md has no corresponding entries to move. - -**Preserve pitch engine — ring prime:** `pitch_shift` now **primes its ring buffer with the -actual upcoming source** at note-on (was zero-filled). Result: gap-free frame-0 onset (the -~25 ms Preserve onset latency is gone — Preserve now speaks on frame 0, matching Varispeed), -clean repitch across the full C1–C8 range, and a real-content-bounded tail (the last-window -tail-truncation that previously clipped the decay is gone). - -**Takeover declick rev-2/3 — bounded blend:** The declick mechanism is now a **bounded blend** -(`out*(1-w) + ref*w`, w decaying from 1.0 over the blend window), applied at every takeover -boundary: mono retrig/fallback, poly at-cap steal, and **preview re-trigger**. This supersedes -the earlier `(1-amp)` envelope-complement gate that zeroed the compensation on Trigger and -zero-attack restarts, leaving a click Daniel still heard in those cases. The preview card is -now covered by the same mechanism. - -- [x] `pitch_shift`: ring buffer primed with actual upcoming source at note-on; `pitch_shift_tests` - updated. -- [x] `sampler_core` `Voice::start`: declick changed from `(1-amp)` gate to bounded blend - (`out*(1-w) + ref*w`, w 1.0→0); covers mono retrig + poly at-cap steal + preview re-trigger. - ---- - -## Preview via real MIDI note path + self-contained playback (pS; 2026-07-28) - -> **Merged to dev 2026-07-28. Integrated suite 55/55 green.** Architecture corrections from DAW testing — not tracked as PLAN points. PLAN.md has no corresponding entries to move. - -**Preview via real MIDI note path:** the dedicated `PreviewCard` is RETIRED. Preview now injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` (the same path host MIDI uses), so preview obeys polyphony/mono/voice-stealing/envelopes. `sampler_core` no longer has a `PreviewCard`; the processor no longer sums a preview voice alongside the engine + drain. The unity-Varispeed-bypass demotion (added in GA2 for the PreviewCard) is removed — GA2's primed shifter speaks on frame 0 anyway. - -**Self-contained playback:** ReaSampler 9000 no longer depends on the extension being loaded to play. `ComponentState` bumped v9→**v10** with a `SampleRefs` table: per referenced sample the instance owns a project-relative path + decode intrinsics (root note, loop points, channels, displayName) — NOT a copy of the audio. On load, `reloadInstrument()` decodes directly from those refs (bank-free — plays with the extension absent). The bank/bridge is now a browser source: loading a capture copies its reference into the instance's `SampleRefs`. The reopen-heal apparatus (SetTimer retry timer, editor-gated poll-to-play) is removed. Pre-v10 blobs lift to empty refs and re-save self-contained; a bounded legacy lift covers migration. - -- [x] `sampler_core`: `PreviewCard` struct and engine methods removed; preview path routes through the main `VoiceEngine` noteOn at the capture's root note. -- [x] `reasampler_processor`: `reloadInstrument` decodes from `SampleRefs_` (bank-free); `SampleRefs` table added to `ComponentState` v10; heal timer + poll-to-play removed; preview summing removed; `PreviewCard` member removed. -- [x] `sample_map`: `SampleRefs` type + `SampleRefEntry` struct; `refreshRefsFromBank`, `retainRefs`, `resolvePerformanceFromRefs` (decodes directly from refs, no bank blob required); `kComponentStateVersion` bumped to 10; pre-v10 lift to empty refs. -- [x] `ComponentState` v10 serialization round-trip: new `sampleRefs` field; pre-v10 blobs migrate on load. - ---- - -## Q-W0 fix-now remediations — closes Q-W0 (2026-07-28) - -> **Merged to `phase-q` 2026-07-28 (commit `546927e`).** Closes the Q-W0 sub-gate — Q-W0 -> (pre-restructure functional + DSP quality audit) is now fully closed. Part of the phase-q -> integration that later reached 60/60 green with Q-W1. - -**Goal:** Land the six Daniel-approved fix-now remediations from the Q-W0 audit triage, plus -seven review riders surfaced during their review, before any structural Q-wave begins. -**Verify:** Each remediation lands with its module's CTest target green; the audible DSP fixes -carry a stated before/after listening check. - -- [x] **T1-01** — linked-lag stereo SOLA + follower self-heal fallback. -- [x] **T1-03** — playable-span prime bound. -- [x] **T1-09** — declick dead-state removal. -- [x] **T2-01a** — provenance wire-cursor hardening backport. -- [x] **T3-01** — rate-derived gain ramp. -- [x] **T3-03** — rate-derived fade ceiling. -- [x] Seven additional review riders landed in the same merge (not individually itemized in - this entry). - -**Notes/decisions:** -- Landing these six fix-nows (plus the review riders) closes Q-W0 entirely — audit, triage, - sign-off, and remediation are all complete — and opens Q-W1. - ---- - -## Q-W1 — safe opener: `core/json` extraction + directory/namespace layout (2026-07-29) - -> **Merged to `phase-q` 2026-07-29. Integrated suite 60/60 green.** First structural wave of -> Phase Q; unblocked by the Q-W0 fix-now remediations closing 2026-07-28. - -**Goal:** The zero-god-module-risk opener. Extract a pure `core/json` module (parser + -serializer) and delete the five hand-rolled JSON decoders — the four `Parser`s in `bank_model` -/ `bank_book` / `view_mode_model` / `owned_manifest` plus `tail_control`'s fifth decoder; impose -the settled `core/`/`shell/`/`app/` directory layout + sub-namespaces on the clean pure libs, -clean shells, and clean VST pure libs — pure relocation, no logic change. -**Verify:** CTest green at every commit (60/60). The five duplicate JSON decoders are gone, -replaced by one `core/json` consumed by all five former consumers; round-trip serialization is -byte-identical to before. Every relocated clean module compiles and its test executable passes -unmoved. No REAPER type crosses into any `core/` file. - -- [x] `core/json` extracted (`json::Reader`/`json::Writer`); `bank_model`, `bank_book`, - `view_mode_model`, `owned_manifest`, and `tail_control` rewired onto it; the five hand-rolled - JSON decoders deleted. -- [x] Wire-`Cursor` family collapsed into `core/wire` — one hardened survivor codec. -- [x] Shared `readFileBytes` pure helper added; linked by both artifacts. -- [x] `slot_map` extracted from `bank_book`, with its own `slot_map_tests`. -- [x] ~50 clean modules relocated into `core/{model,view,capture,audio,ui,reclaim,version,json, - util,wire}/`, `core/instrument/{engine,map,ui}/`, `shell/{capture,panel,view,persist,actions, - instrument}/`; `main.cpp` moved to `app/`. Sub-namespaces applied to every relocated clean - module. -- [x] Rect unification: one concrete `ui::Rect` + `contains()` + per-role aliases — the - XYWH-vs-LTRB fork retired, `footer_bar.h`'s "NAME NOTE" collision workaround gone. -- [x] `clamp01` deduplicated. -- [x] Naming riders: survivor JSON parser minted as `json::Reader`/`json::Writer`; `BankIndex` - renamed to `BankModel` (verified by `bank_model_tests`). -- [x] `reasampler_uid.h` relocated to `core/wire/`. -- [x] Interim `core/namespaces.h` shim added for the six not-yet-split god TUs; each downstream - split wave (Q-W2 onward) retires its own includes of it as that module splits. - -**Notes/decisions:** -- **Riders explicitly skipped/deferred:** T4-22 (`hitIndex` hit-test template) — not trivial, - deferred as an opportunistic follow-on once the rect unification is in use downstream; T4-06 - (`view_mode_model` planner split) — optional, deferred; T4-09 (`view_lanes` split) — - deferred (in scope only if a later wave touches `view.cpp` anyway). -- **Open residual — `bank_book.cpp` still 737 LOC.** The serialize/deserialize seam is - identified but blocked on a `nameKey` linkage design decision, escalated to Daniel and - **pending** as of 2026-07-29. Downstream waves touching `bank_book` should check this - residual before assuming the split is finished. - ---- - -## Q-W2 — split `bank_panel.cpp` (the biggest extension god-module — 3459 LOC at the Q-W0 census) (2026-07-29) - -> **Merged to `phase-q` 2026-07-29 (merge of `pq-w2-panel`). Integrated suite 61/61 green, -> reviewed-approved.** - -**Goal:** Split the largest extension god-module (8+ responsibilities) along the audit's named -seams — eight TUs (Q-5 SETTLED with the T4-01 reshape, Daniel 2026-07-28): `panel_render` / -`panel_thumbnails` / `panel_audition` / `panel_input` / `panel_bank_ops` / `panel_window` + -`panel_layout` (toolbar/footer/menu rects + row/cluster builders + region geometry glue) + -`panel_drag` (the card-drag/hover state machine — it already has a pure mirror, `card_drag`) — -without the two new seams, `panel_render` (~700) and `panel_input` (~800) would ship over the -~600 ceiling on day one. Split the fat `bank_panel.h` alongside (Interface Segregation). -Preserve the audition hot path as a direct call-through, never virtual. `panel_bank_ops` becomes -the single home for the bank-CRUD verbs that W4 will dedupe `actions.cpp` against. CONTEXT.md -§Phase Q (bank_panel split seams; hot-path audition guardrail). See -`docs/product/code-organization.md` §2.1, §5. -**Verify:** CTest green at every commit. Each seam is its own TU under `shell/panel/` and every -TU lands under the ~600-line ceiling (the Q-5 acceptance bar); the panel draws, thumbnails, -auditions, handles input, does bank ops, and manages its window exactly as before (no behavior -change — verify in DAW that the panel is visually and interactively unchanged). Audition/preview -call path stays a direct call-through (no virtual dispatch, no added header→TU indirection on -the preview path). The ~20-function public API is now segmented across the split headers. - -- [x] Split rendering (`draw*`/`paint*`) → `panel_render`; thumbnail compute+cache → - `panel_thumbnails`; toolbar/footer/menu rect + row/cluster builders + region geometry → - `panel_layout` (new seam, T4-01). -- [x] Split the audio audition/preview engine → `panel_audition` — direct call-through, not - virtual; preview idle path unchanged. -- [x] Split input handling (mouse/key/wheel) + new-content detection → `panel_input`; window - lifecycle + OS drag-out/drop-target → `panel_window`; the card-drag/hover state machine → - `panel_drag` (new seam, T4-01). Per-mouse-move work stays plain free-function calls (T4-28). -- [x] Extract bank-CRUD verbs → `panel_bank_ops` (the future single owner; W4 dedupes - `actions.cpp` against it). Split `bank_panel.h` into per-seam headers (I). -- [ ] Verify in DAW: panel unchanged; CTest green; no hot-path indirection added; all eight TUs - under the ~600 ceiling. — **PENDING**: in-DAW panel-parity verification not yet performed on - `phase-q` (deferred by design). - -**Notes/decisions:** -- **Recorded ceiling overages (reviewer-endorsed, preserved as a durable record per CONTEXT's - "silent overshoot is not legitimate" rule):** `panel_input.cpp` 636, `panel_render.cpp` 613, - `panel_state.h` 608 — overage is comment volume; non-comment lines are ~322–369 per file; no - honest seam remains; bisection was rejected. -- **Review note for the Q-W4 planning record:** `panel_bank_ops`'s verbs still embed - prompts/panel-state nudges — Q-W4's dedupe needs promptless inner verbs (`renameBank(id,name)` - etc.), not a call-site swap; `promptText`/`mintBankId` are byte-identical twins with - `actions.cpp` and are the cheapest first dedupe. -- ~50 TU-private helpers wrapped in anonymous namespaces (review follow-up landed in the same - merge). - ---- - -## Q-W2v — split the VST god-modules (2026-07-29) - -> **Merged to `phase-q` 2026-07-29 (merge of `pq-w2v-vst`). Integrated suite 61/61 green, -> reviewed-approved.** - -**Goal:** Close the audit's structural scope gap: the VST artifact's god-modules had no owning -wave, and `reasampler_editor.cpp` (3065 LOC) is the largest file in the repo. Split the editor -into eight TUs along the Sample/Browse/Zone face axis (T4-11): `editor_session` / -`editor_controls` / `editor_layout` (pure-candidate hoist into the existing pure homes — -`editor_geometry` is the named owner; this discharges T2-06's stranded-layout-math finding) / -`editor_paint_sample` / `editor_paint_browse_zone` / `editor_input_sample` / -`editor_input_browse_zone` / `editor_platform`. Split `reasampler_processor.cpp` (1164 LOC) into -three TUs (T4-12): `processor_state` / `processor_reload` / lifecycle+`process()` kept whole. -Split `sample_map` into resolution core vs the `component_state_io` binary codec + matching -header split (T4-13 ≡ T2-07 — the codec grows every envelope bump; the extension stops linking -the whole voice engine to serialize one preset blob). `sampler_core.cpp` stays whole (968 LOC) — -a DOCUMENTED hot-path exception to the ~600 ceiling (T4-14/T4-27: envelope `tick()`s run -per-voice-per-sample; same-TU definition is what lets the compiler inline the stack, no LTO in -the build; a by-class split is the exact heuristic-(3) dispatch blowout); its header splits into -`zone_params.h` + `sampler_core.h`. The `core/wire` LE byte-codec template (`putLE`/`readLE`, -T4-20) lands here with its biggest consumer. CONTEXT.md §Phase Q (VST split seams; `sampler_core` -exception). -**Verify:** CTest green at every commit. Editor and processor behave identically in DAW (visual -+ interactive parity across all three faces; `process()` audio unchanged). Every new TU lands -under the ~600-line ceiling except the one documented `sampler_core.cpp` exception. `process()` -+ its per-block helpers stay one TU; the atomic-pointer-swap reload pattern gains no virtual seam -(T4-29); no dispatch-stack blowout anywhere (heuristic 3). - -- [x] Split `reasampler_editor.cpp` → the eight face-axis TUs; hoist `editor_layout`'s pure - geometry into the existing pure homes (`editor_geometry` — discharges T2-06). -- [x] Split `reasampler_processor.cpp` → `processor_state` / `processor_reload` / - lifecycle+`process()` whole; no virtual seam on the atomic-swap pattern (T4-29). -- [x] Split `sample_map` → resolution core + `component_state_io` codec (+ header split); the - extension's preset-blob path stops linking the voice engine (T4-13 ≡ T2-07). -- [x] `sampler_core`: split `zone_params.h` out of the header; TU stays whole — documented - exception (T4-14/T4-27), recorded in the wave brief so nobody "fixes" it later. -- [x] Land the `core/wire` LE byte-codec template (`putLE`/`readLE`, T4-20) with - `component_state_io`; other consumers rewire opportunistically. -- [x] Rider: adopt the pure `ThumbnailKey` on the VST editor side (T2-10). -- [ ] Verify in DAW: editor + processor unchanged; CTest green; ceiling met (one documented - exception); no added dispatch. — **PENDING**: in-DAW editor/processor-parity verification not - yet performed on `phase-q` (deferred by design). - -**Notes/decisions:** -- Golden full-blob v11 fixture pins the `component_state_io` codec bytes. -- The `src/vst/` directory is gone — all VST sources now live under the Q-W1 `core/instrument/` - and new `shell/instrument/` layout. -- **Deferred/known:** `component_state_io.h` still includes `sample_map.h`→`sampler_core.h` - transitively (T2-07's header half — future work); the `engine` namespace is deferred - (`sampler_core` stays flat `reasampler`); capture-side LE rewires are left for the capture - family. - ---- - -## Q-W3 — split `main.cpp` (hoist orchestration; leave main = pointers + entry + dispatch) (2026-07-29) - -> **Merged to `phase-q` 2026-07-29 (merge of `pq-w3-main`). Integrated suite 61/61 green, -> reviewed-approved.** - -**Goal:** Reduce `main.cpp` (1897 LOC at the Q-W0 census) to its actual job — API pointers + -`ReaperPluginEntry` + dispatch — by hoisting four TUs (T4-02 reshape, SETTLED with Q-5, Daniel -2026-07-28 — the planned three left `capture_orchestrator` at ~885, over the ceiling): -`capture_orchestrator` (`RunCapture` / `captureAndIndexOne` / `renderOffline` / single-capture + -realtime/insert action bodies — lands ~450), `capture_batch` (the batch family + -`RunRecaptureFromSource` + the two RAII selection guards — recapture is planner-driven like batch -and shares the guard machinery), `scope_resolve` (`resolveRange`/`resolveRazorRange`/ -`collectSelectedTracks` + provenance assembly inputs), and `realtime_lifecycle` (the -realtime-capture state machine + globals). `FxBypassGuard` moves out but stays a stack RAII -object (precision-critical); the realtime idle tick stays a single pointer test. Q-W0 riders -owned by this wave (all SETTLED 2026-07-28): delete `ICaptureBackend` (T4-26 — one deriver, zero -polymorphic call sites; `OfflineRenderBackend` becomes concrete; the CLAUDE.md/CONTEXT "two -backends behind one interface" correction rides this wave's own commit, not earlier); the shared -`stampCaptureSample` capture-epilogue dedupe (T2-09); the `capture_realtime_finalize` split -riding the Q-9 rename (T4-08); the `makeUniqueTag` per-session monotonic-counter fix (T1-11); and -the WAV/RIFF consolidation (audit §4e) — one pure `wav_codec` owner (walker + layout + build + -patch), absorbing `ingest.cpp`'s pure WAV build helpers (T2-08 / T4-23 / T4-10). CONTEXT.md -§Phase Q (main split seams; FxBypassGuard + realtime-tick guardrails). See -`docs/product/code-organization.md` §2.1, §3. -**Verify:** CTest green at every commit. Capture (offline + realtime + batch + recapture) behaves -identically in DAW; the null test still nulls, bit-identical repeats still match (the precision -invariants `FxBypassGuard` protects are unchanged); capture ≠ placement holds (no hoisted `Run*` -path gains an `InsertMedia` call). The realtime idle fast-path is still a single pointer test. -`main.cpp` is now pointers + entry + dispatch only. The four hoisted TUs + `wav_codec` land under -the ~600 ceiling; the WAV/RIFF layout has one pure owner (the dedup-by-hash and null-test -invariants now rest on one implementation); `ICaptureBackend` is gone with no behavior change and -the CLAUDE.md/CONTEXT description is corrected in the same commit. - -- [x] Hoist capture orchestration → `capture_orchestrator` (`shell/capture/`); keep - `FxBypassGuard` a stack RAII object as it moves (precision-invariant-critical). -- [x] Hoist the batch family + `RunRecaptureFromSource` + the two RAII selection guards → - `capture_batch` (fourth hoist, T4-02) so `capture_orchestrator` lands ~450. -- [x] Hoist scope/source resolution + provenance assembly inputs → `scope_resolve`. -- [x] Hoist the realtime-capture lifecycle state machine + globals → `realtime_lifecycle`; idle - tick stays a single pointer test. -- [x] Leave `main.cpp` = API-pointer ownership + `ReaperPluginEntry` + dispatch; move to `app/`. -- [x] Naming rider (Q-9 — SETTLED, Daniel 2026-07-28: yes): align the `capture_realtime` (shell) - / `realtime_record` (pure) word-order inversion to the house shell↔core convention — the pure - module takes the stem `capture_realtime`, the shell takes the suffix (`drag_out`↔`drag_out_win` - is the model). Split `capture_realtime_finalize` (async lifecycle vs file-side finalize) in the - same surgery (T4-08). No rename on a file this wave isn't already touching (Q-7). -- [x] Delete `ICaptureBackend` (T4-26): `OfflineRenderBackend` becomes concrete; correct the - CLAUDE.md/CONTEXT "two backends behind one interface" description in the same commit. -- [x] Dedupe the capture-stamp epilogue → shared `stampCaptureSample` (T2-09 — the divergent bits - stay in the realtime caller); fix `makeUniqueTag` with a per-session monotonic counter, both - call sites (T1-11 — same-second batch captures currently collide silently). -- [x] WAV/RIFF consolidation rider (audit §4e — SETTLED, Daniel 2026-07-28): one pure `wav_codec` - owner (chunk walker + layout + build + patch), absorbing `ingest.cpp`'s pure WAV/PCM build - (T4-10 — the ingest shell drops to ~500 and the WAV build gains a test target). -- [ ] Verify in DAW: null test nulls, bit-identical repeats match, capture≠placement holds; CTest - green; no realtime-tick branch-shape change. — **PENDING**: in-DAW null-test / - bit-identical-repeats verification not yet performed on `phase-q` (deferred by design). - -**Notes/decisions:** -- `wav_codec_tests` replaces `wav_trim_tests`; `capture_realtime_tests` replaces - `realtime_record_tests`. -- **Known open:** `wav_trim.h`'s transitional forwarding shim still has three live includers - (`sample_map.h`, `editor_session.cpp`, `processor_reload.cpp`) — repoint-and-retire is a named - follow-up; `ingest.cpp` trimmed to 567 LOC but keeps the `namespaces.h` shim (`ingest` + `view` - remain the shim's unowned consumers). - ---- - -## Q-W4 — split `actions.cpp` + dedupe bank verbs against `panel_bank_ops` (2026-07-29) - -> **Merged to `phase-q` 2026-07-29 (merge of `pq-w4-actions`). Integrated suite 61/61 green, -> reviewed-approved.** - -**Goal:** Split the two unrelated command-id families in one TU (1016 LOC at the Q-W0 census — -T4-03: the planned seams still land sub-600, no reshape) into -`design_view_actions` / `bank_actions` / `prune_action`, and **dedupe** `actions.cpp`'s own -`promptText`/`mintBankId` and bank verbs against the `panel_bank_ops` single-owner established in -Q-W2. `prune_action` keeps the `doBankPruneFolder` deletion authority contract intact (routes to -`persist`'s `prune_fs` after W5). CONTEXT.md §Phase Q (actions split seams; bank-verb dedupe). -See `docs/product/code-organization.md` §2.1, §2.4. -**Verify:** CTest green at every commit. Every action fires identically in DAW (Design View -family; multi-bank create/rename/reorder/delete/evacuate/activate/move/copy/remove; prune). The -bank-CRUD verbs have **one** implementation home (no `bank_panel`/`actions` duplication). Each -bank verb still wraps its mutation in one batched undo point; the prune action still writes no -ext state and opens no undo point. Command-id strings are **unchanged** (FOREVER-STABLE -contract — a reorg must not touch a shipped command id). -**Depends on:** Q-W2 (`panel_bank_ops` is the dedupe target). Independent of Q-W3. - -- [x] Split `actions.cpp` (1019 LOC) → `design_view_actions` (toggle/activate/tag/untag/ - showBoth/moveItems), `bank_actions` (bank CRUD family), `prune_action` (`doBankPruneFolder` — - the single file-deletion action), plus a fourth shared `action_registry` TU under - `shell/actions/`. -- [x] Dedupe `actions.cpp`'s `promptText`/`mintBankId` + bank verbs against `panel_bank_ops` - (one owner); no command-id string changed. Bank verbs reshaped to **promptless inner verbs** - (one mutation home, two UX skins — panel and actions each keep their exact prior UX); - `promptText` renamed `promptBankName`; `persistBankOp`/`persistBook` gain null-session guards. -- [x] `prune_action` verified a clean deletion-authority isolate (no `Undo_*`, no ext-state - writes). Command-id suffixes/display phrases verified byte-identical in review. -- [ ] Verify in DAW: all action families fire unchanged; one bank op = one Ctrl-Z; prune still - no-undo/no-ext-state; CTest green. — **PENDING**: in-DAW verification not yet performed on - `phase-q` (deferred by design). - -**Notes/decisions:** -- **Review 🟡 (resolved in Q-W6):** two session pointers / a null-session-as-model-rejection - misreport (unreachable today) — resolved by Q-W6's `bank_ops` lift. - ---- - -## Q-W5 — split `persist.cpp` (isolate the single file-deletion authority into `prune_fs`) (2026-07-29) - -> **Merged to `phase-q` 2026-07-29 (merge of `pq-w5-persist`). Integrated suite 61/61 green, -> reviewed-approved.** - -**Goal:** Split `persist.cpp` (852 LOC at the Q-W0 census — T4-04: seams unchanged; the -pS-usage growth landed exactly where this wave isolates it; 5 responsibilities) into `session` -(lifecycle+poll, `BeginLoadProjectState` reload hook), `ext_state_io` (the ext-state ↔ JSON -serialization bridge + GUID minting + folder relocation), and **`prune_fs`** (prune scanning + -`deleteOrphanFile` via `SHFileOperationW`). The split **concentrates** the byte-deleting -authority into one obvious module — it must never spread it. **Q-W0 rider (T2-04, SETTLED -2026-07-28):** generalize the `GetProjExtState` grow-loop retry policy into `bridge_marshal`'s -pure decode home (or its `core/` successor) and rewire all three hand-rolled copies — -`usage_scan`'s prune-safety-adjacent copy included. CONTEXT.md §Phase Q (persist split seams; -deletion-authority isolation). See `docs/product/code-organization.md` §2.1, §7. -**Verify:** CTest green at every commit. Session save/load/undo-reload, ext-state round-trip, -folder relocation, and prune deletion all behave identically in DAW. **File deletion lives in -exactly one module (`prune_fs`)** — the single-file-deletion-authority invariant is *improved* -(concentrated), never diluted. Relative-paths-only persistence is unchanged. -**Depends on:** Q-W1. Best after Q-W4 (so `prune_action` routes cleanly to `prune_fs`), but -independently landable. - -- [x] Split `persist.cpp` (853 LOC) → `session` (lifecycle/poll + `projectconfig` reload hook), - `ext_state_io` (serialization bridge + GUID minting + folder relocation), under `shell/persist/` - + `persist_internal.h`. -- [x] Isolate prune scanning + `deleteOrphanFile` (`SHFileOperationW`) → **`prune_fs`** — the - deletion authority concentrated in exactly one anonymous-namespace function in `prune_fs.cpp`, - verified tree-wide; the prune fail-safe chain stays byte-intact. -- [x] Dedupe the `GetProjExtState` grow-loop ×3 (T2-04): unified as a header-only template, all - three copies rewired (`usage_scan`'s start cap raised 4KB→64KB, allocation-only, verified - equivalent); the grow-loop gains a defensive NUL. -- [x] Rider: the Q-W1 `bank_book_json` residual lands via a private static `nameKey` - (Daniel-approved option a) — `bank_book.cpp` is now ~462 LOC. -- [ ] Verify in DAW: save/load/undo-reload/relocation/prune unchanged; deletion authority is one - module; relative-paths-only holds; CTest green. — **PENDING**: in-DAW verification not yet - performed on `phase-q` (deferred by design). - -**Notes/decisions:** -- `persist.h` is kept as a compat umbrella for parallel safety across the in-flight waves - (retired in Q-W6); deletion-authority wording is scoped precisely in headers. - ---- - -## Q-W6 — OCP registration-table + residual fat-header (I) splits (2026-07-29) - -> **Merged to `phase-q` 2026-07-29. Integrated suite 61/61 green, reviewed-approved.** - -**Goal:** Close the last SOLID wart: replace the ~350-line hand-written **non-table** action -registration blocks (now isolated in `app/main.cpp` after Q-W3) with a **registration table**, so -adding an action edits one place, not four parallel ones (OCP). Split any remaining fat headers -(`capture.h`/`persist.h`) not already resolved by their TU splits (I). (Q-W0: no reshape — -T4-02 notes the ~385-line registration residue left in `app/main.cpp` after Q-W3 shrinks -further under the table.) CONTEXT.md §Phase Q (OCP -registration-table). See `docs/product/code-organization.md` §2.3, §6 (Q-6). -**Verify:** CTest green at every commit. Every action still registers, appears in the Actions -list, and fires via `hookcommand` exactly as before; command-id + display strings unchanged -(FOREVER-STABLE, per-channel); unload still mirror-unregisters everything. Adding a hypothetical -new action now touches the table only (demonstrated in review, not shipped). Remaining fat -headers are segmented. -**Depends on:** Q-W3 (registration code must be isolated first). Sequenced last; the most -droppable point if the phase needs narrowing (Q-6). - -- [x] Converted the hand-written `Register("command_id"/"gaccel"/"hookcommand")` blocks to a - data-driven `ActionTableRow` registration table (flat function-pointer dispatch, no - `std::function`/virtual); unload mirror-unregisters from the same table; `main.cpp` shrinks - 653→404. Capture rows derive their suffix+phrase from the pure `captureActionTable()` (the - parallel-list risk is gone by construction). FOREVER-STABLE suffixes/phrases/retired-ids - verified byte-identical row-by-row in review. -- [x] Split residual fat headers: `persist.h` umbrella retired (13 callers repointed); - `capture.h`'s realtime seam moved to `capture_realtime_shell.h`; the `wav_trim.h` shim + its - INTERFACE target deleted. -- [x] Phase-end cleanup riders: `bankOp*` verbs + `persistBankOp` lifted to new `shell/bank_ops` - taking `ReaSamplerSession&` (dissolves the Q-W4 🟡 review note); **`core/namespaces.h` - DELETED** (the interim Q-W1 shim's contract fulfilled — ~26 includers rewired); the grow-loop - rehomed to `core/wire/ext_state_read.h`; a stale-comment sweep (`persist.cpp`/`bank_panel.cpp` - refs); CLAUDE.md's persist/bank_book/actions/wav_codec bullets corrected in-wave. -- [ ] Verify: all actions register/fire/unregister unchanged; command-id strings untouched; CTest - green. — **PENDING**: in-DAW verification not yet performed on `phase-q` (deferred by design). - -**Notes/decisions:** -- **Review-noted follow-on (not landed, deferred):** extending the table pattern to the - design_view/bank/ingest families' hand-registration; `channelIdFor`'s shared string-store scan - is correct-by-prefix-disjointness — a suffix-keyed map would make it structural, but isn't - required; `view_mode_model.h` (748 LOC) remains the largest header (T4-06's planner split - stays optional/deferred). diff --git a/CONTEXT-ARCHIVE.md b/CONTEXT-ARCHIVE.md deleted file mode 100644 index 3e41e15..0000000 --- a/CONTEXT-ARCHIVE.md +++ /dev/null @@ -1,1230 +0,0 @@ -# CONTEXT-ARCHIVE.md — ReaSampler spec provenance - -Build-detail sections moved verbatim out of `CONTEXT.md` once their phase -landed. Nothing here is condensed or rewritten — each section is the original -text, preserved for provenance. `CONTEXT.md` retains the settled decisions, -invariants, guardrails, and unbuilt specs that still constrain future work. - -Sections appear in their original `CONTEXT.md` document order. - ---- - -## Module architecture - -Preserve the scaffold's split: pure, REAPER-free logic in one set of files -(unit-tested outside the DAW via the existing `tests/` + CTest harness), REAPER- -facing shells in another. - -Pure (no REAPER types, fully unit-tested): -- `bank_model` — the `Sample` metadata struct and the `BankIndex` (add / remove / - query / tier moves / dedup-by-hash) plus JSON serialize/deserialize to a - `std::string`. This is the heart; test it hard. -- `peaks` — compute waveform min/max bins from raw PCM. Feed it a known signal - (sine, ramp) and assert the envelope. We compute our own thumbnails from the - captured file rather than depending on REAPER's peak API — we own the file - format, so this is simpler, testable, and dependency-free. - -REAPER-facing: -- `capture` — two CONCRETE backends, `OfflineRenderBackend` (synchronous) and - `RealtimeRecordBackend` (async begin/tick/abort); no shared interface (the - former `ICaptureBackend` was deleted in Q-W3 — T4-26: one deriver, zero - polymorphic call sites). Input: a `CaptureRequest` (capture scope — item or - track, time range, tail, SR/bit-depth/channels, output path). Output: a finished - file + a populated `Sample` handed to `bank_model`. Capture is always wet; the FX - *scope* (not a wet/dry dial) is the control — the pure `render_settings` module - maps a scope to its render-source bits and its FX-bypass plan (see CLAUDE.md §Precision invariants). -- `insert` — placement via `InsertMedia`; conform-to-project-tempo vs literal, - as an explicit flag (never silent stretching). -- `bank_panel` — the docked grid: LICE-drawn thumbnails, audition, multi-select, - keyboard navigation. Reuses the docking setup already in `mpe_view.cpp`. -- `persist` — project ext state <-> `bank_model` JSON; project-relative path - resolution (resolve bank folder from the current project path). -- `actions` — registers the capture/placement/slot action family and routes each - to the modules above (the `command_id` + `gaccel` + `hookcommand` pattern from - `main.cpp`). - -## Data model (sketch — refine in code) - -`Sample`: id, display name, relative file path, source mode, source range (start/ -end in project time + PPQ), track GUID(s) if applicable, wet/dry, channel count, -sample rate, length (seconds + musical/beats), capture tempo, optional key, -peak/RMS/LUFS, clip flag, tier (scratch | archive), content hash, provenance -(parent sample id + FX-chain snapshot string, when resampled from another sample), -created timestamp. - -`BankIndex`: ordered collection of `Sample`, keyed by id; hash lookup for dedup; -tier filtering; JSON round-trip. Scratch tier is auto-prunable; archive is kept. - -## REAPER API surface (verify all signatures) - -Offline render (the crux — prototype this first): -- Drive render settings with `GetSetProjectInfo` (`RENDER_BOUNDSFLAG`, - `RENDER_STARTPOS`, `RENDER_ENDPOS`, `RENDER_TAILFLAG`, `RENDER_TAILMS`, - `RENDER_SRATE`, `RENDER_CHANNELS`, `RENDER_SETTINGS`) and - `GetSetProjectInfo_String` (`RENDER_FILE`, `RENDER_PATTERN`, `RENDER_FORMAT`). - The `RENDER_SETTINGS` bit choice is driven by capture scope: **item** scope renders - selected media items (single-file), **track** scope renders selected tracks via - master. There is **no master-mix scope** (to capture the master, render a track). - Render is always wet — REAPER has no true pre-FX "dry" render bit; FX scoping is done - by the FX-bypass-around-render mechanism, not a render bit (see CLAUDE.md §Precision invariants). -- Trigger a no-dialog render via the appropriate render action / `RENDER_SETTINGS` - bit. Confirm the exact command id and the "render without opening dialog" flag - against current REAPER — do not assume; test that it runs headless. -- Determinism is a hard requirement: two identical requests must produce - bit-identical files (enables the null test below). - -Realtime record (**track scope** — item realtime is deferred): -- Taps the **selected track**, not the master. Recipe: create a hidden temp track, - route a **post-fader send from each selected source track into it** - (`CreateTrackSend`, default post-fader/full-stereo — captures each track's own - output *before* the parent/master sums it, so the tap is chain-independent by - construction and needs no FX bypass), set the temp's record mode to record-output - (latency-compensated, `B_MAINSEND=0` so it does not sum back), arm (`I_RECARM`), - `CSurf_OnRecord`, run for the range, `OnStopButtonEx`, then move the recorded file - into the bank and delete the temp track (which drops the sends — no source track is - left mutated). Verify `I_RECMODE` values for output-recording. -- Master→track sends are refused by REAPER as feedback loops — this is *why* the tap - is the selected track's post-fader output, not the master. - -Sources & metadata: -- Time selection: `GetSet_LoopTimeRange`. Razor edits: - `GetSetMediaTrackInfo_String(track, "P_RAZOREDITS", ...)`. Tempo: - `Master_GetTempo` / `TimeMap2_timeToBeats` / `GetProjectTimeSignature2`. - Selected items/tracks: `CountSelectedMediaItems` / `GetSelectedTrack`. - -Placement: -- `InsertMedia(path, mode)` at edit cursor / new track / replace selection - (verify mode bits). `SetEditCurPos`. Wrap edits in `Undo_BeginBlock2` / - `Undo_EndBlock2`. - -Persistence & paths: -- `SetProjExtState` / `GetProjExtState` (namespace e.g. `"reasampler"`) for the - index JSON. Resolve project folder via `EnumProjects` / `GetProjectPathEx`; - store the bank under a project-relative subfolder; keep only relative paths in - the index. - -## Build order (each milestone independently testable) - -1. `bank_model` + JSON round-trip + unit tests. (pure — no REAPER) -2. `peaks` + unit tests. (pure) -3. Offline capture of the time-selection master mix to a wav in the project bank - folder; add a `Sample`; log it to the console. (the render-driving spike) -4. `persist`: write the index to proj ext state, reload on project open; confirm - it survives Save / Save As. (bank travels with the .rpp) -5. `bank_panel`: docked grid with thumbnails, audition, selection. -6. `insert`: "insert selected sample at edit cursor" action via `InsertMedia`. -7. Capture action family: two FX scopes — **item** (item/take FX only) and **track** - (item FX + the selected track's own track FX), each chain-independent (the - out-of-scope chain is neutralized to unity for the render). No master scope — to - capture the master, render a track. Each scope captures over a **time-or-razor - range** (razor is a range source, not a mode: razor-when-present, else time - selection). Capture is always wet; the scope is the control. All registered as - bindable actions (tail off; a tail-on variant is a later opt-in). -8. `RealtimeRecordBackend` behind the same interface. -9. Slots: "capture to slot N" / "insert slot N", MIDI-bindable (MPC-style). -10. Provenance + "re-capture from source"; null-test verify action. -11. Polish: batch capture (per selected item / per razor area), resample-and-mute- - source, conform-on-insert, drag-out to OS. - ---- - -## Module architecture (preserve the pure/shell split) - -Pure (no REAPER types, unit-tested — the mirror of `bank_model`): -- `view_mode_model` — mode registry (id/name/ordinal; Arrange + Design seeded); - membership index (`track GUID → { mode ids }` + per-track show-both flag; add / - remove / retag / query); **folder-tree-aware** visibility derivation (given the - current parent↔child tree supplied by the shell + the active mode, compute the - visible set); the **parking/restore planner** (given active mode + snapshot - record, emit the exact (track, flag, value) operation lists for park and - restore — where the restore invariant is enforced); JSON round-trip of modes + - membership + show-both + snapshots + active mode. - -REAPER-facing: -- `view` shell — reads `I_FOLDERDEPTH` across the track list to build the - parent↔child tree and feeds it to `view_mode_model`; applies the planner's operations - via `SetMediaTrackInfo_Value` (`B_SHOWINTCP` / `B_SHOWINMIXER` / `B_MAINSEND` / - `I_FXEN`) and `TrackFX_GetCount` + per-FX `TrackFX_SetOffline`; snapshots prior - flag values before parking; resolves GUIDs via - `GetTrackGUID` / `guidToString` / `stringToGuid`. Never touches master - visibility, never touches `B_MUTE` / `I_SOLO`. -- `persist` (slice) — serialize/deserialize the view section into the - `"reasampler"` namespace alongside the bank; on project open, rebuild the tree - and reapply the active mode. -- `actions` (entries) — toggle active mode; activate mode: Arrange / Design; - tag/untag selected tracks → mode; show-both for selected tracks. Registered with - the `command_id` / `gaccel` / `hookcommand` pattern; toggle + mode-jumps - MIDI-bindable. -- UI (in the ReaSampler / bank_panel window) — a **segmented mode switch** - (`[ Arrange | Design ]`) in the window header, active segment lit; small per-mode - membership count; the offlined-FX caveat as a tooltip. Tag/untag acts on the - current REAPER track selection, not a per-track widget. - -## REAPER API surface (verify all signatures) - -- Visibility/routing/FX flags via `GetMediaTrackInfo_Value` (snapshot) / - `SetMediaTrackInfo_Value` (apply): `B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`, - `I_FXEN`. (Note: brief cited `B_SHOWINMCP`; verified SDK name is `B_SHOWINMIXER`.) -- Per-FX offline: `TrackFX_GetCount` + `TrackFX_SetOffline(track, fx, offline)`. -- Folder tree: read `I_FOLDERDEPTH` per track to derive parent↔child structure. -- GUID keying: `GetTrackGUID`, `guidToString`, `stringToGuid`. -- Persistence: `SetProjExtState` / `GetProjExtState` under `"reasampler"` (shared - with the bank index — one blob, two logical sections). -- Wrap flag mutations in `Undo_BeginBlock2` / `Undo_EndBlock2` as appropriate. - ---- - -### Module architecture (preserve the pure/shell split) - -- **Pure (`view_mode_model` extension).** Lane math — which lane maps to which mode, - which `C_LANEPLAYS` value per mode, the item-lane op family alongside the existing - track-flag op family — is **REAPER-free and unit-tested**, mirroring the D1 - planner. The **lane-ownership index** (per (track GUID, lane): managed-which-mode vs - manual) and the **"which lanes may this toggle touch" query** (managed only) are - pure and unit-tested — the planner emits lane ops for managed lanes only and never - for manual lanes. The **auto-tag decision** is pure too: given *a set of new - track GUIDs + active mode* (active-mode rule) and *a set of new items, each - carrying the set of modes already present on its track* (`NewItem::trackModes`) - — plus the manual-lane exemption flag — `autoTagNewContent` produces the - membership writes. The adoption guard runs inside the pure layer: single prior - mode → adopt it; empty / multi-mode track, or new track → fall back to active mode. -- **Shell.** Two shell responsibilities. (1) **Apply** the planner's item-lane ops - (`I_FREEMODE`/`I_FIXEDLANE`/`C_LANEPLAYS`/`B_FIXEDLANE_HIDDEN` via the media-item - info setters, `UpdateTimeline()` after `I_FREEMODE` changes) — for managed lanes - only. (2) **Detect** new content and read live lane state — see below. -- **Persistence.** The tool persists which lane maps to which mode **and the - managed/manual ownership index** (a small addition to the `"reasampler"` view - section); REAPER stores fixed lanes and lane-plays in the `.rpp` natively. - -### New-content detection (implementation design point) - -REAPER exposes **no clean "item added" / "track added" event callback.** Auto-tagging -therefore requires the shell to **diff project state on the panel's existing timer** — -the `bank_panel` already polls and fingerprints the bank; this extends that machinery -to the timeline's tracks and items. - -- Each poll, compare the live track/item GUID set against the previous poll's set. - New **track** GUIDs are tagged to the then-active mode. New **item** GUIDs are - passed to `autoTagNewContent` with the pre-existing mode-span of their track - (`NewItem::trackModes`): single-mode track → adopt that mode; empty / multi-mode - track → fall back to the then-active mode. Manual-lane items are excluded. -- Correctness the implementation must handle: the **first poll after project open must - not mass-tag** pre-existing content (pre-existing defaults to Arrange, per the - membership rule). -- **Manual-lane exemption (design point).** An item added to a **manual** lane is - **not** auto-tagged — auto-tag governs normal timeline content, not lanes the user - hand-manages. Distinguishing the two may need a heuristic at detection (e.g., an item - whose `I_FIXEDLANE` is marked manual in the ownership index is exempt; content - outside any managed lane on a mode-managed track is subject to the adoption rule). The - precise rule is an **open implementation design point**; the settled boundary is that - manual-lane content is off-limits to auto-tag. -- **Lane-identity fragility (design point).** `I_FIXEDLANE` is the lane's identity and - is how the ownership index keys to a lane. Whether the index survives lane - reorder/renumber/deletion without going stale is an **implementation design point** - (same class as GUID-keyed reorder-safety for tracks) — flag, don't solve here. -- Pure/shell seam: the **detection** (diffing REAPER's live set each tick, reading live - lane ownership and per-item `trackModes`) is shell; the **tagging decision** and the - **managed/manual lane query** (new track GUIDs + active mode; new item GUIDs + - `trackModes` + active mode + manual-lane exemption ⇒ membership + lane writes) are - pure and unit-tested. - -### REAPER API surface (verify all signatures) - -- Fixed lanes — track: `I_FREEMODE` (=2), `I_NUMFIXEDLANES`, `C_LANEPLAYS:N` via - `GetMediaTrackInfo_Value`/`SetMediaTrackInfo_Value`; item: `I_FIXEDLANE`, - `C_LANEPLAYS`, `B_FIXEDLANE_HIDDEN` via `GetMediaItemInfo_Value` / - `SetMediaItemInfo_Value`. Call `UpdateTimeline()` after `I_FREEMODE` changes. -- Detection reuses the `bank_panel` timer + GUID fingerprinting already in place; - item GUIDs via the item's `GUID` (`GetSetMediaItemInfo_String` `"GUID"`), track - GUIDs via `GetTrackGUID` as in D1. -- **Verify every name/signature against the SDK header before use** — the surface is - verified *present*, but confirm argument order, types, and flag values. - ---- - -## Module architecture (preserve the pure/shell split) - -Pure (no REAPER types, unit-tested — the mirror of `bank_model` / `view_mode_model`): -- `bank_book` — ordered bank registry (`{ bank id, display name, ordinal, - BankIndex }`); pool seeded with fixed id + name; create / rename / reorder / - delete named banks (pool-privilege rules enforced here: reject delete/rename of - pool; delete drops member index entries; **display names unique** — create/rename - reject a name that duplicates another bank's, trimmed + case-insensitive, "Pool" - protected); **evacuate** a bank (move every member to - the pool, index-only, destination-collapse observed; pool cannot be evacuated); - active-bank id (get/set, defaults to pool); **move** and **copy** a sample between - banks (index-only, destination-collapse observed); query a bank's index; JSON - round-trip of the whole book (pool-as-bank-zero inside the blob + named banks + - per-bank indices + active id + ordinals) and legacy-`bank_index`→pool migration on - parse (one-way; blob authoritative thereafter). - -REAPER-facing: -- `persist` (slice) — serialize/deserialize the book under the `banks` key in - `"reasampler"` (pool-as-bank-zero inside the blob; no separate `bank_index` key - going forward); migrate a legacy `bank_index` key into the pool on first load - (one-way; blob authoritative thereafter, legacy key retired); reload-on-open and - Save-As survival ride the existing M4 machinery. The session exposes the book the - way it exposes the bank today; the active bank's `BankIndex` is what the capture - layer adds to. -- `bank_panel` (extension) — the vertical split: pool grid on top, named-banks - tab-page region below; two full-height toggles; the active-bank indicator; the - create / rename / delete / activate affordances. The named-banks tab strip is - **LICE-drawn** to match the M5 grid and the Design View segmented switch (not a - SWELL-native tab control), with an **overflow/scroll affordance** so it scales past - the ~8–12-tab point. Sample move ships **both ways**: a "move to bank" menu on the - current selection (the bindable front-end for the B3 move action) and - drag-between-regions (the direct-manipulation accelerator); copy is the deliberate - secondary act, offered on the menu. Drag carries clear drop-target highlighting on - the destination region/tab, and a mis-drop is recoverable by design (move is - index-only and reversible — the user moves the sample back). Reuses the existing - LICE grid render loop per bank region. -- `actions` (entries) — create bank / rename bank / delete bank (confirm on - non-empty delete); evacuate bank → pool; activate bank (direct + cycle); move - selected samples → bank; copy selected samples → bank; pool/banks full-height - toggles. Registered with the `command_id` / `gaccel` / `hookcommand` pattern; - bank-activate + move/copy + evacuate MIDI-bindable to suit the capture-heavy - workflow. - -## REAPER API surface (verify all signatures) - -No new REAPER API is invented at spec stage — the multi-bank layer is pure model + -persistence + panel UI over machinery M0–M6 already established. Shells will need to -verify against the SDK header where they extend existing surfaces: -- **Persistence:** `SetProjExtState` / `GetProjExtState` under `"reasampler"`, new - key `banks` (shared blob machinery from M4 — no new API, new key only). -- **Panel UI:** the docked-window + LICE-grid surface from M5 (`bank_panel`), - extended to two grid regions + a **LICE-drawn** tab strip (with overflow/scroll) + - toggles. The tab strip, the toggle affordances, and the drag hit-testing are - custom-drawn on the M5 LICE surface (not SWELL-native tabs); the "move to bank" menu - uses a SWELL popup-menu surface. **Verify LICE drawing and any SWELL menu/drag - hit-test usage against the M5 reference / SWELL headers**, and confirm the drag - hit-test does not collide with the M5 grid's multi-select drag. No new REAPER audio - API involved. -- **Actions:** the `command_id` / `gaccel` / `hookcommand` contract from `main.cpp` - (unchanged), new command-id strings under the sampler family prefix. - -## Open questions to resolve during build - -Forks 1–5 are all settled (see product notes → *Settled forks* and *Fork 5 — settled*, -and the settled-decision prose above). One panel-polish detail remains open. - -- **Fork 5 — tab rendering + move affordance (B4). Settled (2026-07-23).** (5a) The - named-banks region is **LICE-drawn** to match the M5 grid and the Design View - segmented switch — not a SWELL-native tab control — with an **overflow/scroll - affordance in scope from the start** so the strip scales past the ~8–12-tab - breakdown. (5b) Move ships as **both** a "move to bank" menu (the precise, - MIDI-bindable front-end for the B3 move action) **and** drag-between-regions (the - direct-manipulation accelerator); copy stays the deliberate secondary act via the - menu. Drag mis-drop is mitigated by drop-target highlighting and is recoverable by - design (move is index-only and reversible). Folded into the `bank_panel` prose and - the API surface below. **Verify LICE tab drawing and any SWELL menu/drag hit-test - surface against the M5 reference / SWELL headers before use** (confirm no collision - with the M5 grid's multi-select drag). Analysis in product notes → *Fork 5*. -- **Active-bank indicator placement (B4 polish)** — per-region headers vs. a single - header readout vs. lit-tab treatment. The "visually unmistakable" requirement is - settled (fork 4); only the placement is open. Panel-polish detail. - ---- - -## Module architecture (preserve the pure/shell split) - -- `bank_book` / `BankIndex` (pure) — expose remove of a `Sample` from a bank's - index (the existing `BankIndex::remove` primitive, surfaced through the book); - pool contents removable, pool-container privileges unchanged. -- `actions` (entry) — "remove selected sample(s) from bank" (and, under fork R-A, - a scope parameter); registered with the `command_id`/`gaccel`/`hookcommand` - contract; MIDI-bindable to suit the capture-heavy workflow. -- `bank_panel` (affordance) — remove on the current selection (menu entry / key), - reusing the M5 selection model exactly as move/copy do; confirm-on-last-reference - at this layer. - -## REAPER API surface - -No new REAPER API. Pure model + a new action command-id string under the sampler -family prefix + a panel affordance on the existing M5 LICE surface. Verify the -command-id/gaccel/hookcommand usage against `main.cpp` (unchanged contract). - ---- - -## Module architecture (preserve the pure/shell split) - -Pure (no REAPER types, unit-tested — the mirror of `reconcile`): -- **Prune-reconcile core** — given `{ files present in the bank folder }`, - `{ files referenced by the book }`, and `{ files the book owns }` (the owned-file - manifest, R-D), compute the orphan set `(owned ∩ present) − referenced`. - REAPER-free, filesystem-free, unit-tested hard (the prune null test lives here). - The referenced-set is unioned across all banks by asking the `bank_book`. - -REAPER-facing / filesystem-facing (thin): -- `persist` / session — supplies the referenced-set (union across the book) and the - owned-file manifest (R-D, written from capture onward in Phase B); resolves the - current project bank folder via the M4 project-relative machinery. -- A **prune shell** — enumerates the bank folder (filesystem I/O), feeds the - pure core, presents the dry-run manifest, and on confirmation deletes the orphan - set (via OS trash where portably available — fork R-C — else unlink). Filesystem - I/O only; the decision stays in the pure core. -- `actions` (entry) — "Prune bank folder" (dry-run-first, confirm-to-delete), - registered with the `command_id`/`gaccel`/`hookcommand` contract; **plus a - `bank_panel` button** (R-E) that fires the same action. - -## REAPER / platform API surface (verify all signatures) - -No new REAPER *audio* API. New surfaces to verify before use: -- **Filesystem enumeration + delete** — directory listing and file removal for the - project bank folder. **Verify** the portable approach against SWELL / the existing - file-handling in `persist` / `capture` (which already resolve and write files); - prefer reusing whatever path/file machinery M4 established. -- **Move-to-trash (fork R-C, settled trash-preferred)** — verify a portable - move-to-trash exists (SWELL, or per-platform: Win `IFileOperation`/ - `SHFileOperation`, macOS `NSFileManager trashItemAtURL:`, Linux XDG trash spec). - This is a **must-verify per platform** before use, not an assumed capability; - where it is unavailable, fall back to unlink behind the dry-run/confirm guardrail. - (R3 verified: Windows routes to Recycle Bin via `SHFileOperationW` + `FOF_ALLOWUNDO`, - verified against SDK 10.0.26100. macOS / Linux: no portable SWELL trash surface - found — fall back to `unlink` behind the dry-run/confirm guardrail, as specified.) -- **Owned-file manifest persistence (fork R-D, settled)** — a new tracked set in - the `"reasampler"` ext-state (a sibling key or folded into the `banks` blob — - build-time residual); shared M4 blob machinery, new data only. **Written from - capture onward in Phase B** (the seam lands early), consumed by prune in Phase R. -- **Actions** — the `command_id`/`gaccel`/`hookcommand` contract from `main.cpp` - (unchanged), a new command-id string under the sampler family prefix. - ---- - -## Module architecture (preserve the pure/shell split — in the new artifact) - -Pure (no REAPER types, no VST3 types, unit-tested — the mirror of `bank_model`): -- **Sampler core** — voice allocation/polyphony, amplitude envelope (ADSR), key→sample - and velocity→sample mapping (the keymap), repitch/interpolation from root note, and - keymap resolution. REAPER-free *and* VST3-free, unit-tested in CTest against known - signals (mirror of how `peaks` asserts an envelope). This is D3's pure core and the - heart of the phase. - -Shell (VST3-facing / REAPER-facing, thin): -- **VST3 wrapper** — `SingleComponentEffect` subclass: `initialize` (declare an event - input bus + an audio output bus, no audio input), `setupProcessing`, `setActive`, - `setState`/`getState`, and the hot-path `process` that reads MIDI off the event bus, - drives the pure core, and writes the core's per-voice audio to the output bus. Plus the - module factory (`GetPluginFactory` + Windows `InitDll`/`ExitDll` — verify exact export - names at the spike). -- **`IPlugView` LICE editor** — hosts a LICE-drawn surface in the VST3 view seat - (window creation/sizing, host→draw/hit-test event routing). Reuses the `bank_panel` - LICE/SWELL competence and house style. -- **Bridge/state reader** — resolves `GetProjExtState`/`EnumProjExtState` by name over - the host callback, fetches the host project context, reads the live `"reasampler"` - ext-state (bank index + intrinsic fields + performance map), and resolves WAV audio - paths the same project-relative way `persist` does. - ---- - -### WDL pitch/resample surface — corrected finding (feeds S16, not a committed point) - -**Corrected 2026-07-26 (Daniel's duration-preserving directive).** The prior sweep dismissed -`WDL_SimplePitchShifter` as "wrong tool (duration-preserving)". Under the directive, -**duration-preserving is the requirement**, so that header is the Preserve-engine candidate, -not a mismatch — a real viability assessment replaces the dismissal. - -The **full** vendored WDL pitch/resample surface is `vendor/WDL/WDL/resample.h` and -`vendor/WDL/WDL/simple_pitchshift.h` — the **only** two pitch/resample headers; there is -**no** elastique / formant-preserving anywhere in the tree. Honest findings: - -- **`WDL_Resampler` (`resample.h`) — sinc/linear resampler, RT-suitable.** - `SetMode(interp, filtercnt, sinc, sinc_size≤64, sinc_interpsize)`; streaming - `ResamplePrepare`/`ResampleOut` with `Prealloc`. Its sinc mode beats the core's 2-point - linear interp for **Varispeed** base-repitch quality (less aliasing on large transpositions) - at a real CPU cost. **A resampler couples duration** → a Varispeed-quality option, **not a - Preserve engine.** Held as a Tier-2/3 Varispeed-quality toggle; not committed. -- **`WDL_SimplePitchShifter` (`simple_pitchshift.h`) — time-domain OLA, duration-preserving — - the S16 Preserve-engine candidate (fork S16-F2 route a).** Viability from the header: - - **API shape:** push/pull, block-based. `GetBuffer(size)` returns an input buffer to fill; - `BufferDone(filled)` runs the OLA shift and queues output; `GetSamples(req, buf)` pulls - from the queue. Config: `set_srate`, `set_nch`, **`set_shift(ratio)` (pitch, duration- - preserving)**, `set_tempo(scale)` (an *independent* duration knob — Preserve uses - `set_tempo(1.0)`), `SetQualityParameter(q)` (selects window/overlap ms from a fixed table). - - **Per-voice instantiability / memory:** modest. `m_psbuf` is an OLA ring of `bsize·nch` - where `bsize = window_ms · 0.001 · srate` (≈ 2205 frames at 50 ms / 44.1 kHz ≈ a few - KB/voice), plus `m_inbuf` (one input block) and a bounded `m_queue`. `m_rsbuf` allocates - only when `set_tempo ≠ 1` (unused in Preserve). One instance per voice is cheap in memory. - - **RT-safety:** allocations occur in `BufferDone` — `m_psbuf.Resize` (once, when - `bsize·nch` first sets, at a fixed quality/srate/nch — pre-warmable) and `m_queue.Add` - (grows only until the push/pull cadence reaches steady state). **Pre-warm at voice- - allocation** (run silence through once so `m_psbuf` sizes and `m_queue` settles); after - that no `process`-thread allocation. No locks. **RT-viable with the pre-warm discipline.** - - **Latency:** inherent ~half-window (initial `m_pspos = bsize/2` → ~25 ms @ 50 ms window) - plus fill-up — a **real note-onset lag**. This is the load-bearing cost. Mitigation: - pre-warm; and Varispeed (zero-latency) serves the tight-transient one-shot material, so the - lag lands on sustained/loop material where least harmful. Smaller-window quality settings - (the table goes to 3–10 ms) trade latency for more warble. - - **Quality:** basic — this is REAPER's "SimpleWindowed" mode. Audible warble on large - transpositions; **`set_formant_shift` is an explicit empty stub** → no formant preservation. - Usable for loop/phrase Preserve; replaceable by route (b) if not. - - **CPU / polyphony:** `PitchShiftBlock` is O(length) per block — a few mults + one OLA - crossfade branch per frame, **no FFT**. Per-voice cost is modest; **N polyphonic voices - each running one is feasible** within RT discipline. If the aggregate cost is material, a - **Preserve-mode-specific voice cap** (below the Varispeed cap) is the pressure valve — - flagged in Verify, set from measured per-voice budget at build. -- **Formant-preserving / studio-grade time-stretch (elastique-class): NOT in WDL, confirmed.** - REAPER's elastique is **licensed (zplane)**, not in the vendored tree (grep found only - unrelated libpng/giflib string matches). Formant-correct duration-preserving repitch is - **unavailable without a new third-party dependency** (JUCE / rubberband / signalsmith each a - new-dependency fork with D-A weight — not proposed). Stated, not worked around. -- **Recommendation:** the **Preserve** engine (S16-F2) is `WDL_SimplePitchShifter` (route a, - low-cost proof) or a hand-rolled pure `pitch_shift` module (route b, held quality upgrade). - The **pitch-envelope** modulation stays hand-rolled over whichever engine (a per-frame - `ratio_` multiply under Varispeed, a per-frame shift-amount add under Preserve). - `WDL_Resampler` (sinc) is a held **Varispeed-quality** upgrade only. - ---- - -### Sequencing (S15/S16 against S7 stereo, S10 editor) - -S15 and S16 are **S3-core extensions** — they touch the engine Daniel smoke-tests, like S7. -They are **channel-count-agnostic by construction**: the play-mode envelope is a per-frame -**amplitude** function, and both pitch engines carry the channel dimension internally — the -**Varispeed** path is a per-frame per-channel read-rate scalar, and the **Preserve** shifter -is **`set_nch`-aware** (one shifter instance per voice transposes all its channels together). -So S15/S16 **compose cleanly with S7's channel dimension** rather than conflicting: S7 adds a -channel axis to the read/mix; S15 adds an amplitude-shape axis; S16 adds a pitch-engine + -read-rate axis; all orthogonal. **Recommended order:** **S15 before S16** (S16 reuses S15's -per-voice param-plumbing + component-state version bumps; landing S15's `PlayMode`/param -struct first gives S16 a home to hang the pitch-engine mode + pitch-env params on). **S16 is -now meaningfully heavier than the prior "just an envelope" framing** — the Preserve engine is -a per-voice DSP object with its own RT budget, pre-warm, and possible voice-cap; treat S16's -Preserve-engine point as the phase's next real DSP spike, not a thin add-on. **S15/S16 -relative to S7:** no hard dependency — spec them so the envelope/mode code never assumes a -channel count (it operates per-frame, pre-mix; the Preserve shifter is `set_nch`-driven), and -S7 can land before, after, or interleaved. **Relative to S10 (editor):** S15's mode toggle + -Trigger handles and S16's AD control **surface through** the S10/S11 waveform + guided-setup -work, so the *core* halves of S15/S16 can land independently of the editor, with the editor -surfacing following S10/S11 (the same way S12's ADSR editor follows the S3 ADSR math). Land -the **core** engine work (mode split, start point, %-length/fades, pitch-env modulation) as -soon as it is ready — it is testable in CTest without the editor — and wire the UI as the -S10/S11 surfaces mature. **Land S15/S16 core after S10's policy-reversal is settled** only if -sharing the same component-state blob would otherwise churn the version tag twice; otherwise -they are independent. - ---- - -## ReaSampler 9000 — the UX overhaul (S10–S13; DAW-tested S1–S6, "the UX is awful") - -**The bar is set: better than ReaSamplOMatic5000.** Daniel DAW-tested the S1–S6 -instrument and the verdict was that it *works* but the UX is unacceptable — "this is -supposed to be better than ReaSamplOMatic5000." The S1–S6 editor was a spike-grade LICE -panel: a clickable sample list, zone rows each carrying **seven tiny ±1 nudge/delete -mini-buttons** (low-/low+/high-/high+/root-/root+/delete), text-only labels, **no keyboard -visualization, no waveform, no drag interaction of any kind, no scrolling** for long lists. -Setting a zone from C1 to C4 by clicking "+" thirty-six times is the catastrophe; the rest -(no way to *see* a sample, no loop editing by eye, unreachable rows past the panel bottom, -a fixed envelope) compound it. The overhaul is scoped as **S10–S13**, sequenced so the -friction Daniel feels every test pass is removed first. - -### Workflow hierarchy (REVISED 2026-07-26 — Daniel; supersedes the keymap-first S10) - -The overhaul is reframed around the **actual workflow**, not a keymap. Daniel's directive, -distilled: *a giant list of "item" blocks is visually useless; optimize for working with -individual captures, not a huge list of everything.* The settled hierarchy: - -1. **Primary flow = one capture, fast.** Most instances play a **single capture**. The - metric is **time-to-first-note**: open → pick a capture → see it (waveform/peaks) → play - it. The default editor face serves this, not a zone table. -2. **Fresh instance is SILENT — nothing auto-selected (policy reversal of S4).** On open - with no stored selection, the instrument plays **nothing** and shows a clear **empty - state** ("pick a capture") — it does **not** auto-play sample #1. This deliberately - reverses the S4 "first sample plays" convenience: the `selectSample` first-sample - fallback and the processor's Tier-0 fallback that resolved it are removed; an empty - stored id resolves to silence. (Recorded as a reversal, not a regression.) -3. **Capture browser, not an item list.** Scannable **cards/rows** with **peak thumbnails** - (the `Sample` peaks bank_model already carries — the same data the dock panel thumbnails - draw), name, and a **root/key badge** where present; **filterable by bank** (bank_book - named banks). A "giant list of item blocks" is the anti-pattern — the browser is designed - for scanning by eye. -4. **Graphic, descriptive controls with a guided fast path.** Once a capture is picked, a - prominent, self-explanatory single-capture setup surface (root note, play-mode basics, - level). The keyboard strip serves the **single-capture** case first (shows where the - capture sits / its root); drag matters most when zoning. -5. **Zones demoted to secondary (nice-to-have).** Multi-zone keymap editing becomes an - **opt-in "Zones" panel** (S10-Z), not the default face — "most of the time the zones - won't be used." The keyboard-strip drag machinery is still built, but in service of the - capture-first layout. - -**What "better than RS5K" means, specifically (not vibes).** RS5K's genuine strengths — -match or beat each: (1) **drag a file straight onto it** loads the sample (our S13 relay); -(2) **note-start / note-end** range with a visual sense of the keyboard (our S10 keyboard -strip — RS5K's own range UI is two number fields, so a *draggable* strip beats it); (3) a -**waveform** with draggable start/end/loop markers (our S11); (4) **ADSR** sliders (our -S12); (5) velocity layers / round-robin (Tier 2 — held, not in this overhaul). RS5K's real -**weaknesses are our opening:** its **one-sample-per-instance** model forces track sprawl -(one RS5K per drum) and it has **no multi-zone view in a single instance** — ReaSampler -9000 is multi-zone in one instrument by design (S5), so the **opt-in Zones panel** showing -*all* zones at once is a capability RS5K structurally lacks. But per the reframe, the *default* -face is the single-capture fast path (browser + setup), and multi-zone is the demoted -nice-to-have. "Better than RS5K" = a fast single-capture browser where RS5K makes you drag a -file blind, direct-manipulation where RS5K uses number fields, multi-zone-when-you-want-it -where RS5K is one-shot, and bank-integrated ingest where RS5K is file-at-a-time. - -**Constraints (unchanged — settled, do not re-open):** LICE/SWELL drawing only (no toolkit -change — D-A settled); **all layout/hit-test math in pure geometry modules** (mirror of -`mode_switch` / `editor_geometry` / `embed_strip`), the draw + drag-state machine in the -shell; RT discipline untouched (every edit commits **off** the audio thread via the -existing `commitAndReload` → off-thread `reloadFromBank` → atomic swap); the instrument -stays a **read-only bank consumer** (loop/root/ADSR edits are the instrument's *performance -map*, D-B — never written back to the bank); component-state persistence and -read-only-over-bank stay settled. - -- **S10 — capture-first editor: browser + guided single-capture setup (REVISED 2026-07-26).** - The default face is the **capture browser** (scannable cards with **peak thumbnails** from - the `Sample` peaks bank_model carries, name, root/key badge; **bank filter** over bank_book - banks) feeding a **guided single-capture setup** (root note, play-mode basics, level). - Fresh instance is **silent, nothing auto-selected** — the S4 first-sample fallback is - **removed** (empty stored id → silence + a "pick a capture" empty state). New pure modules: - `capture_browser` (card/grid layout + hit-test) and `keyboard_strip` (key-span↔pixel via - the `embed_strip` idiom; a **root marker** for the single loaded capture; `pixel→note`; - drag-delta resolver; per-zone bar rect + edge-grab hit regions for the opt-in Zones panel). - Shell extends the click-only `wndProc` to a `WM_MOUSEMOVE`/`WM_LBUTTONUP` drag-state - machine with live feedback, one coherent edit on release. **Multi-zone keymap editing is an - opt-in "Zones" panel (S10-Z), not the default** — the demoted nice-to-have; it reuses the - same strip geometry + drag machine (edge = resize, body = move, key = root) and retires the - seven ±1 nudge buttons per row. **Built with the current LICE drawing; adopts the Phase L - kit (L1) when it lands** (drawn through the shared component kit rather than flat - `LICE_FillRect`/GDI once available) — **not gated on Phase L**; the drag machine's - `WM_MOUSEMOVE` tracking also lights the kit's hover states at near-zero marginal cost once - the kit is present. **Boundary shifts (from the reframe):** the "sample list" S12 was to - scroll/search **is now this browser** — the card layout, peak thumbnails, and bank filter - are S10's; S12 keeps **scroll** + **type-to-filter search** *layered over* S10's browser - (bank filter picks the bank, search narrows within it). The waveform S11 makes loop-editable - is the same waveform S10 shows read-only for the picked single capture ("see it"). -- **S11 — waveform view + draggable loop points.** Selecting a zone shows its sample's - **waveform** (peaks via the existing `peaks` module over the shell's already-decoded PCM - — no new decode/WAV path) with draggable **start/end/loop-start/loop-end** markers that - **snap to zero-crossings** (the S2 zero-crossing-aware requirement). A dragged loop is a - **per-zone loop override** (additive on `PerformanceZone`, same shape as `rootOverride`; - seeded from the S2 bank intrinsic, never written back). Marker/waveform geometry pure - (`frame↔pixel`, marker grab regions, clamp start≤end, zero-crossing snap helper). -- **S12 — scale + ergonomics.** **Scroll** (wheel + scrollbar) over **S10's capture - browser** so a bank longer than the panel is fully reachable, and a **type-to-filter - search** that narrows the cards by name, **composing with S10's bank filter** (bank filter - selects the bank; search narrows within it). *(Boundary shift from the 2026-07-26 reframe: - the browser card layout, peak thumbnails, and bank filter are now **S10's**; S12 = scroll - + search layered over that browser.)* **Direct numeric entry** for zone low/high/root (a - click-to-type field over the strip, for precision the drag can't hit — Zones-panel-scoped). - An **ADSR editor** — four draggable controls over the S3 `AdsrParams` (the math already - exists and is wired into the voice engine; today the envelope is a fixed default). - Scroll/search/slider/entry layout pure; ADSR + (implicitly) any exposed parameters become - per-instance component state (additive, version-bumped, back-compat). -- **S13 — drop-to-load (the S8 relay, in the editor).** Dropping an OS file / media item - **onto the editor window** ingests into the bank + assigns to this instance — the RS5K - "drop a file straight on it" affordance. **The instrument does not ingest:** the editor's - drop handler **relays a bank-ingest request to the extension** (S8's `option 1`), which - performs the capture/import + assign; refresh is hands-free via S9 (or a direct reload - without it). **Cross-artifact relay is the S8-flagged spike** — proven-and-shipped or - degrade to the docked-`bank_panel` drop path with a clear affordance. Never inserts a - timeline item (capture/placement separation intact). - -**Sequencing (recommendation, argued below in this section's tail).** S10 first — under the -reframe it now carries the **whole felt win**: the empty-state / no-auto-select fix, the -capture browser (peak thumbnails, bank filter) that replaces the useless item list, and the -guided single-capture setup that retires the nudge buttons. This is the entire "the UX is -awful" wound, and time-to-first-note is the metric it moves. S11 (waveform + loop) and S12 -(scroll/search over the browser, numeric entry, ADSR) follow — both lean on S10's browser + -drag machine, and S11's waveform is the same surface S10 shows for the picked capture. S13 -depends on S8's ingest seam, so it sequences after S8. Against the queued engine work: **S10 -should land before or interleaved with S7 (stereo).** S7 is a real engine capability (stereo capture in true -stereo) and touches the DSP Daniel smoke-tests — but the *reason* he'll keep smoke-testing -is the editor, and today every test pass is taxed by the nudge-button UX. Fixing what he -feels first (S10) makes every subsequent S7 test less painful; there is no hard dependency -either way (S7 is engine/bus, S10 is editor/geometry — orthogonal). Honest counter: if the -stereo *sound* is the thing blocking real use, S7 first is defensible — but "it works, the -UX is awful" points at the editor as the live wound, so **S10 leads.** - ---- - -## REAPER / Steinberg API surface (verify all signatures) - -- **VST3 SDK (a new vendored dependency — vendor it at the spike).** `FUnknown` and the - `IComponent` / `IAudioProcessor` / `IEditController` interface family; the - `SingleComponentEffect` / `EditControllerEx1` / `AudioEffect` base classes; the class - factory (`GetPluginFactory` + factory macros); `IPlugView` for the editor; - `ProcessData` / `ProcessSetup` for the hot path. **Verify** interface members, the - base-class overrides, factory-macro spellings, and the Windows module-export symbol - names (`InitDll`/`ExitDll`/`GetPluginFactory`) against the vendored SDK at the spike — - the framing doc flags several of these as experienced estimates. -- **REAPER VST-host bridge.** `hostcb` opcode `0xdeadf00d` (resolve API function by - name) and `0xdeadf00e` (host context); the by-name resolution of - `GetProjExtState`/`SetProjExtState`/`EnumProjExtState`. **Verify** against - `vendor/reaper-sdk/sdk/reaper_plugin.h` + `video_processor.h` + - `reaper_plugin_functions.h`. -- **Embedded UI (D-D, later point).** `IReaperUIEmbedInterface` and the embed - message/lifecycle contract — verify against - `vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h` before use. -- **VST3 bus arrangement (S7 channel mode).** `setBusArrangements` / - `getBusArrangement` and REAPER's mono/stereo instrument-bus expectations — verify against - the vendored Steinberg SDK + `reaper_vst3_interfaces.h`. -- **Ingest surfaces (S8).** `InsertMedia` is the placement path (untouched by ingest); - `CountSelectedMediaItems` / `GetSelectedMediaItem` + `GetSet_LoopTimeRange` are the - arrange-capture inputs (already the capture path's); `OpenMediaExplorer` + - `MediaExplorerGetLastPlayedFileInfo` are the *whole* Media-Explorer contract (thin — no - enumerate-selected, no ME-drop-handler). Drop handling is SWELL/Win32 on ReaSampler's own - panel HWNDs — REAPER exposes **no** drag-drop registration API. All verified against - `reaper_plugin_functions.h`. -- **Bank-generation seam (S9).** New forever-stable `ext_keys.h` key for the generation - counter; read over the same bridge `GetProjExtState` path S4 already uses. No new API — - confirm no torn-read hazard on the integer key. -- **WDL pitch/resample (S15/S16).** **Verified this pass:** `vendor/WDL/WDL/resample.h` - (`WDL_Resampler` — sinc/linear resampler, couples duration → **Varispeed** path) and - `vendor/WDL/WDL/simple_pitchshift.h` (`WDL_SimplePitchShifter` — time-domain OLA, - **duration-preserving** → the S16 **Preserve**-engine candidate, fork S16-F2 route a) are - the whole pitch/resample surface; **no** elastique / formant-preserving in the tree. **S16 - Preserve-engine (route a) must-verify at build:** (i) **pre-warm** `WDL_SimplePitchShifter` - at voice-allocation (run silence so `m_psbuf` sizes and `m_queue` reaches steady state) → - **no `process`-thread `WDL_Queue::Add` growth**; (ii) measure **per-voice CPU + onset - latency** (window·srate) against the polyphony cap; (iii) set a **Preserve-mode-specific - voice cap** if the per-voice cost demands one. The pitch-envelope modulation is hand-rolled - over whichever engine. If the held sinc **Varispeed**-quality upgrade is taken, verify - `WDL_Resampler` streaming/prealloc against the per-voice RT budget before use. -- **LICE/SWELL editor.** Reuses the `bank_panel` LICE/SWELL drawing surface; verify the - `IPlugView`↔LICE window/bitmap bridge at the spike (window creation, sizing, event - routing) — the least-trodden edge of the phase. -- **LICE design-kit surfaces — moved to Phase L.** The shared LICE drawing-kit surface - verification (`LICE_GradRect`/`LICE_RoundRect`/AA lines/circles/beziers/polygons + the - `LICE_CachedFont`/`LICE_IFont` font engine, and the vwnd drawing-craft references) now - lives with **Phase L point L1** on `dev` — see §"LICE / WDL API surface" in this file. Phase S surfaces (S10–S13) adopt that kit when it lands; they are not gated on it. - ---- - -## Drop-and-load — drag a capture onto a track's FX button (S17 spec) - -**The gesture.** While a capture is dragged out of the `bank_panel`, a track's TCP **FX -button** becomes a drop zone. Dropping the capture there **instantiates a ReaSampler 9000 -on that track with the dragged capture already loaded and selected for playback** — one -gesture from bank to playable instrument. This is the *third* integration gesture: capture -(extension), placement-into-arrange (extension), and now **placement-of-the-player** -(this wave). It is drop-and-load, not drop-to-arrange — no media item touches the timeline. - -**Why it needs a new drag mode (the CF_HDROP path can't carry it).** Today's drag-out -(M11) becomes an **OS file drag** (`CF_HDROP` via `drag_out` + `drag_out_win`) the moment -the pointer leaves the panel client rect. REAPER's TCP FX button is **not** a native drop -target that instantiates a plugin-with-a-file, so this feature cannot ride the OS-drag -path: an OS drop of a WAV onto the FX area does not create "an instrument preloaded with -that WAV." It requires an **internal drag** where the extension itself tracks the pointer -over REAPER's own UI, detects the FX-button hover, and on release **drives the insert -itself**. The extension is the actor for the whole gesture. - -**The two-part mechanism.** - -1. **Internal-drag hover detection (extension-side, pure + shell).** The `drag_out` pure - module gains a **third `DragGesture`** beyond `Internal` (bank-to-bank) and `OsDrag` - (M11) — `InstrumentDrop`. The gesture decision is refined: leaving the panel client - rect no longer *immediately* means OS-bound. Instead: - - Pointer **inside** the panel client rect → `Internal` (unchanged bank-to-bank drag). - - Pointer **outside the panel but still over REAPER's own window/UI** → - `InstrumentDrop` (new — the shell hover-tracks the TCP FX button and highlights it). - - Pointer **left REAPER entirely** (Explorer / another app) → `OsDrag` (unchanged M11). - - The pure module stays REAPER-free: it decides `InstrumentDrop` vs. `OsDrag` from - position **plus an "over-REAPER's-own-UI" predicate the shell supplies** (the shell owns - the REAPER window/hit query; the pure layer owns the set/boundary algebra). Mirror of how - M11 kept `decideGesture` pure over a rect the shell supplied. The shell then resolves the - pointer to a track + FX-button hotspot, highlights it, and on release drives the drop. - -2. **FX-button drop → add-VST + load-capture (extension-side shell, then instrument - seam).** On release over an FX button the shell: - - Adds a fresh instance: `TrackFX_AddByName(track, "VST3:ReaSampler 9000", /*recFX*/ - false, /*instantiate*/ )`. **Verified present** in - `reaper_plugin_functions.h`: - `int TrackFX_AddByName(MediaTrack* track, const char* fxname, bool recFX, int - instantiate)` — a **negative** `instantiate` always creates a new effect (per the - header comment); the `"VST3:"` prefix selects the format. Captures the returned FX - index (or `-1` on failure). - - **Loads the dragged capture into that instance via the load-capture seam** (below). - - Wraps the whole thing in one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the - gesture is one Ctrl-Z — the same discipline the bank verbs use. - -**The ReaSampler 9000 load-capture seam (the hard coupling — MUST be added; does not yet -exist).** The Phase S spec today gives the instrument a **live-state *read* seam** (it -reads bank index + mapping from `"reasampler"` ext-state via the bridge — CONTEXT.md §The two seams) -but **no entry point for an external actor to say "this fresh instance should play *this -specific* capture."** Reading the bank is not the same as being *pointed at one sample*. -This wave is the reason to add that seam, and the seam lands **inside the instrument** -(the `phase-s` artifact), not the extension. - -**Mechanism (SETTLED — (B) VST3 component-state injection).** Right after -`TrackFX_AddByName` returns the new FX index, the extension writes the instance's component -state directly — the same blob the instrument's `getChunk`/`setChunk` round-trips — with -the target capture pre-selected. Deterministic, no shared-state race, no cross-process -handshake; it uses the instrument's own persistence format. The state-set path is -`TrackFX_SetNamedConfigParm` — **verified present** in `reaper_plugin_functions.h`: `bool -TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char* -value)`, and the header documents the write-parms `vst_chunk` / `vst_chunk_program` as the -base64-encoded VST-specific chunk. So the injection call is -`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", )`. - -**Load-bearing caveat — `vst_chunk` is the plugin's own serialized chunk.** `vst_chunk` is -ReaSampler 9000's **own** base64-encoded serialized state (its `getChunk`/`setChunk` -FXP/FXB-style blob), **not** a raw VST3 `IComponent::setState` stream that REAPER -re-marshals into the plugin. The extension therefore has to construct **exactly the -instrument's own state-blob bytes** with the capture pre-selected — REAPER does not -translate a neutral state representation on its behalf. This makes the **component-state -blob format a shared cross-artifact contract** — one that is **still being defined in Phase -S** — and a **coordination dependency between the extension and the instrument:** both must -agree on the exact byte layout that ReaSampler 9000's `setChunk` accepts before either half -is final. The load-capture seam and the component-state persistence work (CONTEXT.md §Where the toggle lives / component-state version bumps) share this one blob format. - -**Rejected alternative — (A) fresh-instance ext-state handshake.** The extension writes a -small "pending load" hint into `"reasampler"` ext-state keyed to the target track/FX (a -capture id + a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via -the bridge it already uses, claims + clears the hint, and self-selects that capture. It -would keep the artifacts loosely coupled through the one ext-state seam they already share -and avoid the extension hard-coding the instrument's state format — but it **loses on the -claim/clear race:** "which instance claims which hint" needs a stable key and a -cross-process handshake to get right, and (B) sidesteps that entirely by writing the state -directly and deterministically. - -**Coexistence with the OS drag-out (disambiguation contract).** The two OS-vs-internal -modes are disambiguated **by pointer location, not a mode toggle** — the user never picks -"OS drag" vs. "instrument drop"; the extension infers it from where the pointer is when -released. The M11 boundary (left the client rect) is *refined*, not replaced: leaving the -rect now asks "over REAPER's UI → InstrumentDrop, else → OsDrag." Both M11 OS drag-out and -the internal bank-to-bank drag must remain **byte-for-byte unchanged** in their own -regions — this wave only inserts a new middle case. Multi-capture payloads are a -disambiguation input too (see open question — instrument drop is naturally single-capture; -a multi-capture drag over the FX button is either rejected or loads the first). - -**Precision / invariant implications (drop-and-load).** -- **Explicit user-driven placement — consistent with capture↔placement separation.** This - is a *deliberate placement gesture*: the user chooses to put a playing instrument on a - track, exactly as inserting an item into the arrange is a deliberate act. It does **not** - auto-capture (the file already exists in the bank) and does **not** insert a media item - into the timeline. It instantiates a *reader* of the bank on a track and points it at one - already-captured sample. Capture, placement, and playback stay three distinct acts; this - is placement-of-the-player, not a capture and not a timeline insert. -- **No private sample copy.** The instantiated instrument consumes the one authoritative - bank (it resolves the WAV via the shared M4 project-relative machinery like any - ReaSampler 9000 instance); the seam hands it a *reference* (a capture identity), never a - copied file. Any path that copies bytes into the instance is a bug. -- **The internal drag stays pure-decidable and testable.** The new `InstrumentDrop` - gesture is decided in the `drag_out` pure module (REAPER-free) over a shell-supplied - predicate; the M11 `drag_out` unit tests must not regress. - -**Open questions (Daniel / Phase S team to decide).** -- **Multi-capture drag over an FX button** — reject (only single-capture drags arm - `InstrumentDrop`), or load the first / a keymap of all? Tier-0 leans reject-or-first; - a multi-capture keymap load is a Tier-1 stretch. -- **FX-button hotspot vs. whole TCP.** Does the drop zone have to be the FX button - specifically, or is dropping anywhere on the target track's TCP enough (simpler hit - resolution, arguably clearer target)? Depends on what the SDK exposes (see must-verify). - -**Must-verify before build (drop-and-load).** -- `TrackFX_AddByName` — **verified present** (`reaper_plugin_functions.h`): signature and - the `"VST3:"`-prefix + negative-`instantiate` semantics confirmed from the header. -- **Pointer→track / FX-button hit resolution during a drag** — **not yet confirmed.** - Candidates: `GetTrackFromPoint` / `GetThingFromPoint` (verify names + signatures against - `reaper_plugin_functions.h`); whether the FX button specifically is addressable vs. the - TCP as a whole is an open verification that also decides the "hotspot vs. whole TCP" - question. -- **Instance state injection (seam mechanism (B) — SETTLED, load-bearing prerequisite)** — - **verified present** in `reaper_plugin_functions.h`: `bool - TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char* - value)`, with the header documenting `vst_chunk` / `vst_chunk_program` as the - base64-encoded VST-specific chunk write-parms. The injection call is - `TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", )`. The remaining - prerequisite is **not** the API but the **shared component-state blob format**: `vst_chunk` - carries the instrument's *own* serialized chunk (its `setChunk` input), so the extension - must construct exactly ReaSampler 9000's state bytes — the cross-artifact contract still - being defined in Phase S. Blocks the drop half until the blob format is agreed. - ---- - -### Module architecture (preserve the pure/shell split) - -- **Pure (new/extended):** `envelope_overlay` (AHDSR/Trigger params + frame-length → polyline in a - rect; unit-tested); `envelope_edit` (NEW — node hit-test + pixel-delta→clamped-param inverse map - for the draggable envelope handles; mirror of `card_drag`; unit-tested at the monotonic/clamp - boundaries); `velocity_curve` (NEW, r10 — bezier `eval(velocity 0–127)→amp 0–1` + control-point - add/move/delete clamped to the 0–127×0–1 box, x-ordered, hit-test + pixel-delta inverse map; - mirror of `envelope_edit`; unit-tested at eval + clamp/order boundaries); `keyboard_strip` - extended with the natural/accidental predicate; the sampler core extended with the **key-track - scalar** in the repitch math (unit-tested against known note/root/keyTrack → ratio) **and the - velocity-curve eval at `Voice::start()`** replacing the linear `velocity/127` (r10; off the - per-frame path); `sample_map` (`PerformanceZone`) extended with the additive `keyTrack` field - **and the additive `velocityCurve` field** (r10 — both on the zones-payload version axis; the - velocity-curve field sequences AFTER T-KEYTRK's `keyTrack` bump), plus the **envelope-v6 - `previewVelocity` field on `ComponentState`** (S-VIEW-F1) — all additive, version bumped, - back-compat defaults on read. -- **Shell (`reasampler_editor.cpp`):** re-partition the paint/hit-test into the three views - (Sample face, Browse modal overlay, Zone surface) replacing the two-view toggle; add the - preview-trigger button + velocity knob wired to an off-audio-thread preview note through the - voice engine (the velocity **persists** via the envelope-v6 `ComponentState` field, S-VIEW-F1); - draw the envelope overlay + **its draggable node handles** (routing mouse events through the pure - `envelope_edit` module and committing params via the same off-thread path the sliders use, - S-VIEW-F2) + piano-key overlay via the kit; set the larger default `ViewRect` + - `checkSizeConstraint`. All layout/hit-test math stays in the pure geometry modules. - ---- - -## Kit architecture (the pure/shell split) - -Pure (no LICE, no REAPER types, unit-tested — the mirror of `mode_switch`/`bank_grid`): -- **`theme`/palette module** — role→color mapping, direction-selectable via one constants - block (the B + REAPER-grey-neutral + three-accent-pastel + pastel-spectral values). Pure; - unit-tested that each text-on-surface pair clears its WCAG floor (the "punch" rule made - testable). *DS-2 revision (2026-07-26):* (a) the neutral ladder moves from near-black up - into REAPER's mid-grey family (`bg/base` `#2b2b2b` / `bg/panel` `#333333` / `bg/cell` - `#3a3a3a` / `line/hairline` `#4a4a4a` / `text/primary` `#dcdcdc` / `text/dim` `~#a0a0a0`+), - and (b) the accent role expands from one to three (`accent/primary` lime, `accent/secondary` - teal, `accent/tertiary` purple) with the spectral ramp a pastel sweep anchored on those - three — all still confined to the one constants block + the shell's font lifecycle. **The - WCAG test must be re-run against the GREY ladder** — it gains the newly-tight grey pairs: - `text/dim`-on-grey (AA 4.5:1) and each pastel-as-state-indicator on `bg/cell` (3:1), plus - the existing text-on-pastel-fill pairs. (No font change: a bundled-font upgrade was - declined — the cached-font set keeps the kit's current face.) -- **Component geometry/hit-test helpers** — button rect, slider track/handle geometry, - list-row rect + hover hit-test, and any new layout module L2 needs (an action-bar layout - module). No LICE, no host types; CTest-covered. Existing pure modules - (`bank_grid`/`tab_strip`/`mode_switch`) stay the source of truth for what they own. - -Shell (LICE-facing, DAW-verified — thin draw layer): -- **Draw kit** — `fillSurface` (micro-gradient + inner highlight/shadow), - `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`/segmented-switch/tab draw, and a - shared `text()` over a cached `LICE_CachedFont` set (title/label/value-mono/micro). Owns - the cached-font lifecycle. Honors the interaction state model (rest/hover/active/pressed/ - dragging/focus/disabled). **DS-1: WDL/vwnd reuse is assessed here at build time** — reuse - a vwnd piece where genuinely cheaper, else draw on LICE; hit-test geometry stays pure - regardless. - ---- - -## L2 dock-panel layout contract (the M11-aware inventory) - -DS-3 makes L2 a **layout design**, not a skin pass, because M11 adds a real button -inventory. L2 must place **every** affordance below without crowding the grid (the -centerpiece), grouping by *task*: - -**Existing (landed / specced):** -- Bank **grid** — thumbnails, multi-select, keyboard nav, audition, focus ring. -- Design View **segmented mode switch** (`[ Arrange | Design ]`) + per-mode membership count. -- Multi-bank: **named-banks tab strip** (LICE-drawn, overflow/scroll), **active-bank - indicator**, **pool/banks full-height toggles**, create/rename/delete/activate-bank - affordances, per-selection **move / copy / remove** sample menu. -- **Prune** button (R-E) — the byte-deleting action; `warn`-colored, set apart. - -**M11 adds (COMPLETED.md §M11):** -- **Action-trigger buttons** — clickable buttons firing the capture + provenance action - family directly (capture item / capture track scopes, re-capture from source, - resample-and-mute-source, batch capture, conform-on-insert, insert-at-cursor, drag-out, - null-test verify). A cluster. -- **Keybinding-help labels** — each capture/provenance action surfaces its current key - binding (e.g. "Capture Item → F5") or an "unbound"/"—" marker. - -**Layout mandate:** group by task (capture / organize / reclaim / view), not by phase; a -compact action bar/toolbar for the frequent capture actions (icon+label, keybinding as a -`micro` sub-label), an overflow/menu for the rare ones, header space for the mode switch + -active-bank indicator, the bank tab strip + move/copy/remove organize cluster together, -prune set apart and `warn`-marked. Density is a design decision — 8px grid, elevation -layers over hairlines, hover on every interactive element. Then apply the L1 kit to draw -it. New layout math goes in a pure geometry module; `bank_grid`/`tab_strip`/`mode_switch` -stay the pure source of truth for their own hit-testing. **L2 sequences after M11 merges** -so it inventories the actual landed buttons. - ---- - -## L4 dock-panel button layout (top / bottom / footer re-home) - -L4 is a **second layout pass over the same `bank_panel`** that re-homes the L2 button inventory -around *frequency and intent*. It ships **no new action and changes no capture/placement -behavior** — every button fires an existing registered action; the "capture ≠ placement" -load-bearing principle is untouched (the buttons only *fire* the split acts, they never fuse -them). It is **ungated by Phase S** (the dock panel is on dev) and independent of L3. -**L4 sequences AFTER the in-flight DS-2 palette-revision branch merges to dev** — both rework -`bank_panel` heavily, and landing L4 concurrently would collide the same file. Drawn through the -L1 kit in the DS-2 grey-neutral + three-accent-pastel palette (prune stays `warn`); no palette -or font decisions are re-opened here. - -**The new three-zone structure:** - -- **Top toolbar = capture + placement + maintenance.** The capture cluster (capture item, - capture track, batch items, batch razor, capture RT), the placement cluster (insert, - insert-conform), and the maintenance cluster (re-capture, cancel-realtime) move from the - bottom L2 action bar to a **top** toolbar — the eye's first landing, matching the acts the - tool exists for. Icon+label buttons with the keybinding as a `micro` sub-label (the M11 - keybinding-help convention), drawn through the kit. -- **Bottom toolbar = Design View tagging + switching.** The space the capture/placement buttons - vacate holds the **Design View action family** as buttons: **tag / untag selected tracks for a - mode, activate Arrange, activate Design, toggle active mode, show-both.** These are the - registered Design View actions today; L4 gives them a button home here. -- **Footer = narrow Arrange|Design toggle · Tail button · … · Prune.** The large top - `[ Arrange | Design ]` segmented toggle **shrinks to just-wide-enough-for-its-text** and moves - into the footer at the **left**, carrying its per-mode membership count as a compact adjacent - label. The **Tail** affordance is converted from a **click-zone to a proper kit button** - (rest/hover/pressed states; click still cycles the tail setting). **Prune** stays the - byte-deleting action — **set apart at the far right, `warn`-colored** — the only file-deleting - affordance, kept isolated so no benign toggle sits next to it. - -**Footer affordance order (left → right):** `[Arrange|Design]` toggle · Tail button · … · -**Prune** (rightmost, set apart, `warn`). The order reads benign/frequent at the left -(view-mode toggle, tail-length control — both "how this panel/capture behaves") → destructive/ -rare at the right (Prune, isolated), so a mis-click near the left is cheap and the one -destructive control is spatially and chromatically distinct. - -**Pure/shell discipline (unchanged).** All new toolbar-row and footer-strip layout math goes in -pure CTest-covered geometry modules — **extend/mirror `action_bar`** (the toolbar row layout, -now instantiated top and bottom) and **`mode_switch`** (the now-narrow, fit-to-text footer -toggle geometry + hit-test). `bank_grid` and `tab_strip` remain the pure owners of their own -surfaces' hit-testing. The L1 kit draws; the geometry stays pure. **L4 resolves its re-home -against the post-palette-revision `bank_panel`** (build-time inventory once that branch is on -dev). - ---- - -## L5 dock-panel button refinements (overflow menu · faces+tooltips · opposite-mode tags · Toggle removal · grouping) - -L5 is a **third refinement pass over the same `bank_panel` toolbars L4 built** — it re-homes -and re-labels buttons for legibility; it **ships no new action and changes no capture/placement -behavior** (every button fires an existing registered action; "capture ≠ placement" is -untouched — the buttons only *fire* the split acts). **Ungated by Phase S** (the dock panel is -on dev) and **independent of the L3 gate.** Drawn through the L1 kit in the DS-2 grey-neutral + -three-accent-pastel palette (prune stays `warn`); **no palette or font decision is re-opened.** -L5 sequences AFTER L4 (both rework the same `bank_panel` toolbars). - -**1. Top-toolbar overflow menu.** The less-frequent capture variants — **Batch Items, Batch -Razor, Capture RT, Cancel RT** — leave the visible top bar for a **right-anchored "⋯ / More" -menu button** (kit-drawn button; on click a `TrackPopupMenu` popup lists all four, each entry -firing its existing command id via `Main_OnCommand`). The frequent acts stay on the bar in -cluster order Capture → Maintenance → Placement: Capture Item, Capture Track (Capture); -Re-capture (Maintenance); Insert, Insert Conform (Placement). The menu button's rect + -hit-test is **pure** (extend/mirror `action_bar` / `prune_button`); the popup + dispatch is -shell. The menu entries reuse the same `resolveBarCommandId` path the bar buttons use, so a -keybinding and a menu pick fire identically. - -**2. Short faces + full-name tooltips (drop the `ReaSampler:` prefix).** Button *faces* carry -the terse `ActionBarRow.shortLabel` (already the case since L4) — L5 formalizes the face set and -adds a **hover tooltip showing the FULL action name with the `ReaSampler:` display prefix -stripped.** Note the prefix (`actionDisplayPrefix()`) is baked into the *registered gaccel -action name*, not the button face — so the tooltip derives the full name and strips the prefix -for display; the button face never carried the prefix. The keybinding is surfaced in the -hover tooltip ("`phrase — binding`" when bound, bare phrase when unbound) via -`kbd_getTextFromCmd`; the `micro` sub-row is removed from `ActionBarSlot`. **Tooltip mechanism: custom LICE-kit hover-delay tooltip** (`tooltip` pure -module) — owns its own hover timer + LICE overlay draw, stays inside the L1 kit (DS-1 "keep -drawing in the kit"). Sourced from the registered action phrase (not `kbd_getTextFromCmd`); -`ReaSampler:` prefix stripped at draw time; tooltip box width clamped to the client so it never -overhangs a narrow dock. The hover-timer threshold + overlay draw are the only DAW-bound pieces; -the "which button, what text" decision stays pure. - -**3. Bottom-toolbar Item/Track × Arrange/Design tag buttons, opposite-mode-only.** The current -`Tag Design` / `Untag` pair is replaced by **four buttons — "Item: Arrange", "Item: Design", -"Track: Arrange", "Track: Design"** — in the Tagging cluster. **Both action families already -exist in the model + actions layer** (research-confirmed): the *track* family is -`doTag`/`doUntag` on the track selection (`VIEW_TAG_DESIGN` and `VIEW_TAG_ARRANGE`, where -Tag→Arrange == untag); the *item* family is `doMoveItems` on the item selection -(`VIEW_MOVE_ITEMS_DESIGN` / `VIEW_MOVE_ITEMS_ARRANGE`, driving `planItemRetag` + lane minting). -**So the four buttons are layout + enablement wiring over existing actions — NOT new feature -work; no `view_mode_model` / `view` / `actions` change is required.** - -**Enablement rule (precise).** Let `active` = `view().activeModeId()` (the SAME read the footer -`[Arrange|Design]` toggle uses — one source of truth). A tag button's *target mode* is the mode -in its label (Arrange or Design). A button is **live iff target ≠ active**; otherwise it is -drawn `Disabled` (kit `InteractionState::Disabled`, `Role::TextDim`) and its click is a no-op. -Concretely: -- **Design active** → `Item: Arrange` and `Track: Arrange` are live (they send the selection to - Arrange); `Item: Design` and `Track: Design` are disabled (the selection is already there). -- **Arrange active** → the reverse: the `…: Design` buttons are live; the `…: Arrange` buttons - are disabled. -The disabled predicate is **pure** (active mode → per-button live/disabled, unit-tested); the -shell reads the active mode once per draw and applies it. Item buttons act on the current media- -item selection; Track buttons act on the current track selection — matching the existing action -bodies exactly (no selection semantics change). - -**4. Toggle button removed.** `VIEW_TOGGLE_MODE` leaves the bottom toolbar — the footer's -`[Arrange|Design]` toggle (L4) already covers mode switching. **The action stays registered** -(keybinding-bound, FOREVER-STABLE id unchanged); only its *button home* is removed. - -**Fate of Activate-Arrange / Activate-Design / Show-Both (FORK — RESOLVED, Daniel's call).** -- `VIEW_ACTIVATE_ARRANGE` / `VIEW_ACTIVATE_DESIGN` — **dropped from the bottom toolbar** (actions - stay registered, FOREVER-STABLE ids unchanged). The footer `[Arrange|Design]` toggle is the - single mode-switch affordance; the bottom bar is tagging + Show Both only. -- `VIEW_SHOW_BOTH` — **kept** as a set-apart button on the bottom toolbar. It is the cross-mode - "pin visible in every mode" escape hatch and is not covered by the footer toggle. -- **Landed bottom-toolbar inventory:** `[ Item: Arrange | Item: Design ] · [ Track: Arrange | - Track: Design ] ⟩⟩ [ Show Both ]` — Tagging cluster (four opposite-mode buttons) set apart - from a lone `Show Both`. No Toggle, no Activate-Arrange/Design (footer toggle owns switching). - -**5. Semantic-grouping spacing.** L4's `kBarSpec` is `buttonGap=4` / `clusterGap=16` (4:1). With -the bottom bar's cluster boundary now more meaningful (four tag buttons vs. the `Show Both` -remnant) the grouping should read at a glance — **widen the inter-cluster gap: start ≈ -`clusterGap=24` / `buttonGap=4` (6:1), tuned in-DAW.** One `kBarSpec` still serves both -toolbars (identical button shape top and bottom); only the gap ratio changes. - -**Pure/shell discipline (unchanged).** All new geometry — the top-bar overflow menu-button rect -+ hit-test, the opposite-mode enablement predicate, the wider grouping spacing — goes in pure -CTest-covered modules (extend/mirror `action_bar` / `prune_button`; add a pure enablement -predicate). The `TrackPopupMenu` popup, the `Main_OnCommand` dispatch, and the tooltip hover- -timer + LICE overlay draw are the only DAW-bound pieces; the L1 kit draws. **L5 resolves its -menu/tooltip/action-id specifics against the landed L4 `bank_panel`** (build-time confirmation). - ---- - -### 1. Persisted deterministic order + sparse (gap-preserving) placement - -Each bank (and the pool) carries an **explicit, persisted display position per sample** — the -grid no longer derives order from insertion order. Positions are **gap-preserving**: a sample -may occupy a slot that leaves earlier slots empty (an empty first row above an occupied second -row is a valid, persisted state). - -**Where the position data lives (the CLAUDE.md constraint).** `bank_model` / `Sample` are stated -**untouched by `bank_book`'s wrapping** (no `bankId` on `Sample`; a bank is a logical grouping -over the shared pool). Display position is a **per-bank display concern**, so it belongs with the -bank's membership, **not on `Sample`** — a copy of a sample into two banks can sit at different -slots. **Recommended carrier: a per-`Bank` ordered position map in `bank_book`** (sample id → -slot), leaving `bank_model` untouched. The exact carrier (ordered id list with gaps vs. explicit -id→slot map) is settled at build; the *contract* below holds either way. - -**Ordering contract (deterministic, gap-preserving):** -- **Deterministic:** the grid iterates positions in ascending slot order; ties are impossible - (one sample per slot). The order is fully determined by the persisted position data, not by - insertion order or hash. -- **Insert (new capture):** a new capture takes the **next free slot after the last occupied - slot** (append). It never fills an earlier gap automatically — a gap is a user's deliberate - layout, not a hole to be plugged. -- **Delete / remove / prune:** removing a sample **leaves its slot empty** (does not re-pack) so - every other sample keeps its position. Trailing empty tail is trimmed for scroll-extent - purposes; interior gaps are preserved (confirmed at build). -- **Reorder:** the user drags a card to a target slot within its bank; the pure reorder mutator - moves that sample's position to the target slot, gap-preserving. **Drop-into-empty-slot places - there; drop-onto-occupied inserts-before and shifts the tail** (matching common file-manager - reorder) — SETTLED as the *default* drop. (The **Alt+drop-onto-occupied = REPLACE** override is - specified below under Drag disambiguation, F3.) - -**JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless -including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position -data** → on load it defaults to **current insertion order, densely packed (no gaps)**, so a -project saved before L7 is visually identical on first post-L7 load. This default is the -migration; it is one-way (once re-saved, the position data is authoritative). Pure and -hard-tested — this is a persisted-model change, tested to the same bar as `bank_book`'s existing -round-trip + legacy migration. - -**Undo.** A reorder is **one Ctrl-Z** — the actions/shell layer wraps the mutation in a batched -undo point (`Undo_BeginBlock2`/`EndBlock2`), matching every existing bank-verb's undo discipline. - -**Position-model shape (F2 — SETTLED 2026-07-27: interchangeable substrate, NOT fixed slots).** -The carrier is the **plain gap-preserving interchangeable-slot substrate** — a per-bank id→slot -map (Daniel: *"I don't think I want fixed slots MPC style, but the substrate of interchangeable -slots is valuable"*). It is **not** M9-shaped: no slot *identities*, no numbered/addressable -slots that persist independent of their occupant, no slot actions, no MIDI-bindable slot numbers, -no capture-to-slot-N. A slot is just a display position a sample occupies; dragging cards -rearranges which sample sits where. - -**M9 note.** M9 "slots" (capture-to-slot-N / insert-slot-N, MIDI-bindable, MPC-style) is -**abandoned (Daniel, 2026-07-27)** — will not be built. L7 adds **no** slot-numbered -capture/insert actions and **no** MIDI bindings. The "plain vs. M9-shaped" sub-fork is closed: -plain gap-preserving substrate, per F2 above. - ---- - -### 2. Decorative metadata overlay (bars.beats · s.ms) - -Each card overlays, on the waveform, two decorative read-outs of **capture length**: -- **bottom-LEFT:** length in **bars.beats.subdivisions** (musical). -- **bottom-RIGHT:** length in **seconds.milliseconds** (wall-clock). - -**Contract:** decorative and **non-interactive** — no hit-test, no hover, no selection role. -Drawn via the L1 kit `text()` in the **micro / value-mono** type class, in `text/dim` (or a -subtle shadowed variant for legibility over the peaks), subordinate to the waveform. **Respects -the speed constraint (no animation).** Pure formatting helpers (below) are unit-tested; only the -kit draw is shell. - -**Bars.beats source (F1 — SETTLED 2026-07-27: capture-time stamp).** bars.beats.subdivisions -requires a **tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at -capture) and `lengthBeats`, but **no time-signature.** **Decision: capture-time stamp** — add -`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the -capture path (read the project meter at capture via `TimeMap_GetTimeSigAtTime` — confirmed -at build against the SDK). bars.beats.subdivisions renders -from the stamped tempo + meter, **stable under later project tempo/meter changes** — a bank sample -outlives the project state it was captured under, matching the existing `captureTempo` stamp -philosophy. This is a **capture-path write beyond draw work** (its own checkbox in the PLAN, now -settled). Old samples with no stamp fall back gracefully (blank musical read-out, or a documented -assumed 4/4). -- **(rejected) Live project meter at draw time:** the label would drift under the card as the - project tempo/meter changes, and would be wrong for any sample captured under a different - meter than the project's current one. - -**Formatting helpers (pure, tested).** `bars.beats.subdivisions` from `lengthSeconds` + -`captureTempo` + capture-time signature; `seconds.milliseconds` from `lengthSeconds`. -Deterministic; graceful on edge cases (zero length → both read empty/`0`; missing tempo → blank -musical read-out, keep the s.ms read-out). - ---- - -### 3. Selection styling — tertiary border replaces inversion - -A selected card **drops the inverted accent-fill treatment** and instead draws the **normal -cell** (Rest or Hover surface) + an **`accent/tertiary` border** (pastel purple `#C2AAE8`); the -**waveform draws in its normal accent color** (the inverted `bg/base` wave is removed). The four -grid-card interaction states stay **visually distinct and coherent:** -- **Selected:** normal cell surface + **`accent/tertiary` (purple) 1px border**. No fill change, - no wave inversion. -- **Focus (caret):** the existing distinct inner ring — keep it separate from the selection - border so a focused *and* selected card reads both (e.g. purple outer border + a - `text/primary` inner focus ring). Settle the exact inner treatment at build so focus is - legible on top of the selection border. -- **Hover:** the kit Hover surface (unchanged) — a fill-state change, orthogonal to the purple - border, so a hovered selected card still reads as selected. -- **Drag-target slot:** the reorder drop-target highlight (a distinct accent — recommend - `accent/hot` outline on the target slot) must not be confusable with the purple selection - border; spec the exact treatment at build. - ---- - -### Pure/shell discipline (L7) - -Model: the position carrier + gap semantics + **reorder mutator + Alt-replace mutator** (the -latter reusing the existing index-only remove-from-bank semantics + pool-privilege guard) + JSON -round-trip/migration are **pure** (in `bank_book`, CTest-covered to the bar of its existing -round-trip). Layout: the sparse-aware slot↔rect math + point→slot hit-test + the -drag-disambiguation decision (**including the resolved-gesture result that drives the cursor cue, -and the Alt-over-occupied → replace resolution**) are **pure** (extend `bank_grid`; mirror -`mode_switch` / `drag_out::decideGesture`). Formatting: the bars.beats and s.ms formatters are -**pure**. Shell (DAW-bound): the reorder/replace drag wiring + drop-target highlight, the -**cursor `SetCursor` call mapping the pure resolved-gesture to a cursor resource**, the -capture-time-signature stamp (F1, settled) read on the capture path, and the kit -overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened. - ---- - -## The L3 gate + Phase S coordination contract - -**L3 (VST editor + embed-strip restyle) has landed (merged `c53683e`, 2026-07-27).** The -Phase S surfaces (`reasampler_editor.cpp` + `reasampler_embed.cpp`) arrived on dev drawing -flat `LICE_FillRect` blocks + raw GDI `DrawTextA`, off a local pre-L1 forest-green palette -— the coordination contract's "born in the kit" branch did not occur — so L3 performed a -full restyle of both draw shells through the L1 kit. **L1, L2, L3, L4, L5, L6, and L7 -have all landed — see `COMPLETED.md`. Phase L is complete.** The Phase Q gate condition -"Phase S + L3 merged to dev" is now satisfied. - -**Coordination contract (load-bearing — resolved):** Phase S's S10–S13 built their -interaction UX with the current drawing and adopted the L1 kit via L3 (the coordination -contract's "L3 restyles them" branch). There is now one kit and one look across both -artifacts; L3 completed the VST/embed adoption and applied the settled-and-revised **B + -three-accent pastel** treatment (with C's **pastel** spectral keyboard strip as the -signature surface), routing text through the kit's cached-font `text()` (§3.1 — the kit's -current face; no font change). The VST3 class UID is unchanged — a visual refresh is not a -compat event. - ---- - -## LICE / WDL API surface (verify all signatures) - -- **LICE design-kit surfaces (L1).** `LICE_GradRect`, `LICE_RoundRect`, AA - `LICE_Line`/`LICE_FLine`/`LICE_ThickFLine`/`LICE_Circle`/`LICE_FillCircle`/ - `LICE_DrawCBezier`, `LICE_FillTriangle`/`FillTrapezoid`/`FillConvexPolygon`, and the - `LICE_CachedFont`/`LICE_IFont` font engine (`SetFromHFont`, AA `DrawText`, shadow/outline/ - glow FX flags). Verified *present* in `vendor/WDL/WDL/lice/lice.h` + `lice_text.h`; - **confirm exact signatures + the `LICE_CachedFont`↔`HFONT` lifecycle at build.** -- **WDL/vwnd reuse (DS-1, build-time assessment).** `virtwnd-slider.cpp` / - `vwnd_slider_drawknobstack` (slider/knob drawing reference), `virtwnd-listbox.cpp` - (candidate scroll listbox), `virtwnd-controls.h` (`WDL_STYLE_*` gradient hooks), - `virtwnd-skin.h` (image-skin helpers) — all in `vendor/WDL/WDL/wingui/`. Reuse where a - piece beats re-deriving; keep hit-test geometry pure regardless. -- **Panel drawing/hit-test (L2).** Reuses the `bank_panel` LICE surface + the existing pure - `bank_grid`/`tab_strip`/`mode_switch` hit-test modules; the new action-bar layout is a new - pure module. `WM_MOUSEMOVE`/`TrackMouseEvent` (`WM_MOUSELEAVE`) for hover on the panel's - existing timer-driven `wndProc`. Verify against the SWELL headers as `bank_panel` already - does. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 810316d..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,2779 +0,0 @@ -# ReaSampler — implementation briefing - -A native C++ REAPER extension that captures any arbitrary audio source into a -per-project **sample bank** (cached files + a docked grid), decoupled from the -arrange view, with keyboard/MIDI-bindable capture and placement. Built as a -precision tool: deterministic, non-destructive, no clutter. - -This document is the spec. Read the existing scaffold first (`src/main.cpp` is -the REAPER<->extension contract; the pure/testable-core split in `src/bank_model.*` -is the pattern to preserve — the *discipline* of a pure testable core split from -REAPER-facing shells runs throughout the codebase). Verify every REAPER API name -and signature against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use -— the API names in this brief are correct-by-intent but treat them as hints, not -gospel, and check argument order/types. - -## The load-bearing principle - -**Capture and placement are separate acts.** Capturing audio writes a file to -the bank and adds an index entry — it NEVER puts an item in the arrange. Placement -is a distinct, on-demand action (insert / drag). Any code path that auto-inserts a -capture into the timeline violates the entire point of the tool and must be -rejected in review. This single rule is why the tool exists. - -## Settled decisions - -- **Capture modes:** offline render (deterministic, the default) AND realtime - record (for hardware / performed FX). Both sit behind one capture interface and - produce identical bank entries. -- **Bank scope:** per-project, travels with the `.rpp`. Files live in a - project-relative subfolder; the index persists in project ext state. No absolute - paths anywhere in the index. -- **Material:** must handle full-mix/stem bounces, chops/one-shots, and - single-cycle/wavetable grabs equally. That means exact sample-accurate bounds, - explicit tail control, channel-count preservation, and loop/zero-crossing - handling all matter from day one. - -> Build detail for this phase (module architecture, data model sketch, API surface, build order) moved to CONTEXT-ARCHIVE.md. - -## Precision invariants (enforce, test) - -- **Null test:** a dry offline capture of a range, re-inserted at its source - position, nulls to silence against the source. Ship this as a verification - action; it is the tool's trust anchor. (Verification action cut per - `docs/product/provenance.md` — manual verification only.) -- **Bit-identical repeats:** identical offline requests produce identical files. -- **Non-destructive:** capture never mutates source items or tracks (realtime's - temp track is created and removed cleanly; source routing is restored). -- **Exact bounds:** no rounding of the requested range; no added silence unless a - tail is explicitly requested; channel count preserved (no silent stereo fold). -- **Relative paths only** in the persisted index. - -## Non-goals / guardrails - -- No auto-insertion of captures into the arrange (see the load-bearing principle). -- Native OS drag-out is deferred to the final milestone — `InsertMedia`-driven - placement is the primary path and must work first. -- Do not depend on REAPER's peak API for thumbnails; compute from the captured - file. -- Do not silently time-stretch on insert; conform is opt-in. -- Do not trust this brief's API names blindly — verify against the SDK header. - -## Open questions to resolve during build - -- Exact no-dialog render command/flag on the current REAPER build. -- ~~Realtime record routing that captures wet master output without altering the - user's monitoring.~~ **Resolved:** realtime taps the *selected track* (track scope), - not the master — a post-fader track→temp send is chain-independent by construction, - so there is no wet-master-routing problem and monitoring is untouched (see the - Realtime record API surface above). -- Audio format for the bank (wav bit depth default; allow float for wavetable - fidelity). -- Thumbnail cache: recompute vs store peak bins alongside the index. - ---- - -# Design View — additive phase spec - -> **Additive section.** This is a standalone phase parallel to — not part of — the -> M0–M11 capture roadmap above. Nothing above changes. Product framing (workflow -> narrative, screenset differentiation, N-mode reasoning, design-direction calls) -> lives in `docs/product/design-view.md`; this section is the authoritative -> technical spec, matching the house style of the capture spec. Same standing -> discipline applies: **verify every REAPER API name/signature against -> `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** - -## What it is - -A **track-visibility-plus-processing "mode" system**, toggled from the ReaSampler -window. Tracks used purely for sound design (scratch oscillators, FX mangling, -resampling sources) are tagged into **Design** mode; the arrangement's real tracks -are **Arrange** mode (the default). Toggling to a mode **hides and disables** the -tracks that don't belong to it. Workflow value: mental separation + clutter -elimination — be in Design view, resample into the bank, flip to Arrange, place it. - -Design View is a **mode projection over REAPER's single arrange timeline** — -reaching both **tracks** (parked per mode) and **items** (lane-split per mode on -shared tracks). It approximates two canvases without a second surface: same project, -same timeline, but each stance sees its own tracks and its own items. It **never** -duplicates the project or opens a second window. - -> **Two-canvas reach — settled (2026-07-23).** The original single-canvas framing -> here was derived from a REAPER constraint, not chosen as a product stance. Daniel -> reopened it — "as close as possible to two separate design and arrange canvases, -> same project, different items and leaves" — and has now signed off on the -> mechanism and resolved all forks. The "different leaves" half is delivered by -> track-parking (D1). The "different items" half is delivered by **per-item mode -> membership via REAPER fixed lanes** (track-side `I_FREEMODE=2`, `I_NUMFIXEDLANES`, -> `C_LANEPLAYS:N`; item-side `I_FIXEDLANE`, `C_LANEPLAYS`, `B_FIXEDLANE_HIDDEN`): -> each mode owns a lane on a shared track, and toggling shows/plays only the active -> mode's lane. This is an item-visibility projection over the one timeline — the -> exact analog of today's track-visibility projection — with still **no** literal -> second surface, window, or duplicate project. It is an **additive sub-phase (Phase -> D2 / Phase E)** on top of D1, spec'd in *§Two-canvas sub-phase* below. Full framing -> and the rejected alternatives (timebase-offset, subproject) live in -> `docs/product/design-view.md` §Two-canvas direction. - -It is the visibility/processing analog of the capture pillar's load-bearing rule: -**designing and arranging are separate stances on one timeline**, and the tool -enforces the separation **without ever destroying the user's real state.** - -## Settled decisions - -- **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves - **opt in** to Design (or any mode). No track appears in two modes at once except - (a) via an explicit **show-both** toggle, or (b) parent/folder derivation. -- **Parents are derived, never tagged.** A parent/folder track is visible in mode M - if **either** (a) any descendant leaf is visible in M (descendant-derived), **or** - (b) the parent belongs to M by its **own membership** — and an untagged parent is - an Arrange member by default. So an untagged folder carrying its own FX/media - above all-Design leaves shows in **both** Arrange (its own default) and Design - (derived from its children). A parent is **never parked** in any mode it is visible - in. Rule of thumb: **tag leaves; parents follow** — the common case, since a - content-bearing folder still surfaces wherever its own membership places it. - Master track is always visible and never touched. -- **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; the - UI ships **Arrange** + **Design**. A mode is (stable id, display name, ordinal). - Arrange is special only as the default home for untagged leaves. -- **Parking a track** (inactive-mode leaf): `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` - (hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and - `TrackFX_SetOffline(track, fx, true)` for **each** FX (reclaim CPU). Full CPU-park - is the deliberate choice over mix-removal-only. -- **Never touches `B_MUTE` / `I_SOLO`.** The tool owns only visibility, - `B_MAINSEND`, `I_FXEN`, and per-FX offline — on every managed leaf, tagged or - untagged. User mute/solo survives every toggle untouched. -- **Persistence.** Membership index + last-active mode + per-track flag snapshots - ride in the existing `"reasampler"` project ext-state namespace and travel with - the `.rpp`. On project open, reapply the active mode's visibility + processing. - -## Precision invariants (enforce, test) - -- **Non-destructive restore.** For every flag the tool drives, snapshot the prior - value **before** parking; on toggle-back restore **from the snapshot**, never to a - hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag - to its captured value. This is the phase's trust anchor — the analog of the - capture null test — and is enforced in the pure layer. -- **Mute/solo untouched.** No toggle ever reads or writes `B_MUTE` / `I_SOLO`. -- **Master untouched.** The tool never drives the master track's visibility flags - (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` on master; the invariant agrees). -- **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`), - never track index; tolerates unknown/stale GUIDs (prune on reconcile). -- **Relative/portable state only** in the persisted view section (GUID strings, mode - ids — no absolute paths, no index positions). - -## Documented caveat - -Offlined FX **re-instantiate** when a track returns to the active mode. Stateful -plugins (convolution, loaded samplers, tail-holding effects) re-initialize on -return — possible load hitch, un-persisted internal state lost. Accepted cost of -the CPU reclaim; surface it at the toggle affordance (tooltip). - -## Show-both semantics - -A **per-track "pin visible across modes"** flag that **re-enables processing** -whenever shown. A show-both leaf appears in every mode's visible set and is **never -parked** — its driven flags stay at snapshot/restored values, FX online, in the -mix. ("Show but keep parked" is not offered — a visible-but-silent-and-offline -track is clutter with a thumbnail.) Stored on the membership record; persists; -togglable per selection. - -> Build detail for this phase (module architecture, API surface) moved to CONTEXT-ARCHIVE.md. - -## Non-goals / guardrails - -- **No *literal* second canvas.** A literal second arrange surface, a second window, - or a duplicated project stays **rejected** — reject any such path in review. This - guardrail was **narrowed (2026-07-23), not lifted**: per-item mode separation via - REAPER fixed lanes on shared tracks (an item-visibility projection over the one - timeline) is now **allowed** and specified in *§Two-canvas sub-phase* below. Paths - that remain **rejected**: subproject / second-project-file approaches, and - overloading item `D_POSITION` with mode semantics (timebase-offset regions) — the - latter collides with the capture null test. See `docs/product/design-view.md` - §Two-canvas direction for why those were rejected. -- **Never touch mute/solo.** Any code path reading/writing `B_MUTE` / `I_SOLO` is a - bug. -- **Never drive a manual lane** (Phase D2). A mode toggle touches only **managed** - lanes (minted by the mode system, keyed in the lane-ownership index). Any code path - that shows, hides, silences, or re-lanes a **manual** lane — a user-created comp/take - lane outside the mode system — is a bug. The tool drives only lanes it created. -- **Every leaf is managed.** The mode system owns all leaf tracks: an untagged leaf - is an Arrange member, and when the active mode is not Arrange it is fully parked - and snapshot-restored exactly like a tagged leaf out of its mode. **show-both** is - the only way to opt a leaf out of parking. (Parents stay visibility-only, master - is never touched — see below.) -- **Restore from snapshot, never to a default.** No hardcoded "on" restores. -- **Verify API names** against the SDK header before use. - -## Open questions to resolve during build - -- Reconcile residuals (bulk behavior shipped — `ViewModeModel::reconcile(liveGuids)` - prunes orphaned snapshots on every toggle/load; folder restructure is self-healing - because the tree is rebuilt each toggle; membership is intentionally kept so - undo-delete preserves the tag). Two sub-items remain deferred: - - **FX-GUID keying for `restoreFxOffline`:** currently restores by slot index; a - reshuffled FX chain while parked will restore to the wrong slot. Fix requires - FX-GUID keying + snapshot-schema migration — deferred. - - **Dormant membership entries:** truly-deleted tracks accumulate stale entries in - persisted `view_state` (harmless and bounded); natural home is a future - user-initiated "compact" action, not automatic pruning (which would reintroduce - undo-delete tag-loss). -- Interaction with the user having a screenset active (Design View drives the same - flags a screenset recall would; last writer wins — confirm no surprising fight). - -## Two-canvas sub-phase (Phase D2 / Phase E) — additive to D1 - -> **Additive sub-phase, settled 2026-07-23.** Extends D1's track-level mode -> projection to **item level** so each stance owns its own items as well as its own -> tracks. Nothing in D1 changes; this wraps it. Runtime floor rises to **REAPER 7** -> for this sub-phase (fixed lanes shipped in v7). Product framing in -> `docs/product/design-view.md` §Two-canvas direction. - -### What it adds - -On a track present in **both** stances (a show-both track, or a folder carrying its -own media), each mode owns a **fixed lane**: the active mode's lane shows and plays; -the inactive mode's lane is hidden and silent. A Design take and an Arrange take can -then live on the *same track, same time position*, without colliding on the view. -Track-only-in-one-mode content is still handled by D1 track-parking, unchanged. - -### Settled decisions - -- **Mechanism: fixed item lanes.** Map mode → lane; toggle drives per-lane play/show - so only the active mode's lane is present. Items keep their real position and real - track — nothing is moved in time or deleted. SDK surface (**verified present in - `vendor/reaper-sdk`**): track-side `I_FREEMODE = 2`, `I_NUMFIXEDLANES`, - `C_LANEPLAYS:N`; item-side `I_FIXEDLANE`, `C_LANEPLAYS`, `B_FIXEDLANE_HIDDEN`. - **`I_FREEMODE` changes require `UpdateTimeline()`** to take visible effect. -- **Membership: adoption rule for new items; active mode for new tracks.** New - **tracks** are tagged to the active mode at creation. New **items** follow an - adoption rule: if the item's track has pre-existing managed-eligible content - spanning exactly **one** mode, the item adopts **that mode** — the track stays - single-mode, no lane split, nothing stranded. The active-mode fallback applies - only when the track is empty (no prior content) or already spans multiple modes. - Items in **manual lanes** are excluded from the prior-mode computation and are - not auto-tagged at all (manual/managed boundary unchanged). Deliberate multi-mode - splits occur only via the explicit tag/move-item actions, never via auto-tag. - Pre-existing content defaults to **Arrange**. Membership is **exclusive per - item**: an item lives in exactly one mode, except via the existing **show-both** - escape hatch. (New-track tagging follows the same active-mode rule as D1 track - membership; the adoption rule is a refinement for items on shared tracks.) -- **Inactive-mode content is hidden AND silenced.** The off-mode lane is set - `C_LANEPLAYS = 0` — neither shown nor played — consistent with exclusive - membership and with D1's "flipping modes is a real change, not cosmetic." Show-both - is the deliberate opt-out for a lane that must stay audible across modes. -- **Managed vs. manual lanes — indexed and distinct.** Fixed lanes are also REAPER's - native comping surface: a user may keep **their own** manual lanes (comp takes, - alternate reads) on a track alongside the mode system's lanes. The tool maintains a - **lane-ownership index** — per (track GUID, lane): **managed** (which mode owns it) - vs **manual** (user-minted, outside the mode system). Mode operations touch **only - managed lanes**; manual lanes are never shown, hidden, silenced, or re-laned by a - toggle, and their `C_LANEPLAYS` stays exactly as the user set it. Items a user adds - to a **manual** lane are **not** auto-tagged (auto-tag governs normal timeline - content, not hand-managed lanes). The ownership index rides in `"reasampler"` - `view_state` alongside the membership index, GUID-keyed and portable. -- **Capture placement is mode-aware.** An **explicit** placement while in Design mode - — including capture-and-place — lands the item in the **Design lane**; the same - rule governs manual insertion. **The capture load-bearing principle is untouched:** - capture still writes a file + index entry and **never** auto-inserts; this governs - only *where an explicit placement lands*. -- **REAPER floor: v7.** No version-gate branch — below v7 this sub-phase is simply - unavailable. - -### Precision invariants (unaffected — called out explicitly) - -The capture precision invariants — **null test, bit-identical repeats, -non-destructive, exact bounds, relative-paths-only** — are **entirely unaffected** by -this sub-phase. No capture path changes; lanes are a placement/view concern -downstream of the written file. Lane assignment and `C_LANEPLAYS` are **reversible -flags**: the item is never relocated in time or deleted, so the sub-phase extends the -D1 non-destructive snapshot/restore contract to a new (item-lane) flag family rather -than introducing any destructive operation. - -**New invariant — mode operations touch only managed lanes.** A mode toggle drives -only lanes the mode system minted (managed lanes, keyed in the lane-ownership index). -Manual lanes — user-created comp/take lanes outside the mode system — are **never** -shown, hidden, silenced, or re-laned by a toggle; their `C_LANEPLAYS` is left exactly -as the user set it. This is the fixed-lane analog of *never touch `B_MUTE`/`I_SOLO`* -and *never touch master*: **the tool drives only what it created.** Enforceable and -testable in the pure layer — the "which lanes may this toggle touch" decision is a -pure query over the ownership index; only reading REAPER's live lane state is shell. - -> Build detail for this sub-phase (module architecture, new-content detection, API surface) moved to CONTEXT-ARCHIVE.md. - ---- - -# Multi-bank — additive phase spec - -> **Additive section.** This is a standalone phase parallel to — not part of — the -> M0–M11 capture roadmap and Phase D above. Nothing above changes. Product framing -> (workflow narrative, pool-privilege reasoning, movement semantics, UI-direction -> calls) lives in `docs/product/multi-bank.md`; this section is the authoritative -> technical spec, matching the house style of the capture and Design View specs. -> Same standing discipline applies: **verify every REAPER API name/signature -> against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use.** - -## What it is - -The single per-project **bank** (`bank_model` / `BankIndex`) is generalized into a -**multi-bank system**. The existing bank becomes **the pool** — a default, -always-present bank that every capture lands in unless another bank is the active -target. On top of the pool the user creates **named banks** ("Drums", "1-Shots", -"Synth Hits") that group samples for a purpose. Samples move freely between any -banks, including to and from the pool. Exactly one bank is the **active bank** — the -capture target — the pool by default. - -**This is the container generalization of the capture pillar's bank.** The pool is -to banks what Arrange is to modes: structurally one member of an N-collection, but -privileged as the default home. The capture pillar's load-bearing rule is -untouched — capture still writes a file + an index entry and never inserts into the -arrange; the only change is *which* index the entry lands in. - -## Settled decisions - -- **The pool is privileged, not special-cased.** Structurally the pool is one - `BankIndex` among many in the container (mirror of "Arrange is just another - mode"). Semantically it is privileged: it **always exists**, is **un-deletable**, - and is **un-renamable** (fixed id + fixed display name "Pool"). New projects and - migrated single-bank projects start with the pool and **zero** named banks. This - keeps the data model uniform (no pool-shaped special type) while the rules layer - enforces the three privileges. -- **Container in the pure core; a `BankIndex` per bank.** A new pure module - `bank_book` owns an ordered registry of banks, each bank = `{ stable bank id, - display name, ordinal, BankIndex }`. **`BankIndex` is untouched** — the multi-bank - layer wraps it, it does not modify it (additive; no `bank-id` field on `Sample`). - Bank id is the stable key (GUID-style, minted on bank create); display name and - ordinal are mutable (rename / reorder). **Display names are unique**, enforced in the - pure model on create and rename: `createBank` / `renameBank` reject a name that - duplicates an existing bank's (renaming a bank to its own current name is a no-op - success). The comparison is **trimmed + case-insensitive (ASCII)**, so "Drums", - "drums", and " Drums " cannot coexist; the pool's reserved name "Pool" is protected - by the same check. Uniqueness makes by-name resolution in the action shell - unambiguous by construction. The pool is the first, seeded, fixed-id - member. `bank_book` is the mirror of `bank_model` and `view_mode_model`: pure, no - REAPER types, unit-tested outside the DAW, JSON round-trip. -- **Active bank lives in the model, routes through the capture path.** `bank_book` - carries the active bank id (defaults to the pool). The capture action family - resolves "which bank does this capture land in?" by asking the session for the - active bank's `BankIndex`, then adds exactly as today. No capture backend changes; - only the add-target is selected upstream. Activating a bank is a model mutation + - a persist write; it never touches the timeline. -- **Movement moves the index entry, not the file.** Moving a sample from - bank A to bank B is an **index-only** operation: remove the `Sample` from A's - `BankIndex`, add it to B's. The underlying file stays in the project bank folder — - banks are logical groupings over one shared file pool, not separate folders on - disk. This keeps movement cheap, non-destructive, and immune to path-rewrite bugs. - (Per-bank subfolders on disk are an explicit non-goal — see guardrails.) -- **Dedup-by-hash is per-bank.** Each `BankIndex` dedups within itself, unchanged. - Moving a sample whose hash already exists in the destination bank **collapses** - onto the existing entry there (the move is a no-op add on the destination side, - and the source entry is still removed) — the same collapse semantics - `BankIndex::add` already has, now observed across a move. Cross-bank dedup is - **not** enforced: the same hash may exist in the pool and in a named bank - simultaneously (that is the point — copy lets a sample be grouped into "Drums" - while still living in the pool). -- **Move vs. copy are distinct acts; move is the default.** *Move* removes from - source, adds to destination (one logical sample, regrouped) — it is the **primary, - low-friction gesture**, so a sample lives in exactly one bank at a time. *Copy* adds - to destination and leaves the source entry intact (same file, two index entries, two - banks) — the **deliberate secondary act** for the "in two places at once" case. Both - are index-only; both share the destination-collapse rule. Under move-as-default the - pool is the default home and staging ground, not a permanent superset: moving a - sample into a named bank takes it out of the pool. (See product notes for the - mental-model reconciliation.) -- **Delete drops members; evacuate returns them.** Deleting a **named** bank drops - its member index entries (files are **not** deleted — file lifecycle stays owned by - the capture/prune path). A separate **evacuate** operation moves all of a bank's - members back to the pool (index-only, same destination-collapse-by-hash as move), - leaving the bank empty. Intended workflow: *evacuate then delete* to keep the - samples, *plain delete* to drop the grouping and its members. A plain delete of a - non-empty bank orphans those members out of every index — their files persist on - disk until prune, referenced by no bank — so the UI **confirms on non-empty delete** - and offers evacuate as the alternative. Evacuate cannot be applied to the pool. -- **Persistence: a new ext-state key; the pool folds in and the legacy key is - retired.** The multi-bank state serializes to a new key `banks` in the existing - `"reasampler"` namespace, alongside `view_state` and `project_guid`. The pool's - index rides *inside* the `banks` blob as bank-zero — persisted identically to any - named bank (one blob, one section, one JSON shape). **Migration:** on load, if a - `banks` key is absent but a legacy `bank_index` key is present, the legacy index is - promoted into the pool inside a freshly-minted `banks` blob and the book is - `{ pool }` with zero named banks — a one-way, lossless promotion. After migration - the `banks` blob is **authoritative**; the legacy `bank_index` key is **retired** (not - written or read back going forward). The one-way retirement trades pre-multi-bank - backward-read compatibility for the clean single-blob shape — an accepted, - forward-only migration consistent with how M4 project state already moves forward. -- **Vertical-split UI, pool on top.** The bank window splits vertically: **pool on - top**, the **named-banks region below** (a tab-page strip, one tab per named bank, - empty when none exist). Two full-height toggles collapse the split: **pool - full-height** (hide the named-banks region) and **banks full-height** (hide the - pool). The Design View segmented mode switch already in the window header is - **orthogonal** and stays — it governs timeline visibility, not bank grouping; the - two coexist in the header/body without interaction. - -## Precision / invariant implications - -- **Relative-paths-only survives unchanged.** Every `BankIndex` in the book keeps - the relative-path invariant at its `add` boundary — the book adds no new path - handling, because movement is index-only and files never relocate. The invariant - is enforced N times (once per bank) by the exact code that enforces it today. -- **Non-destructive.** Bank create / rename / delete / activate / evacuate and sample - move / copy mutate only index + ext-state; no file is written, moved, or deleted, - and no timeline item is touched. Deleting a **named** bank drops its member index - entries but does **not** delete their files; file lifecycle stays owned by the - capture/prune path, not the bank container. A file referenced *only* by the deleted - bank becomes an orphan on disk — present but indexed by no bank — until the - capture/prune path reclaims it. That orphaned-until-prune window is designed, not - accidental; the *evacuate* verb and the confirm-on-non-empty-delete guardrail exist - to keep the user out of it unintentionally. -- **Travels-with-the-.rpp preserved.** The `banks` blob rides the same ext-state - namespace and the same GUID-primary project-identity / Save-As-relocation - machinery as the bank index does today (M4). One shared physical bank folder, one - ext-state namespace, now three logical sections (banks + view + identity). -- **Determinism / null-test / bit-identical are untouched** — they are properties - of the capture path and the file, and the multi-bank layer sits above the file - entirely. - -> Build detail for this phase (module architecture, API surface) moved to CONTEXT-ARCHIVE.md. - -## Non-goals / guardrails - -- **No per-bank folders on disk.** Banks are logical groupings over one shared - project bank folder. Do not create a subfolder per bank or move files on - bank-move — reject any such path in review (it reintroduces the path-rewrite - bug class M4 closed). -- **No cross-bank dedup enforcement.** The same hash may exist in multiple banks - (that is what copy is for). Do not add a global dedup that collapses across banks. -- **Pool privileges are inviolable.** No action path may delete or rename the pool, - leave a project with zero banks, or **evacuate** the pool (the pool is evacuation's - destination, not a source). Enforce in the pure rules layer, not just the UI. -- **Delete drops members; files are never deleted by a bank op.** Deleting a named - bank removes its member index entries only. No bank operation writes, moves, or - deletes a file — file lifecycle stays with capture/prune. The UI **confirms on - non-empty delete** and offers evacuate; do not silently orphan members. -- **Capture still never inserts into the arrange.** The load-bearing principle is - unchanged; multi-bank only redirects which index the capture lands in. -- **Additive only.** Do not alter `BankIndex`, the M0–M11 capture roadmap, or Phase - D semantics. `bank_book` wraps; it does not modify. -- **Verify API names** against the SDK header before use. - ---- - -# Sample removal — additive spec (Phase B, point B5) - -> **Additive section, part of the Multi-bank pillar.** The sample-level companion -> to move/copy/evacuate/delete-bank: a verb that **drops a `Sample`'s index entry** -> from a bank (or the pool). Index-only, non-destructive to the file — it sits on -> the same side of the index/file line as every other Phase B op. Product framing: -> `docs/product/removal-and-prune.md` §Sample-remove. Same verify discipline: -> **verify every REAPER API name/signature against the SDK header before use.** - -## What it is - -Move, copy, and evacuate all keep a sample *somewhere*; there was no verb to drop -a sample outright. **Sample-remove** is that verb: it removes one `Sample` entry -from one `BankIndex`. It exposes the `remove` primitive `bank_model`'s `BankIndex` -**already has** — B5 wires it to an action + a panel affordance, it does not add a -model capability. - -## Settled decisions (spec-level) - -- **Remove is index-only.** It removes the `Sample` from a `BankIndex` and mutates - only index + ext-state. No file is written, moved, or deleted; no timeline item - is touched. Identical non-destructive posture to move/copy/evacuate/delete-bank. -- **Remove can orphan a file — the same designed orphaned-until-prune state a - non-empty delete-bank produces.** When remove drops the *last* index reference to - a file (no other bank holds its hash), that file becomes an orphan on disk, - referenced by no bank, reclaimed later by **prune** (Phase R) — never by remove. - This is not a new hazard class; it is the existing "files persist until prune" - window, reached by a sample-level verb instead of a bank-level one. -- **Collapse-by-hash is unaffected.** Remove targets a specific entry in a specific - bank. Because cross-bank dedup is deliberately not enforced, removing a sample - from one bank leaves any same-hash entry in another bank intact — the same - coexistence copy relies on. -- **The pool's *contents* are removable; the pool *container* is not.** Pool - privileges (un-deletable, un-renamable, un-evacuable) govern the pool as a - container. Individual samples **can** be removed from the pool — otherwise the - pool would be a one-way trap. Remove-from-pool is the pool's own "drop this - sample" verb and is allowed. -- **Remove scope (fork R-A, SETTLED 2026-07-24 — this-bank).** Remove drops the - entry from *this* bank only, leaving copies in other banks untouched — the core - and only shipped verb. The action carries a `scope: this-bank | all-banks` seam, - but **this-bank is the settled default and the only surfaced affordance**; - all-banks stays a latent parameter (promotable later behind the seam without a - rewrite), never a surfaced verb now. See product notes §Fork R-A. - -## Precision / invariant implications - -- **Non-destructive** extends to remove verbatim: index + ext-state only, no file - touched, no timeline item touched. -- **Relative-paths-only** is unaffected — remove deletes an entry, it adds no path - handling. -- **Determinism / bit-identical / null-test (capture)** untouched — remove sits - above the file, same as all of multi-bank. - -## Guardrails - -- **Removes are silent — no confirm dialog.** Recoverability is provided by the - batched REAPER undo (R-B): one Ctrl-Z restores the index entry, whether or not - the sample was a last reference. Files are never deleted by remove - (orphaned-until-prune is unchanged). `hashReferencedElsewhere` is a tested - model API retained for Phase R prune; it has no shell caller in the remove path. -- **Undo (fork R-B, SETTLED 2026-07-24 — batched REAPER undo points, Phase-B-wide).** - Bank/index mutations integrate into REAPER's undo system as **batched undo points** - (`Undo_BeginBlock` / `Undo_EndBlock`): the related index mutations of one bank - operation are batched into a single undo point, so one bank operation is one - Ctrl-Z. This is a **Phase-B-wide** decision — it applies to - create/rename/reorder/delete-bank, move, copy, evacuate, *and* remove, retro- - touching B1–B4, not just B5. **Must-verify before build:** confirm against - `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly - in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on - it. Surfaced with remove because remove is the first verb whose *only* effect is - index-entry destruction with no relocation, so it is where the gap first bit; the - fix is shared. See product notes §Fork R-B. - -> Build detail for this phase (module architecture, API surface) moved to CONTEXT-ARCHIVE.md. - -## Settled forks (Daniel, 2026-07-24) - -- **Fork R-A — remove scope.** Settled: **this-bank** (this-bank-primary, all-banks - a latent seam-only parameter). Folded into Settled decisions above. -- **Fork R-B — undo model for index mutations.** Settled: **batched REAPER undo - points** (`Undo_BeginBlock`/`Undo_EndBlock`), Phase-B-wide (retro-touches B1–B4), - with the ext-state-participation SDK check as a must-verify-before-build. Folded - into Guardrails above and the Phase B / B1 plan points. - ---- - -# Prune — file-lifecycle spec (Phase R — Reclaim) - -> **New pillar, its own lettered phase.** Prune is the file-lifecycle path the -> capture and multi-bank specs forward-reference throughout ("files persist on disk -> until prune", "the capture/prune path reclaims it") but that had no phase, module, -> or point until now. It is the **only** operation in ReaSampler that deletes bytes -> off disk. Namespaced **`R` (Reclaim)** alongside `M` (capture), `D` (Design View), -> `B` (Banks) — it is a distinct pillar, not a Multi-bank sub-step, because it -> serves *every* orphan-producing path (delete-bank, sample-remove, re-capture) and -> carries a new risk class (file deletion) with its own invariants. Product framing -> and the phase-placement justification: `docs/product/removal-and-prune.md` §Prune. -> Same discipline: **verify every REAPER/SWELL/filesystem API name/signature against -> the SDK/SWELL headers before use.** - -## What it is - -Over a project's life, delete-bank and sample-remove (and, potentially, M10 -re-capture superseding an old file) leave `.wav` files on disk that no bank index -references — the "orphaned-until-prune" state the specs design in on purpose. -**Prune is the reclaim pass**: reconcile the physical bank folder against the union -of every bank's index, and reclaim the files nothing references. It makes good on -the promise the rest of the spec keeps making. - -## The load-bearing rule - -> **Remove creates orphans; prune reclaims them.** Sample-remove and delete-bank -> drop index entries and may leave a file referenced by nothing. Prune is the -> single path that turns such an orphan back into free disk space. **No other -> operation deletes a file; prune deletes *only* files that no index references.** -> A bank op that deletes a file is still a bug — prune is not a bank op, it is the -> file-lifecycle op. - -This asymmetry is deliberate and must be stated loudly: every *other* invariant -says "no operation deletes a file." Prune is the sole, explicit exception, and its -entire job is deletion — so it must be the *only* file-deleting authority in the -system, with the strongest guardrails. - -## Mirror of `reconcile` — the pure pattern one level down - -Prune reuses the shape Design View already shipped. `view_mode_model`'s -`ViewModeModel::reconcile(liveGuids)` reconciles *membership entries* against *live -tracks* and returns the residuals to drop. **Prune reconciles *files on disk* -against *referenced files*** (the union of every bank's index) and returns the -orphan set to delete. Same pure pattern, one level down (files instead of GUIDs). - -The **decision is pure and unit-tested**: given the set of files present in the -bank folder and the set of files referenced by the book, compute the orphan set. -Only the two ends touch the shell — *enumerating* the bank folder and *deleting* -the orphans are filesystem I/O. Keep the "which files are orphans" core REAPER-free -and hard-tested (this is the safety-critical part); keep the I/O thin. Same -pure/shell split as `bank_model` / `view_mode_model` / `bank_book`. - -## Settled decisions (spec-level) - -- **Referenced-set is the union across ALL banks, pool included.** A file is an - orphan iff **no** bank in the book references it. Because copy lets one file be - referenced by several banks, prune must union references across the whole book - before deciding. This is the safety-critical computation — the **prune null - test** is *prune never deletes a file that any index references.* -- **Project-relative resolution, current folder.** Prune enumerates and deletes - within the project bank folder using the **same M4 project-relative path - resolution** the index uses, against the *resolved current* folder — never a - stale absolute path — so a Save-As relocation cannot cause it to mis-identify or - mis-target orphans. -- **Dry-run first, always.** Prune reports before it deletes: the orphan count, - reclaimed size, and (for a small set) the files. The dry-run — compute-and-report, - the pure core with no deletion — is the primary surface; actual deletion is the - confirmed second step. A prune that silently sweeps is unacceptable for an - irreversible file-delete. -- **Scope is the bank system's own leavings, not the folder at large.** Prune - reclaims files that *were* bank files and are now unreferenced — never a file a - user hand-dropped into the folder. Prune is a reclaimer of ReaSampler's own - orphans, not a general folder cleaner. -- **Orphan attribution is an owned-file manifest (fork R-D, SETTLED 2026-07-24).** - The book tracks the set of files it has created (an **owned-file manifest**); - prune reclaims `(owned ∩ on-disk) − referenced`. This is the honest encoding of - "reclaim only our own leavings" and rejects folder-sweep (which would delete - hand-dropped files). **The seam lands early:** because the manifest is cheap to - maintain from capture onward but a backfill cliff to reconstruct later, **capture - writes each file it creates into the owned-file manifest starting in Phase B**, - even though prune consumes it only in Phase R. R1/R2 consume the manifest; they do - not build it. The manifest is persisted in the `"reasampler"` ext-state; the exact - persistence shape (a sibling key vs. folded into the `banks` blob) is a small - build-time residual, but the manifest-now decision is firm. - -## Precision / invariant implications - -- **The single intentional exception to "no operation deletes files."** Stated - above; called out again here so the invariant table is honest: prune is - destructive-to-files *by design and by exclusive authority*. -- **Relative-paths-only / Save-As machinery reused** — prune resolves paths the - same way the index does (M4), so it inherits relative-path correctness and - Save-As survival; it introduces no new path handling. -- **Determinism / bit-identical / null-test (capture)** untouched — prune sits - below the capture path entirely. -- **Prune null test (new invariant):** a prune of a folder whose every file is - referenced by some bank deletes nothing; a prune deletes exactly the - `present − referenced` orphan set and nothing else. Ship as a tested property of - the pure core. - -## Guardrails — the genuinely destructive act - -- **Dry-run + confirm-with-manifest** (above): the user approves a *specific* - deletion (count + size + files), never an abstract "clean up." -- **Never a referenced file; never a non-bank file.** The union-across-all-banks - rule protects referenced files; the ownership-attribution rule (fork R-D) - protects hand-dropped files. -- **Safest platform deletion available (fork R-C, SETTLED 2026-07-24 — trash- - preferred, unlink fallback).** Route deletions to the platform recycle bin / trash - wherever a portable move-to-trash is available (recoverable outside the app); fall - back to unlink — behind the dry-run + confirm guardrail — only where the platform - affords no portable trash. "Delete where possible" means recoverable-trash- - preferred, never plain unlink-by-default. The move-to-trash surface is an explicit - per-platform **to-verify** (see REAPER/platform API surface). (R3 verified: Windows - `SHFileOperationW` + `FOF_ALLOWUNDO` confirmed against SDK 10.0.26100; macOS/Linux - unlink fallback — no portable SWELL trash surface.) -- **Manual, explicit trigger (fork R-E, SETTLED 2026-07-24 — manual action + panel - button).** Prune runs via a bindable manual action (dry-run-first, confirm-to- - delete) **and** a `bank_panel` button that fires that same action — never a silent - background sweep. The earlier optional "…and prune now at the delete-bank - confirmation" convenience was **not** selected and is out of scope; a periodic - background sweep remains rejected (silent irreversible file-deletion violates the - guardrails). - -> Build detail for this phase (module architecture, API surface) moved to CONTEXT-ARCHIVE.md. - -## Non-goals / guardrails - -- **Prune is the ONLY file-deletion authority.** No bank op, no capture op, no - Design View op deletes a file. If any path other than prune deletes a bank file, - reject it in review. -- **No general folder cleaning.** Prune reclaims the bank system's own unreferenced - leavings, not arbitrary files a user placed in the folder (fork R-D governs the - attribution). -- **No silent deletion.** Dry-run + explicit confirm always; no background sweep. -- **No file deleted while any index references it.** The referenced-set union - across all banks is the safety-critical invariant — enforce and test it in the - pure core, not just the UI. -- **Additive only.** Prune reads the book and the folder; it does not modify - `BankIndex`, `bank_book`, the capture roadmap, or Design View semantics. - -## Settled forks (Daniel, 2026-07-24) - -- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.** - Route to the OS trash where a portable move-to-trash is available (recoverable), - else unlink behind the dry-run/confirm guardrail. Per-platform trash surface is a - must-verify. Folded into Settled decisions + Guardrails + API surface above. - Product notes §Fork R-C. -- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, `(owned ∩ - present) − referenced`; folder-sweep rejected as unsafe. The **seam lands early** — - capture writes each created file to the manifest starting in Phase B, prune - consumes it in Phase R. Persistence shape (sibling key vs. `banks` blob) is a - build-time residual. Folded into Settled decisions + Module architecture + API - surface above, and added as an up-front Phase B / capture plan point. Product - notes §Fork R-D. -- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**, dry-run- - first, confirm-to-delete; no background sweep. The delete-time "…and prune now" - convenience was not selected (out of scope). Folded into Guardrails + Module - architecture above and the R3 plan points. Product notes §Fork R-E. - -**Build-time residual (not a fork):** the owned-file manifest's exact persistence -shape (sibling `"reasampler"` ext-state key vs. folded into the `banks` blob). - ---- - -# MIDI-playback instrument — additive phase spec (Phase S — Sampler) - -> **New pillar, its own lettered phase, and — uniquely — its own build artifact.** -> Every prior phase (M / D / B / R / V) ships inside the one `reaper_reasampler` -> extension binary. Phase S does **not**: a REAPER extension *cannot* be a -> MIDI-triggered instrument (it is not a node in any track's signal chain), so the -> instrument is a **second, separate binary** — a native **VST3** plugin the user -> instantiates on an instrument track — that reads ReaSampler's banks and plays them -> MIDI-triggered. Namespaced **`S` (Sampler)** rather than "D" (which would collide -> with Design View). The `M`/`D`/`B`/`R`/`V` extension pillars are untouched. Product -> framing, the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, and the -> settled decision record: `docs/product/midi-playback.md`. Same standing discipline: -> **verify every Steinberg VST3 SDK and REAPER/SWELL API name/signature against the -> vendored headers before use.** - -## What it is - -A native **VST3 sampler instrument** — a separate product/artifact from the -extension — that maps ReaSampler's captured bank samples across a MIDI keyboard and -plays them back with a real voice engine (polyphony, velocity, envelopes). The -extension stays the sole owner of **capture + organization**; the instrument is the -**playback surface**. The two are *tightly integrated but distinct acts*: the -extension captures and organizes; the instrument plays. Neither crosses into the -other's role — the instrument never captures, the extension never becomes an -instrument. - -**Why a VST3 and not the extension (load-bearing, settled — see D1/D5/D6 below).** An -instrument track's "read live MIDI, emit audio per-voice, in REAPER's -routing/record/render path" contract belongs to VST/VST3/CLAP/JSFX plugins, hosted -through an entirely different mechanism than the extension API. The extension SDK's -audio-adjacent surfaces (`Audio_RegHardwareHook`, `kbd_OnMidiEvent`, `PlayPreview`, -`pcmsrc` subclassing) are each the wrong tool for a live-MIDI instrument — the full -reasoning is in `docs/product/midi-playback.md` §1. The instrument is therefore a -standard VST3 plugin; this is not an engineering-around-able limitation, it is what -the plugin *format* is. - -## The three locked decisions this spec assumes - -Settled by Daniel (2026-07-26); everything below assumes them. Reasoning preserved in -`docs/product/midi-playback.md` §4. - -- **D1 — native VST3.** Not JSFX. Full sampler sophistication, clean integration, and - access to the REAPER VST-host bridge. JSFX retired (cross-platform-for-free is - worthless under D5, and JSFX gets no bridge). -- **D5 — Windows-only, VST3-only, REAPER-only.** No cross-platform DSP/build/signing - matrix, no multi-format wrapper, no standalone-in-other-hosts concern. REAPER-coupling - via the bridge is intended. This is the single biggest simplifier — it deletes most of - what makes VST3 painful. -- **D6 — two products, tightly integrated.** A separate artifact, but **not** a - divorced file-only companion: via the VST-host bridge it reads the live - `"reasampler"` project ext-state and is project-aware. - -## The VST-host bridge (the integration mechanism, stated once) - -A VST3 *hosted inside REAPER* can call back into REAPER's own API by resolving -function pointers by name over the host callback -(`hostcb(&effect, 0xdeadbeef, 0xdeadf00d, 0, "FunctionName", 0.0)` — the same -string-keyed API table the extension uses; verified in `video_processor.h` and the -`reaper_plugin_functions.h` `GetProjExtState`/`SetProjExtState`/`EnumProjExtState` -entries). The plugin can also fetch its **host context** — the track/take/project it -was instantiated in (opcode `0xdeadf00e`). **Consequence:** the instrument reads the -*same* live `"reasampler"` ext-state that `persist` writes, follows the active -project, and needs no "point me at the bank folder" wiring — it asks REAPER which -project it is in. This capability exists *because* the plugin is hosted in REAPER; it -is the technical affordance D6 leaned on. **Must-verify before build:** confirm the -bridge opcodes and the by-name resolution against the vendored -`vendor/reaper-sdk/sdk/` headers (`reaper_plugin.h`, `video_processor.h`, -`reaper_plugin_functions.h`) — the framing doc's opcode citations are verified from -those headers but the exact call marshalling should be confirmed at the spike. - -## The two seams (audio via files, mapping via live state) - -- **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float - WAVs — project-relative, travelling with the `.rpp` via the M4 machinery. The - instrument resolves those paths **the same way `persist` does** (a shared convention, - not a re-implementation). There is no live PCM stream across the bridge, by design. -- **Live-state seam (the mapping, via the bridge).** For everything that is *not* raw - audio — the bank index, the mapping, which project is active — the instrument reads - the live `"reasampler"` ext-state via the bridge. It sees what `persist` last wrote - and follows the active project. - -## The seam fields — what becomes a bank intrinsic (D-B, settled 2026-07-26) - -**The split model (option iii) is the settled answer.** It mirrors the -capture/placement separation: - -- **Bank intrinsics (facts about the captured file) live on `Sample`.** **Root note** - (the MIDI note the sample was recorded at, so it can be repitched across the - keyboard — distinct from the existing optional *musical key* field) and **loop - points** (sustain-loop start/end for held notes; sample-accurate, zero-crossing-aware) - are *facts about the file*, analogous to sample rate, length, and peaks. They are added - to `Sample` as an **additive field extension** — the same shape as how `provenance` - was added in M1: new optional fields with JSON round-trip, populated at/after capture, - defaulting cleanly for pre-existing samples. This keeps the bank a clean, - tool-agnostic library (WAVs + facts, readable by anything). -- **The performance map (a creative arrangement) lives in the instrument.** **Key - zones** (low/high note per sample), **velocity layers**, **round-robin groups**, - **amplitude envelopes** (ADSR), and per-sample tuning/gain trim are a *performance - choice*, not a fact about a file — they belong to the instrument, not the bank. Under - the live-state seam the instrument may still *read* performance-map data out of shared - `"reasampler"` state, so "who owns which field" is a data-ownership decision, not a - transport one. - -**Why the intrinsic fields are added early (D-B, the backfill-cliff reasoning).** The -`Sample` field addition is scheduled as an **early Phase S point** even though the -instrument that consumes them lands later. Rationale (the *design-the-seam-even-if-you- -defer-the-feature* instinct, same as Fork R-D's owned-file manifest): if the fields are -added only when the instrument needs them, every sample captured before then lacks a -root note / loop points and must be backfilled by hand. Adding the fields now — so -capture starts populating them (or at least defaulting them cleanly) — costs almost -nothing and closes the cliff. The field addition touches the **extension** codebase -(`bank_model` + capture + persist), is independently shippable, and lands before the -instrument build leans on it. - -## Scope tiers (D-C, settled 2026-07-26 — Tier 0–1 committed, Tier 2 held, Tier 3 optional-forever) - -Tiers are minimal → sophisticated; **Tier 0 delivers the core promise** and each tier -above is optional depth, not a prerequisite for the one below. - -- **Tier 0 — "the bank plays" (committed).** One sample mapped chromatically across - the keyboard from its root note; basic polyphony; a simple amp envelope; velocity → - volume. The honest MVP: point a bank sample at a MIDI track and play it repitched. - On the native path this is the `SingleComponentEffect` skeleton plus a single-voice - core, editor deferrable behind a parameters-only default view. -- **Tier 1 — "a keymap" (committed).** Multiple samples zoned across the keyboard (key - ranges), each with its own root note — a captured *kit* or a *multisampled instrument* - plays correctly. This is where the root-note + key-range seam fields earn their place. - One sample per key-region. -- **Tier 2 — "expressive" (HELD — noted, not specified).** Velocity layers, round-robin - (the anti-machine-gun feature), full ADSR, per-sample tuning/gain trim, sustain loops. - Where it becomes a tool people reach for. **Explicitly a follow-on** — its points are - not drawn up in this spec; it is recorded as the next depth increment once Tier 0–1 - proves the instrument belongs in ReaSampler's world. -- **Tier 3 — "instrument polish" (optional-forever).** Filters, filter/pitch envelopes, - LFOs, per-voice pan, choke groups, a modest FX slot. A direction to leave room for, - never a commitment. Do **not** let a Tier-3 feature list inflate the build-shape - decisions. - -## The build shape (D-A, settled 2026-07-26 — bare Steinberg VST3 SDK + LICE editor) - -**Settled: bare Steinberg VST3 SDK, no JUCE, with the editor drawn in the same -LICE/SWELL stack `bank_panel` already uses.** Reasoning (full assessment in -`docs/product/midi-playback.md` §1a and §4 D-A): - -- **The audio-processing scaffolding is bounded.** Using `SingleComponentEffect` (the - SDK's combined processor+controller base — sanctioned for a non-distributable, - REAPER-only plugin under D5/D6) plus the SDK's factory macros, a silent-but-loading - VST3 instrument skeleton is order-of-magnitude a few-hundred lines of - adapt-from-example ceremony, written once. The AGain / Note Expression Synth SDK - examples are the copy-source. Not a tar pit. -- **D5 deletes JUCE's biggest justification.** JUCE exists largely for multi-format / - cross-platform, both of which D5 removed. Its one genuine remaining pull is the editor - UI — and ReaSampler is the atypical case where even that is weak, because it already - has a working, docked, custom-drawn LICE UI (`bank_panel`) and a house style. Drawing - the editor in a VST3 `IPlugView` that hosts a LICE surface reuses that muscle, keeps - the look house-consistent, and avoids JUCE's AGPL-or-pay license posture (the Steinberg - SDK is permissive, no revenue gate). -- **The one real edge — the `IPlugView`↔LICE bridge** (window lifecycle, sizing, event - routing from the host into the draw/hit-test loop) — is *the same class of work* - ReaSampler already did to dock `bank_panel`, not a new competence, but it is less - trodden than dropping in a JUCE editor. It is therefore the phase's **opening spike** - (below), which also converts §1a's experienced-estimates (Windows module-export - symbol names, factory-macro spellings, exact bridge marshalling) into verified fact - before the engine build leans on them. VSTGUI (the SDK's bundled toolkit) is the noted - fallback rung *only if* the LICE bridge proves gnarlier than the panel work suggests; - JUCE is the last resort behind that. - -## The pure core (D3 — the load-bearing split, transplanted) - -**The sampler's voice engine, envelope math, key/velocity mapping, repitch/interpolation, -and keymap resolution are a pure, REAPER-free, DAW-free, unit-tested module** — the -mirror of `bank_model` / `peaks` / `view_mode_model` / `bank_book`, tested in the CTest -harness outside any host. This is the heart of the phase; **test it hard.** The VST3 -wrapper — the `SingleComponentEffect` subclass, bus setup, the `process` call -marshalling MIDI→core and core→audio-buffer, the `IPlugView` LICE editor, and the bridge -calls that read `"reasampler"` ext-state — is the **thin shell**, the only part that -touches VST3 or REAPER at all. Critically, this split is **invariant under the build-shape -choice**: whether the shell is bare-SDK or (hypothetically) JUCE, the pure core is -identical, REAPER-free, and tested the same way. The format choice is a shell choice; the -core is invariant. - -> Build detail for this phase (module architecture, WDL API surface, build sequencing, the superseded S10 workflow hierarchy, Steinberg API surface, the S17 drop-and-load spec) moved to CONTEXT-ARCHIVE.md. - -## Precision / invariant implications - -- **The bank is one source; the instrument is another view of it (never a fork).** The - instrument is a pure *consumer* of the bank — it does not copy samples, does not own a - private sample store, and does not mutate the bank. The bank stays the single - authoritative artifact (the one-source-multiple-views instinct). Any instrument path - that writes back into the bank or keeps its own sample copies is a bug. -- **Capture/placement/playback stay distinct acts.** The instrument reads and plays; it - never captures and never inserts into the arrange. The capture load-bearing principle - is untouched — Phase S adds a *third* distinct act (playback) without weakening the - capture↔placement separation. -- **`Sample` field addition is additive and lossless.** Root note + loop points are new - optional fields with JSON round-trip, defaulting cleanly for samples captured before - the addition — the same additive, backward-compatible shape as `provenance` (M1). No - existing `Sample` field changes; no `BankIndex` behavior changes. -- **Relative-paths-only survives.** The instrument resolves audio via the M4 - project-relative machinery; it introduces no absolute paths. - -## Embedded TCP/MCP UI (D-D, settled 2026-07-26 — SCHEDULED as a later Phase S point) - -A REAPER-hosted VST3 can draw its own UI **inline in the track/mixer control panel** -via `reaper_plugin_fx_embed.h` (the plugin implements `IReaperUIEmbedInterface`; the -same Cockos surface REAPER's own embedded FX use). A ReaSampler instrument can render a -compact keymap/level strip inline in the TCP/MCP, not only in its own window. Because -this uses the **same LICE-class drawing as the D-A editor path**, it composes naturally -with the bare-SDK-plus-LICE build — the groundwork is the groundwork. - -**Settled: scheduled, not deferred.** This is a real, in-phase later point — it lands -**after** the main `IPlugView` editor exists (it composes with that LICE path), not a -someday-note. It is polish, not a Tier-0 need, so it sequences last in the phase; but it -is on the roadmap. **Must-verify before build:** the `IReaperUIEmbedInterface` contract -and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`. - -## Channel mode — mono | stereo (D-E, decided 2026-07-26; PLAN.md S7) - -**Decided direction: the instrument gets a per-instance channel-mode toggle — 1 (mono) or -2 (stereo) — that negotiates the REAPER audio bus automatically.** Captures are often -stereo; the current mono downmix is a Tier-0 simplification, not a permanent shape. - -- **Mono mode keeps today's path.** The decode-side downmix stands: a stereo source in - mono mode downmixes (the existing policy), a mono source plays as-is. No engine change - for mono. -- **Stereo mode is an S3-core extension, not a shell hack (honest).** The S3 core is - **mono-per-sample by design today** — `SampleData::frames` is one mono stream, - `Voice::renderFrame` returns a single value, `VoiceEngine::render` writes one channel. - Stereo mode grows the core a **channel dimension**: 2-channel decoded PCM, per-voice - **stereo** render (per-channel fractional read + linear interpolation + loop), and a - per-channel mix in the engine. Mono stays the degenerate (single-channel) case, so - existing mono behavior is unchanged. This is why S7 sequences first after the editor/embed - work: it touches the engine Daniel smoke-tests. -- **Where the toggle lives.** Per-instance component state (setState/getState), alongside - the selected sample — a **performance choice the instrument owns**, never written to the - bank (D-B: not a file fact). Default preserves current behavior (mono). -- **Cross-mode policy (settled).** Mono source + stereo mode → **dual-mono** (same signal - both channels, centered). Stereo source + mono mode → **downmix** (the existing - decode-side policy). The bank's per-sample channel-count intrinsic (already on `Sample`) - tells the shell how many channels to decode into `SampleData`. -- **Bus negotiation (the "works with the REAPER bus automatically" requirement).** The VST3 - implements `setBusArrangements` so the output bus reports mono or stereo per the - instance's channel mode, and REAPER's routing follows without manual channel wiring. - **Must-verify before build:** the `setBusArrangements` / `getBusArrangement` contract and - REAPER's mono/stereo instrument-bus expectations against the vendored Steinberg SDK + - `reaper_vst3_interfaces.h`. - -## Sampling modes — Trigger vs Gate + pitch envelope (S15/S16; core + editor) - -**Daniel's directive (2026-07-26, verbatim):** *"Sampling mode: Trigger vs Gate. Gate has -an AHDSR envelope. Trigger has fade in, % length, and fade out. Both modes have modifiable -start point, Gate has modifiable loop points too. In addition to amp env, there will be a -pitch envelope/curve (AD?) which is off by default."* The feature set is **settled**; two -forks (S15-F1 choke, S15-F2 param granularity) are flagged with leans below. - -**Daniel's S16 correction (2026-07-26, verbatim):** *"isn't that ratio stuff going to change -the playback rate? I want duration-preserving repitching."* Correct — the `readPos_ += ratio_` -path is **varispeed** (pitch and duration coupled). S16 is revised from "pitch envelope only" -into a **pitch-engine mode (Varispeed vs Preserve) + pitch envelope** (see §Pitch engine -modes below). Two new S16 forks are flagged: **S16-F1** (the engine default — lean Preserve) -and **S16-F2** (the Preserve implementation — lean `WDL_SimplePitchShifter` first, hand-rolled -held). The prior WDL finding that dismissed `WDL_SimplePitchShifter` is **corrected in place** -below (duration-preserving is now the requirement, so that shifter is the Preserve candidate). - -### Play mode — Gate vs Trigger (S15) - -Each played sample carries a **play mode** — a per-sample/per-zone **performance choice** -(D-B, instrument-owned, never a bank fact). Two modes, precisely: - -- **Gate — classic held note (grows the current path).** Note-on enters the amp envelope; - note-off enters release; a **sustain loop** applies for held notes (S11's draggable loop - markers are Gate-mode UI). The current core envelope is **ADSR**; Gate adds a **Hold** - stage → **AHDSR**: `0→1` over attack, **hold at 1** over `holdFrames`, `1→sustain` over - decay, hold sustain until note-off, `level→0` over release. **`holdFrames == 0` is - exactly today's ADSR** — a back-compat degenerate, no behavior change for existing Gate - play. Segment math is the existing linear-ramp idiom (`AdsrEnvelope::tick`) with one new - stage inserted between Attack and Decay. -- **Trigger — one-shot drum-pad.** Note-on fires playback of a defined **% of sample - length** with a **fade-in** and **fade-out** ramp; **note-off is ignored** (the voice - plays through); **no sustain loop**. Envelope math (distinct from AHDSR): play the frame - span `[startFrame, playEnd)` where `playEnd = startFrame + round(lengthFraction·(frames − - startFrame))`, `lengthFraction ∈ (0,1]`; amplitude ramps `0→1` over `fadeInFrames` - (fade-in) at the head and `1→0` over `fadeOutFrames` anchored to `playEnd` (fade-out), - unity between; fades clamp so `fadeInFrames + fadeOutFrames ≤ play length`. The voice - frees when `readPos_ ≥ playEnd` (mirror of the current run-off-end idle). **Fade curve - default: equal-power** (constant-power `sin`/`cos` — click-free on one-shots); linear is a - build-time residual. **Note-off in Trigger is a no-op** (choke is held — fork S15-F1). - -**Both modes: modifiable start point.** Playback begins at `startFrame` (a frame offset into -the sample, clamped `0 ≤ startFrame < frames`), not always frame 0. This is the voice's -initial `readPos_`; the existing per-frame `readPos_ += ratio_` read and linear-interp/loop -machinery are otherwise unchanged. Gate additionally has **modifiable loop points** (already -the S2 loop intrinsic + S11 override); Trigger has none (it is a one-shot). - -**Voice-stealing interaction (unchanged).** The S3 stealing policy (oldest-in-release, else -oldest-overall) is mode-agnostic — a Trigger one-shot is a normal active voice until it runs -off `playEnd`; it can be stolen like any voice. No new stealing rule. - -**Confirmed from the core (`sampler_core.cpp`):** the read loop advances `readPos_` by an -arbitrary `ratio_` per frame with 2-point linear interpolation, and the amp is a per-frame -`env_.tick()` multiply — so both the AHDSR hold stage and the Trigger fade/%-length envelope -are **per-frame amplitude functions** over the existing read machinery, and the start point -is just a non-zero initial `readPos_`. No resampler or voice-lifecycle rewrite is needed. - -**Parameter ownership (D-B).** The play mode + its params (Gate: AHDSR; Trigger: %-length + -fade-in + fade-out; both: start point) attach to the **capture selection / zone** and live -in the instrument's **performance map** (component state, version-bumped, back-compat: a -truncated/older blob defaults to **Gate, hold=0, start=0, no fades = exactly today**). Start -point joins `rootOverride` / loop-override as another per-`PerformanceZone` optional -override; a per-zone `PlayMode` + param struct is added additively. **Fork S15-F2 (flagged):** -per-capture-selection *and* per-zone, or per-zone only with the single-capture case as a -one-zone map? **Lean: per-zone only** — the single capture is already a one-zone map -(S10-Z's back-compat lift), so one storage site serves both; flagged because it touches -S10's single-capture setup surface shape. - -**Editor (mode-aware, on the S11 waveform surface).** Gate shows draggable **start + loop -markers**; Trigger shows **start + %-length end + fade-in/out** handles — same waveform, same -pure `frame↔pixel` + marker-grab geometry module (S11), mode switches which markers draw. A -**mode toggle** per capture/zone sits in the S10 guided setup / S10-Z Zones panel. Every edit -commits **off-thread** via `commitAndReload`; the instrument stays a **read-only bank -consumer** (mode/params are performance map, never written to `Sample` or the bank). - -### Pitch engine modes — Varispeed vs Preserve (S16) - -**Daniel's correction (2026-07-26, verbatim):** *"isn't that ratio stuff going to change the -playback rate? I want duration-preserving repitching."* Correct: the `readPos_ += ratio_` -resampling path is **Varispeed** — pitch and duration are coupled (an octave up halves the -note's duration). Daniel wants **duration-preserving** repitch. So S16 grows a per-voice/ -per-zone **pitch-engine mode**, not just a pitch envelope: - -- **Varispeed engine (current path).** `ratio_ = pitchRatio(note,root)`, `readPos_ += ratio_` - with 2-point linear interp — resampling that couples pitch and duration. This is the - **classic sampler / RS5K** behavior and today's shipped S3/S5 output. Cheap, zero-latency. - Musically right for **drums / one-shots** (pitch-down-lengthens-the-hit is a feature there). -- **Preserve engine (duration-preserving).** The read advances at the **source** rate - (duration held) while a **pitch shifter** transposes the output by `2^((note−root)/12)`. - Musically right for **tempo-locked loops and phrases** — a transposed loop still lines up to - the bar. Since captured banks are project slices (loop/phrase-heavy), this is the default - lean (fork S16-F1). - -Mode is **per-`PerformanceZone` performance state (D-B)** — instrument-owned, never a bank -fact — additive/version-bumped (absent/older blob → the S16-F1 default). A per-zone -**Varispeed/Preserve toggle** surfaces in the S10 guided setup / S10-Z Zones panel. - -**Preserve engine implementation (fork S16-F2).** Two RT-disciplined routes behind the -`PitchEngine::Preserve` seam (identical contract either way): -- **(a) `WDL_SimplePitchShifter`** (`vendor/WDL/WDL/simple_pitchshift.h`) — a per-voice - time-domain OLA shifter. Under the duration-preserving directive this is **the right - category** (see the corrected WDL finding below). `set_shift(2^(semi/12))` for pitch, - `set_tempo(1.0)` to hold duration — pitch and duration are separately controllable. **Lean: - route (a) first** (low-cost proof), with two costs owned in the build: an inherent - **onset latency** (~half-window, ~25 ms @ the 50 ms quality-0 window; pre-warm at voice- - allocation, and it lands on sustained/loop material where least harmful) and a **queue-growth - allocation** hazard in `BufferDone` (`WDL_Queue::Add`) that is settled by a silence pre-warm - at voice-allocation so no `process`-thread allocation occurs in steady state. -- **(b) hand-rolled pure `pitch_shift` OLA/granular module** (house pattern — CTest-testable, - no REAPER/VST3/WDL type at the boundary) — **held** as the quality/latency upgrade if the - SimpleWindowed warble or onset lag proves musically unacceptable. - -**`WDL_Resampler` is not a Preserve engine** — it is a *resampler* (couples duration); it -remains a held **Varispeed-quality** upgrade only. **elastique is NOT available** (licensed -zplane, not vendored — restated). JUCE / rubberband / signalsmith are **new-dependency forks -carrying D-A weight** (bare-VST3-no-framework is the locked D-A) — **not proposed**. - -**S15 × S16 interaction (Preserve consumes S15's source-frame read).** S15's amplitude -semantics are defined over the voice's **source-frame** timeline; the Preserve engine wraps -that read and transposes the output, so: -- **Trigger %-length** stays a source-frame fact (`playEnd = start + round(lengthFraction· - (frames − start))`); under Preserve its **wall-clock is stable under transpose** — *cleaner* - than Varispeed, where transposing a Trigger also scales its audible length. -- **Gate sustain loop** — under Preserve, **loop the source read** (the `[loopStart, loopEnd)` - source-frame region) and feed the looped stream into the shifter, which transposes the - **output**. Contract: *loop the source, shift the output*; loop points stay source-frame - facts (S11 markers unchanged). Under Varispeed the loop read itself carries the pitch. -- **Start point** is a source-frame offset in both engines (engine-independent). - -### Pitch envelope — AD, off by default, engine-aware (S16) - -A per-voice **pitch modulation curve** riding on top of whichever engine — a short **AD** -(attack-decay) envelope that biases pitch over time. **Off by default** (so existing playback -is bit-identical under the same engine). The classic use is a percussive **pitch drop**. - -- **Shape (lean, build-time residual): two-segment AD** — at note-on the pitch offset rises - to `peakSemitones` over `attackFrames`, then falls to 0 (base pitch) over `decayFrames`. - A **zero attack** gives the pure "start high, drop to base" percussive drop. -- **Range: semitones (±).** `peakSemitones` is signed; default depth range noted at build. -- **Applied per engine.** Under **Varispeed** the offset is a **per-frame multiply of - `ratio_`** by `2^(pitchEnvSemitones(frame)/12)` (the effective read increment varies frame- - by-frame at no structural cost — the same per-frame `tick()` idiom as the amp envelope, - RT-safe, no `process` allocation). Under **Preserve** the offset is **added to the shifter's - shift amount** — `set_shift(2^((note−root + pitchEnvSemitones(frame))/12))` — bending pitch - without touching duration. Per-voice (polyphonic notes each run their own). -- **Ownership + editor.** Per-zone instrument performance-map state (D-B), additive/version- - bumped (absent → disabled). Editor exposure folds into the S12 ADSR-editor tier: attack + - decay + a ±semitone depth control, default-off (discoverable but inert until enabled). - -## Ingest through the bank — the extension owns ingest (decided "option 1", 2026-07-26; PLAN.md S8) - -**Decided: loading a sample into the sampler is ONE gesture — capture/import-into-bank AND -auto-assign to the active sampler instance — and the *extension* owns it.** The instrument -stays a **read-only bank consumer**; it never captures and never imports. The extension is -the right owner: it has arrange access, Media-Explorer access, and the drop-target surface -on its own docked panels. Ingest lives in the *extension* codebase (actions + `bank_panel` + -capture/import add-path), routing through the existing capture add-path and the live -`"reasampler"` seam the instrument already reads. - -**The three ingest surfaces, with the honest SDK reality (verified against the vendored -headers):** - -- **Arrange capture → bank → assign.** A one-click action captures the selected item / - time-selection into the bank (reusing the existing capture request path — - `CountSelectedMediaItems` / `GetSelectedMediaItem` + `GetSet_LoopTimeRange` are already the - capture inputs) and assigns the resulting `Sample` id to the target instance. **It never - inserts a timeline item** — the capture/placement separation is load-bearing; assignment - is a bank-index + instance-selection act, not a placement. -- **Media Explorer import → bank → instrument on selected track.** The Media-Explorer surface is **thin**: - `OpenMediaExplorer` (open/select a file) and `MediaExplorerGetLastPlayedFileInfo` (read the - *one* last-played/selected file path + its selection range/pitch/vol/rate) are the whole - contract. There is **no** enumerate-selected-files and **no** register-a-drop-handler-on- - the-Media-Explorer API. So ME import is **single-file, pull-on-action** — an action fired - while a file is selected in the ME — not a push/drop from inside the ME. **Shipped behavior:** - after importing the file into the active bank, the action adds a ReaSampler 9000 instrument to - the user's **currently selected track** via `loadInstrumentOntoTrack` in `instrument_drop_win` - — no new track is created and no routing is changed ("new sound, existing track"). If no track - is selected the sound still lands in the bank but no instrument is placed and a console message - explains why. No `assignment_request` is written; it never touches a live instance's selection. - Undo-wrapped: persist + FX-add + inject = one Ctrl-Z. -- **Drag-and-drop onto ReaSampler surfaces.** REAPER exposes **no** drag-drop registration - API. Drop handling is on ReaSampler's *own* HWNDs via SWELL/Win32 (`WM_DROPFILES` / - `IDropTarget` on the docked `bank_panel` HWND — the surface the panel already owns) → ingest - (bank-fill only). Dropped files are imported into the active bank; no `assignment_request` is - written and no live instance's selection is affected. The bank-generation bump is retained so - open instances' browsers refresh to show newly available sounds. **Assess-and-flag (spike, - not promised):** a drop *onto the VST3 editor window* — whether the `IPlugView` HWND can - accept an OS file drop and **relay it to the extension as a bank-ingest request** (the - instrument does not ingest; it forwards a request over an agreed seam). This crosses the - two-artifact boundary and the relay is unproven; if gnarly, drop-onto-panel is the shipped - path and drop-onto-editor is deferred. - -**The assign seam.** The ingest action names the target instance (lean: the active/ -last-focused instance, discovered via the host context the bridge already resolves) and hands -it the new sample id — the same instance-owned selection state S4 already persists, so a -reload picks it up. With the change-detection seam (below) the assignment refreshes -hands-free; without it the ingest action pokes the target instance's reload directly. - -**Guardrail (load-bearing, restated):** ingest is an *extension* act. Any instrument code -path that captures, imports, inserts a timeline item, or writes back into the bank is a bug — -the instrument reads and plays only. - -## Bank-generation change-detection — hands-free refresh (decided 2026-07-26; PLAN.md S9) - -Instances reference sample **ids**. So a **recapture** (M10) landing under the same id — or -an **ingest** (S8) touching the active bank — should refresh playing instances **hands-free**, -without re-opening each editor. The missing trigger: a **bank-generation counter** in -`"reasampler"` ext-state. - -- **Writer (extension).** A monotonic **bank-generation counter**, stamped into - `"reasampler"` ext-state under a new forever-stable `ext_keys.h` constant, bumped on every - bank-content mutation that changes what an instance would play (capture add, recapture-in- - place, sample-remove, move/copy affecting the active bank). Additive to the persist blob; - defaults to 0 for projects saved before the stamp exists. -- **Reader (instrument).** Poll the generation over the bridge on a safe **off-audio-thread - cadence** (a UI/timer tick, **never** `process`), compare to the last-seen value, and call - the existing off-thread `reloadInstrument()` on change — reusing S4's atomic pointer-swap - handoff (graveyard-reclaim) so a mid-play refresh does not glitch. No new audio-thread work; - no allocation in `process`. -- **Cadence + safety.** A low-frequency UI timer, coalescing multiple bumps between polls into - one reload (build-time residual). The read already tolerates a stale value by design (it - reloads on the *next* poll). **Must-verify before build:** no torn-read hazard on the single - integer generation key for a bridge read on the instrument's UI/timer thread concurrent with - an extension write. - -This seam serves **both** S8 ingest and M10 recapture; the writer side is extension-only and -independent of S8, so it can land alongside either. - -## Design-system foundation — moved to Phase L (2026-07-26) - -> **The visual design language moved out of Phase S into its own Phase L.** The -> design-system content that stood here (the toolkit assessment, the shared LICE drawing -> kit **S0-DS**, and the dock-panel refresh **S14**) has been lifted into **Phase L** -> (Look-and-feel) on `dev`, taken up by a parallel team so Phase S feature work proceeds -> ungated. S0-DS is now **Phase L point L1** (the shared kit); S14 is now **L2** — and, -> per Daniel's DS-3 call, expanded from a light re-skin into a **thorough dock-panel layout -> redesign** that lays out the full M11-aware button inventory before applying the kit; the -> VST editor + embed-strip restyle is now the explicit **L3** point (gated on Phase S -> landing on dev). The design-system forks DS-1 (LICE + WDL free game, no external -> frameworks), DS-2 (Direction B "Neon Console" + Direction C's spectral keyboard strip), -> and DS-3 (thorough panel layout redesign) are all **SETTLED (Daniel, 2026-07-26)**. -> -> **Authoritative from here:** **PLAN.md §Phase L + CONTEXT.md §Phase L on `dev`**, and -> `docs/product/visual-design-language.md` (on `dev`). Phase S's **S10–S13 build their -> interaction UX with the current drawing and adopt the Phase L kit when it lands — they -> are not gated on Phase L.** LICE/SWELL-only, the pure-geometry-module discipline, RT -> discipline, and the read-only-over-bank / VST3-class-UID-unchanged guardrails all hold -> exactly as before — a visual refresh is not a data-ownership or compat event. - -## Product name — ReaSampler 9000 (Daniel, 2026-07-26) - -The MIDI-playback instrument's product name is **ReaSampler 9000**. The extension stays -**ReaSampler** (capture + organization); the instrument is **ReaSampler 9000** (playback). -Set by Daniel on DAW-testing the S1–S6 instrument, alongside the UX-overhaul directive. - -- **Propagate the display name** across user-visible surfaces: the VST3 class **display - name** string in the factory registration, the **factory vendor/name strings**, the - `IPlugView` editor **title band** (currently "ReaSampler Instrument"), the **S6 embed-strip - label**, and the Phase S docs. -- **Do NOT change the VST3 class UID.** Instances in already-saved projects key off the - class UID; changing it orphans every existing instance in every saved project. The UID is - a forever-stable contract (mirror of the command-id / ext-state-namespace forever-stable - strings). -- **S-NAME-1 SETTLED (Daniel, 2026-07-26): rename the binary filename too.** The on-disk - module name is renamed to match the product (e.g. `reasampler_9000.vst3`), not just the - display strings. Full rename surface: **CMake `OUTPUT_NAME`** on the second VST3 target, - the **factory vendor/name strings**, the **editor title**, and the **embed label**. The - **class UID stays locked** as the compat anchor. -- **Compat verification (must-DAW-verify before shipping the rename).** The working - assumption is that REAPER **rebinds a saved instance by its VST3 class UID, not by the - module filename** — so a filename rename with an unchanged UID keeps saved projects working. - **This is a to-verify assumption, not a confirmed fact:** a web check surfaced a - JUCE/VST3-replace-VST2 case suggesting REAPER's binding can be more nuanced than "UID only" - (an FXID match is involved), so it is not safe to assert UID-only rebinding from source. - **DAW-verify:** save a project with an instance under the old filename, rename the module, - reopen, and confirm the instance rebinds and restores its state. If REAPER keys partly on - filename, fall back to keeping the current filename (display-strings-only) and record that - as the shipped choice. - -## VST3 channel identity — the UID pair + the pairing surface (S18; extends Phase V V4) - -**Decided (Daniel, 2026-07-26):** the beta/stable channel split Phase V V4 gave the -*extension* extends to the **ReaSampler 9000 VST3 instrument** — a beta-built VST pairs with -the beta extension only, a stable VST with stable only, both installable side-by-side in one -REAPER. This is the instrument-side companion to V4 and mirrors its philosophy exactly: -**one channel per binary; all channel identity derives from the ONE -`REASAMPLER_CHANNEL_IS_BETA` bit via the pure `app_version` module — no scattered `#ifdef`s -in the VST shell.** - -**What is already isolated (structural, not added by S18).** The wire/data pairing is -already done and needs no per-key work: `ext_keys.h`'s `kProjExtNamespace()` delegates to -`app_version::extStateNamespace()`, so a beta-compiled VST's bridge reads `"reasampler_beta"`. -Every wire key — `banks`, `assign_request`, S9's bank-generation key (in-flight), S17's -component-state contract, and **any future key** — is a plain constant *under* that -namespace, so channel data-isolation is **structural: no per-key opt-in, and a future key -that forgets to isolate is impossible by construction** (it keys off the namespace accessor, -not a raw literal). What S18 adds is only the missing *plugin identity* layer. - -- **The UID-pair invariant (the permanent commitment).** The VST3 class UID is the plugin's - identity — a saved REAPER project records it and rebinds a saved instance by it. Today - `reasampler_vst.h` holds **one** forever-locked UID (`kReaSamplerProcessorUID`, - `REASAMPLER_PROC_UID_1..4`, S-NAME-1). A beta VST with the **same** UID cannot coexist with - stable in one install (same UID = identity collision / arbitrary rebind). So beta needs its - **own** forever-stable UID: a second constant, minted once, locked exactly as the first. - **Invariant: BOTH UIDs are frozen forever once shipped; the channel bit selects which is - compiled into this binary** (one `DEF_CLASS2`, one class per binary — not both classes in - one binary; that mirrors V4's fully-isolated-binary philosophy and keeps a beta build from - ever presenting the stable identity). Saved-project isolation follows directly: a project - saved with beta instances rebinds only to the beta VST; a stable-saved instance opened - where only the beta extension has banks resolves the stable UID and shows a clean empty - "pick a capture" state (S10 policy), not an error. -- **Binary + display identity, channel-derived.** Mirror the extension's `OUTPUT_NAME` fork - (`reaper_reasampler` / `reaper_reasampler_beta`): the VST3 module's on-disk name forks - `reasampler_9000` / `reasampler_9000_beta`, its factory display name "ReaSampler 9000" / - "ReaSampler 9000 beta", and its editor title band + S6 embed-strip label are channel-aware - — **all sourced from `app_version` channel accessors (a VST-name accessor beside - `binaryName()`/`dockTitle()`), never a literal in `reasampler_vst.h`/`vst_entry.cpp`.** The - factory version string carries the `-beta` render where V4's `appVersion()` already does; - vendor/url/email stay shared unless V4 qualified the equivalent (V4 kept the lane-name - prefix shared — shared-where-V4-shares is the default). -- **The complete pairing surface (the guarantee to state, not new code).** A channel's VST - talks to that channel's extension **only**, because (1) plugin identity — UID + filename + - display — is channel-forked (above), and (2) **all** wire keys live under the - channel-derived `kProjExtNamespace()`. The two together make pairing complete and - structural: identity keeps the *plugins* distinct; the namespace keeps the *data* distinct. - No per-key or per-seam isolation work is ever needed — S8's assignment key, S9's generation - key, and S17's blob-injection key all inherit it. **Verify all identity/factory wiring - against the vendored Steinberg SDK** (`DEF_CLASS2` / `INLINE_UID` / `FUID` from - `pluginfactory.h` + `funknown.h`); the pure `app_version` name accessors are CTest-tested. -- **Fork S18-F1 (flagged — Daniel's call): mint the beta UID now vs. at first beta release.** - Lean **mint now** — mirrors the stable UID (minted at the S1 spike, locked long before - ship), removes a "remember to mint before shipping beta" landmine, zero cost for an - unused-until-beta constant. The alternative (a locked-once placeholder replaced before the - first beta VST ships) defers the commitment but adds a release-gate step. Flagged only - because the UID is a forever commitment. - -## Non-goals / guardrails - -- **The instrument never captures and never inserts into the arrange.** Playback is a - read-only act over the bank. Any instrument path that captures, places a timeline item, - or writes back into the bank is a bug — reject in review. -- **The instrument keeps no private copy of the samples.** It consumes the one - authoritative bank; per-instance sample stores are a non-goal (they refork the source - the one-source-multiple-views instinct keeps single). -- **The instrument never ingests (S8).** Capture, import, and drop-ingest are *extension* - acts; the instrument only reads and plays. A drop onto the editor window (if the spike - proves it viable) is *relayed to the extension* as an ingest request — the instrument - never writes the bank itself. -- **Channel mode is a performance choice, not a bank fact (S7).** The mono/stereo toggle is - per-instance component state, never written to `Sample` or the bank (D-B). The bank's - per-sample channel-count intrinsic is a *file fact*; the play mode is the instrument's. -- **No cross-platform / multi-format.** Windows-only, VST3-only, REAPER-only (D5). Do - not add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target. -- **The pure core stays REAPER-free *and* VST3-free.** The voice engine / envelope / - keymap / repitch module takes no VST3 or REAPER type at its boundary — the shell - marshals. Any VST3 or REAPER type leaking into the core is a bug (the D3 split). -- **Additive to the extension.** The `Sample` intrinsic-field addition is additive - (new optional fields; no existing field or `BankIndex` behavior changes); everything - else in Phase S lives in the *second* artifact and does not alter the extension's - M/D/B/R/V pillars. -- **Do not spec Tier 2/3.** Tier 2 is held (noted, not specified); Tier 3 is - optional-forever. Do not let their feature lists drive Tier 0–1's build shape. **Note:** - S7 stereo is *not* a Tier-2 feature — it is a channel-count dimension on the existing - Tier 0–1 engine, orthogonal to Tier 2's velocity-layers / round-robin / per-sample trim. - (S7's stereo loop read is the same loop the core already has, extended per-channel — not - the Tier-2 "sustain loops" feature.) **Likewise S15/S16** (Trigger/Gate modes + pitch - envelope) are Daniel-directed engine features on the Tier 0–1 core, *not* Tier 2/3 — the - AHDSR hold, Trigger one-shot, start point, and AD pitch envelope are orthogonal amplitude- - shape / read-rate dimensions, not the held velocity-layers / round-robin / filter work. -- **S15/S16 params are performance choices, not bank facts.** Play mode, start point, - %-length, fades, AHDSR, and the pitch envelope are per-instance performance-map state - (component state), never written to `Sample` or the bank (D-B). The bank carries file - facts (root note, loop intrinsic, channel count); the instrument owns how they are played. -- **S15/S16 stay channel-count-agnostic (S7 interplay).** The mode/envelope logic is - per-frame amplitude and read-rate, independent of the S7 channel dimension. Any S15/S16 - code that assumes a fixed channel count (mono) — rather than operating per-frame pre-mix — - is a bug that would collide with S7. Spec and build them channel-agnostic. -- **Trigger ignores note-off; choke is out of scope (S15).** In Trigger mode note-off is a - no-op and the one-shot plays to `playEnd`. Choke-on-note-off / choke-groups are held - (fork S15-F1, Tier-3-adjacent) — do not add a choke path in S15. -- **Pitch envelope is off by default (S16).** Default-disabled → offset always 0 → the - engine's un-modulated output → playback bit-identical to the same engine pre-envelope. A - regression that applies pitch modulation when the envelope is off is a bug. -- **Pitch engine is a per-zone performance choice, not a bank fact (S16).** Varispeed vs - Preserve is per-`PerformanceZone` component state (D-B), never written to `Sample` or the - bank. The engine default is fork S16-F1 (**lean Preserve** — Daniel's call), with a - prominent per-zone toggle so drum/one-shot zones opt into Varispeed cheaply. -- **Preserve engine is RT-disciplined (S16).** The `WDL_SimplePitchShifter` (or hand-rolled) - Preserve path **pre-warms at voice-allocation** and does **no allocation in `process`** — a - `WDL_Queue::Add` or `Resize` on the audio thread in steady state is a bug. Preserve's onset - latency (shifter window) is an accepted property, **not** a defect; a note-onset **click or - smear** from a cold-started (un-pre-warmed) shifter **is** a bug. -- **`WDL_Resampler` is not a Preserve engine (S16).** It is a resampler (couples duration) — - a held Varispeed-quality option only. Do not wire it as the duration-preserving path. -- **Drop-and-load must not regress the two existing drags.** S17 inserts a new middle case - (`InstrumentDrop`) between the M11 OS drag-out and the internal bank-to-bank drag; both - existing gestures stay byte-for-byte unchanged in their own regions. Drop-and-load never - inserts a media item into the arrange and never copies sample bytes into the instance — - it hands the new instance a *reference* to an already-captured bank sample. -- **Verify Steinberg SDK, bridge, embed, and LICE-view surfaces** against the vendored - headers before use — several §1a claims are experienced estimates until the spike - confirms them. - -## Editor view-model redesign — three views: Sample / Browse / Zone (S-VIEW; Daniel, 2026-07-27, r9) - -> **Additive sub-phase of Phase S — an *editor* redesign, not an engine change.** Re-partitions -> the ReaSampler 9000 editor from today's two-view toggle (Browser + Zones) into a **three-view -> model where the loaded sample is the home**, adds three new performance parameters (key-tracking, -> preview velocity, and the r10 velocity→amp transfer curve) and three visual components (envelope -> overlay, real piano-key pattern, and the r10 velocity-curve editor), and -> frames two engineering prerequisites (drop-to-FX bug, default window size). The S3 voice -> engine, keymap resolution, and read-only-over-bank contract are **unchanged**; the -> component-state format extends additively (key-tracking + the r10 velocity curve on the -> zones-payload axis, preview velocity on the envelope axis). Product framing: `docs/product/ -> midi-playback.md` §Addendum r9 + r10. Same standing discipline: **LICE/SWELL drawing only, -> all layout/hit-test in pure geometry modules, RT-safe, VST3 class UID unchanged, verify every -> API name/signature against the vendored headers before use.** - -### The view model — Sample is home; Browse is modal; Zone is a dedicated surface - -The three views are **not a flat three-way toggle** (today's Browser|Zones segmented switch is -retired). The model is **document-with-modal-picker** (mirror: Ableton Simpler — the device face -*is* the loaded sample; loading a new one is a distinct act): - -- **Sample (the home / default face).** What the editor shows on open with a capture loaded. The - hero waveform, the envelope overlay, all per-sample tuning controls, the preview-trigger. This - is where the user lives. -- **Browse (a modal page layered over Sample).** Summoned by a **Browse button** (and, when - nothing is loaded, the empty-state's primary affordance — Browse must be *very* easy to open - when there is no capture selected). It renders as a **full-window overlay** over the Sample - face — filters + captures grid + **select-then-confirm** to change the loaded sample, then - dismisses back to Sample. It is a picker sheet, not a peer tab. -- **Zone (a dedicated editing surface).** Opened by its own **Zone button** when the user wants - to map the capture(s) across the keyboard. Not shown by default ("most of the time zones won't - be used" — the S10 reframe). Returns to Sample on close. - -**Navigation contract.** From Sample: a Browse button opens the Browse overlay; a Zone button -opens the Zone surface. From Browse: select a card + confirm (or cancel) returns to Sample with -(or without) a new loaded capture. From Zone: a close/back affordance returns to Sample. The -empty state (no capture loaded) surfaces Browse as its dominant call-to-action. **The fresh -instance stays silent with a "pick a capture" empty state (S10 reversal, unchanged).** - -### View 1 — Sample (the new main view) - -The home face. Composition follows the reference devices (Simpler / Phase Plant): a **hero -waveform up top with the envelope drawn over it at accurate time**, a **dense labelled -value-strip beneath**, **root fenced as its own affordance**, and a **preview cluster**. Bands, -top to bottom: - -- **Title band.** Plugin name (channel-derived `vstPluginName()`) + live readout (loaded capture - name / `[pick a capture]` / `[bank empty]` / `[host: no bridge]`) + the **Browse** and **Zone** - view buttons. (Inherits today's title-band text logic.) -- **Hero waveform band (enlarged) with the ENVELOPE OVERLAY (new).** The picked capture's - full-resolution envelope (from `peaks` over the cached mono PCM — no new decode), given real - vertical space (the Simpler/Phase-Plant hero, materially taller than today's 72px strip). Drawn - over it, at **accurate wall-clock time**: (a) the **S11 markers** — start (teal/secondary), - loop start/end (purple/tertiary), the faint loop-region fill — moved here from Browse - unchanged; and (b) the **NEW amplitude-envelope overlay** — the AHDSR shape (Gate) or the - fade-in/%-length/fade-out shape (Trigger) traced as a curve across the sample at the same time - base the voice engine uses, in an accent hue. This is the "reads like a real sampler, not a - spreadsheet" move: the envelope becomes a *shape over the sound*, not four abstract sliders. -- **Root + preview cluster (fenced, new preview-trigger).** Root note shown as a first-class, - always-visible control (fenced like Phase Plant's "Root" box), draggable/typeable — this is the - keyboard-strip root-drag from today's Browse setup, promoted to a fenced control. Adjacent: the - **NEW preview-trigger button** (fires the sampler at the loaded capture's root note through the - live voice engine, off the audio-thread commit path — no MIDI controller needed) with an - **adjacent velocity knob** setting the preview velocity level. The **Mono/Stereo toggle** moves - here from Browse (it is a per-capture output-mode tuning concern, not a choosing concern). -- **The "Modes-and-down" control strip (moved from Zone).** Every per-sample control that lives - on today's Zones param panel for the single-capture case — **Mode** (Gate/Trigger), **Pitch - engine** (Varispeed/Preserve), the **AHDSR** sliders (Gate) or **Length%/Fade-in/Fade-out** - (Trigger), and the **AD pitch envelope** (Off/On + P.Attack/P.Decay/P.Depth) — renders here as - the Sample view's control strip, laid out as a dense labelled value-row (bold micro-caps over a - value/slider, the reference grammar). For a single loaded capture this is the same one-zone - storage site the S15-F2 lean already established (the empty-map single-capture face reads/writes - the same `PerformanceZone` defaults) — no new storage. - -**Browse elements that MOVE to Sample (inventory — nothing silently dropped):** the large -waveform preview, the S11 start/loop markers + loop fill, the keyboard-strip root affordance (now -the fenced root control), and the Mono/Stereo toggle. - -### View 2 — Browse (reduced to *choosing*, laid over Sample as a modal) - -Browse's only job is **pick a capture**. Today's Browser view is close but overloaded; it is cut -to the choosing essentials and rendered as a modal overlay: - -- **Kept (the excellent core, unchanged):** the **type-to-filter search box**, the **bank filter - tabs** (All + one per named bank), the **captures grid** (cards: peak thumbnail + name + - root/key badge), **scroll** (wheel + thumb), and card **selection**. -- **Added:** a **confirm/cancel** affordance (select a card, confirm to load it into Sample and - dismiss; cancel to dismiss unchanged) — the modal-picker close semantics. Double-click-to-load - is the natural accelerator. -- **Removed from Browse (moved to Sample or cut):** the **large waveform preview** (redundant — - the grid thumbnails already show every capture's waveform; moved to Sample as the hero); the - **Mono/Stereo toggle** (moved to Sample); the **root-note keyboard-strip** in the setup band - (moved to Sample as the fenced root control); the **loop-point labels** and the **track-root - message** (cut — they waste space and add nothing to *choosing*). - -**Rationale (why cut, not keep-and-hide):** Browse is a picker. Every tuning affordance on it is -a mode error — you tune what you *have*, you browse for what you *want*. Concentrating tuning on -Sample and choosing on Browse makes each view do one job, and makes the modal overlay light -enough to summon and dismiss without ceremony. - -### View 3 — Zone (dedicated keyboard-mapping surface + key-tracking + real piano pattern) - -Zone is the multi-zone keymap editor — RS5K structurally lacks a multi-zone-in-one-instance view, -so this is a genuine capability, kept as the deliberate secondary surface. Retains today's Zones -view wholesale, with two changes: - -- **Kept (inventory — nothing silently dropped):** **+ Add Zone** / **Delete** buttons, the - **keyboard strip with one bar per zone** (selected zone lit accent-primary + static glow, others - categorical), the **selected-zone legend** with the three **Low/High/Root numeric-entry fields** - (click-to-type, `parseNoteEntry`), and the **per-zone param control panel** (Mode, Pitch engine, - AHDSR/Trigger, AD pitch envelope) — the same panel Sample now also hosts for the single-capture - case (one storage site, two surfaces). -- **NEW — key-tracking parameter (0%–200%, default 100%).** A per-`PerformanceZone` scalar on how - sample pitch tracks the keyboard around the root note. **100% = standard 12-tone-ET tracking** - (today's behavior, bit-identical); **0% = no tracking** (the sample plays at root pitch on every - key — a fixed one-shot); **200% = double-rate tracking**. Additive on `PerformanceZone`, - version-bumped, defaulting to 100% so pre-existing zones are unchanged. Applies inside the - repitch math in **both** engines: under **Varispeed** it scales the semitone offset feeding - `pitchRatio(note, root)` (`effectiveSemis = (note − root) · keyTrack`); under **Preserve** it - scales the semitone offset feeding `set_shift(2^(effectiveSemis/12))`. Surfaces as a control in - the Zone param panel (and, for the single capture, on the Sample control strip). **The pure - sampler core owns the key-track math** (unit-tested: a known note/root/keyTrack triple asserts - the expected ratio); the shell only maps the 0–200% control to the scalar. -- **NEW — real piano-key pattern on the keyboard strip.** Today's spectral strip is pretty but - does not read as a keyboard (Daniel's note). Keep the pastel spectral hue as the backdrop, but - **overlay the actual alternating white/black key pattern** — bright cells for naturals, dark - cells for accidentals (C#/D#/F#/G#/A#), per the palette (bright ≈ a light neutral, dark ≈ - `bg/base`/hairline) — so the strip is instantly identifiable as a keyboard. It need not be - *shaped* like a keyboard (no protruding black keys); it carries the **pattern** as an overlay, - so a glance reads pitch position without counting. The pure `keyboard_strip` geometry gains a - `isBlackKey(note)`/per-key-natural query (12-tone pattern, pure + unit-tested); the shell draws - the two-tone overlay over the spectral fill. This same strip serves the Sample view's fenced - root affordance (one keyboard grammar everywhere). - -### New parameters — ownership and persistence (D-B; instrument-owned, never bank facts) - -- **Key-tracking** — per-`PerformanceZone`, additive/version-bumped component state, default 100%. - A **performance choice**, never written to `Sample` or the bank. Back-compat: an absent field on - an older blob → 100% (bit-identical playback). -- **Preview velocity** — a **utility** setting for the Sample view's preview-trigger button, not a - musical parameter of the capture. **SETTLED (S-VIEW-F1, Daniel 2026-07-27): it PERSISTS across - reloads.** The seam is **VST3 component state**, not the extension's `persist` ext-state module. - This distinction is load-bearing and was verified against the existing VST source, not recalled: - - **Wrong seam — the extension's `persist` module.** `persist` writes REAPER *project* ext-state - (`SetProjExtState`, namespace `"reasampler"`) and is owned by the **extension**, not the - instrument. Preview velocity is a **per-instance** instrument-editor setting; putting it in - project ext-state would (a) make it project-global rather than per-instance (two ReaSampler 9000 - instances would share one preview level), (b) route an instrument-owned setting through a seam - the instrument only *reads* from (bank state), violating the read-only-over-bank contract, and - (c) leak an instrument concern into the extension's key space. Rejected. - - **Right seam — the instrument's own VST3 component state**, the same blob the processor already - round-trips via `ReaSamplerProcessor::getState`/`setState` over `IBStream`, whose format is the - **envelope-versioned `ComponentState`** in `src/vst/sample_map.h` (currently **v5**: version tag - + channel-mode byte + last-consumed-assignment marker + selection-id + zones payload). Preview - velocity is a **top-level instance concern** — a sibling of `channelMode` and - `lastConsumedAssignGeneration`, **not** a per-`PerformanceZone` field (it is one setting per - instance, not per zone). It therefore lands as a **new field on `ComponentState`** added by - bumping the **envelope version to v6** (a new `float previewVelocity` after the assignment - marker, before the selection-id), leaving the **zones payload untouched** — exactly the - independent-version-axes composition the header already documents (envelope grows a field; the - zone-record payload version does not move). Round-trip: `serializeComponentState` appends it, - `deserializeComponentState` reads it; a v6 blob restores it directly, and **every older blob - (v5 and down) lifts to a sensible default** (e.g. 100/127 ≈ 0.79 or a chosen mid level), so - already-saved instances are unchanged and no compat surface is broken. This is the same additive - envelope-bump discipline S7 (v4→v5) already used — a well-trodden move in this codebase, not new - machinery. - -### The envelope overlay — visual component (new) - -The amp envelope drawn as a curve over the Sample view's hero waveform at accurate wall-clock -time (the Simpler/Phase-Plant grammar). Gate → the AHDSR shape (attack ramp, hold, decay to -sustain, release tail); Trigger → the fade-in/unity/%-length/fade-out shape anchored to `playEnd`. -The time base is the same the voice engine resolves (seconds → frames at the live rate), so the -drawn shape lines up with the waveform under it. **Pure geometry:** an `envelope_overlay` module -(mirror of `waveform_view` / `param_slider`) maps the AHDSR/Trigger params + the sample's -frame-length to a polyline in the waveform rect (`param↔pixel` at the shared time base), -unit-tested against known param sets; the shell traces it via kit line draws in an accent hue. - -**The overlay is directly editable — DRAGGABLE NODES (SETTLED, S-VIEW-F2, Daniel 2026-07-27).** -The envelope is not a read-only informative curve — its breakpoints are **draggable handles** that -set the envelope parameters directly. Dragging a node and the existing sliders are **two surfaces -onto one model**: the sliders stay as the precise numeric-entry surface, node-drag is the direct- -manipulation surface, and **both read and write the same `PerformanceZone` envelope fields** — a -drag updates the params, the sliders reflect them live, and a slider edit re-lays the nodes. There -is exactly one source of truth (the zone's envelope params); the two surfaces never diverge. This -is the same one-source-multiple-views instinct the whole system runs on. - -- **Which nodes, and what each axis means.** The AHDSR shape (Gate) exposes handles at the segment - breakpoints — **attack-end** (top of the attack ramp), **hold-end**, **decay-end / sustain-level** - (the corner where decay settles to the sustain plateau), and **release-end**. The Trigger shape - exposes **fade-in-end**, **%-length** (the `playEnd` anchor), and **fade-out-end**. For each node, - **the horizontal axis maps to time** (the segment duration — attack/hold/decay/release seconds, or - fade/length fractions) and **the vertical axis maps to level** where the node is a level breakpoint - (the sustain node's vertical drag sets sustain 0..1; the peak nodes sit at unity). Time-only nodes - (attack-end, hold-end, release-end) drag horizontally; the sustain node drags on **both** axes - (its X sets decay time, its Y sets sustain level) — the standard ADSR-editor grammar (Ableton - Simpler, Phase Plant, Serum all use exactly this). -- **Constraints.** Nodes are **monotonic in time** — a node cannot be dragged left of its - predecessor or right of its successor (attack-end can't pass hold-end, etc.); each segment stays - ≥ 0. Level drags clamp to **0..1** (sustain) / unity (peaks). Times clamp to the same per-param - min/max the sliders already enforce (so node-drag can never produce a param the slider couldn't). - The `playEnd`/%-length node additionally clamps to the sample's frame-length. **No snapping** by - default (continuous drag, matching the sliders' resolution); a fine-drag modifier (drag with a - held modifier for reduced sensitivity) is an optional polish, not required. -- **Pure module owns the geometry/hit-test math (house pattern).** A pure REAPER/LICE-free module — - name it **`envelope_edit`** (sibling to `envelope_overlay`; mirror of `card_drag` / `mode_switch` - / `param_slider`) — owns **node hit-testing** (point → which node, with a pick radius) and the - **drag→param mapping** (a pixel delta on a given node → the resulting clamped envelope param set, - respecting the monotonic + range constraints above). It is **unit-tested**: a known node + a known - pixel delta asserts the expected param delta and the clamp/monotonic behavior at the boundaries. - `envelope_overlay` keeps the params→polyline forward map (draw); `envelope_edit` owns the - pixel→params inverse map (edit) + hit-test. The shell (`reasampler_editor.cpp`) does the LICE - handle draw (small node markers at each breakpoint, hover/drag-lit via kit states) and routes - mouse events through `envelope_edit`, then commits the resulting params to the zone through the - same off-audio-thread path the sliders use (no new RT surface — a node-drag is a param edit, same - as a slider drag). Zones and the single-capture Sample face share this one edit surface (one - storage site, S15-F2). -- **Sync with sliders (load-bearing).** Because both surfaces write the same `PerformanceZone` - envelope fields, keeping them in sync is **structural, not a listener chain**: the editor re-reads - the zone's params every paint, so a slider edit re-lays the nodes and a node-drag re-positions the - sliders with no explicit cross-wiring. The single source of truth makes divergence impossible by - construction. - -### The real piano-key pattern — visual component (new) - -Covered under View 3 above. Pure: `keyboard_strip` gains the natural/accidental predicate; shell -draws the bright/dark overlay over the existing pastel spectral fill. Shared by the Zone strip and -the Sample root affordance. - -### The velocity → amp transfer-curve editor — visual component (new; Daniel, 2026-07-27, r10) - -A **visual transfer-curve editor** mapping MIDI velocity to an amp scalar: **X = velocity (0–127), -Y = amp scalar (0–1)**, an editable bezier from a flat default to an arbitrary multi-point curve. -It gives fully shapeable velocity dynamics per sound. Today the engine maps velocity to gain -*linearly* — `velocityGain_ = velocity / 127.0`, computed once at note-on in `Voice::start()` -(`src/vst/sampler_core.cpp:261`); this replaces that fixed line with an editable curve evaluated at -the same point. - -**State home — per-`PerformanceZone` (instrument-owned, D-B).** Velocity response is a per-sound -performance characteristic — a sibling of the AHDSR amp envelope, the pitch engine, and the r9 -`keyTrack` scalar, all of which already live on `PerformanceZone`. A drum and a pad want different -velocity curves, so the curve varies **per zone**, not per instance. This is deliberately **not** -`ComponentState` (per-instance): that is where **preview velocity** correctly lives, because -preview velocity is a *utility* setting (one per instrument, like a metronome level), whereas the -transfer curve is a *musical* setting (one per sound). The curve is an **additive field on -`PerformanceZone`** riding the **zones-payload version axis** (contrast preview velocity's -envelope-v6 bump — a top-level per-instance field on the *envelope* axis; the two version axes are -independent, as the header documents). The single-capture Sample face reads/writes the same -one-zone storage site (S15-F2), so Sample and Zone views share one curve store per zone. - -**Default — flat y=1, a deliberate behavior change (fork R10-F1, Daniel's call).** Daniel's -verbatim default is *"any velocity plays at full level"* — a flat curve at y=1. This is **not** -bit-identical to today's shipped linear `velocity/127` map: today soft hits are quieter; under a -flat-y=1 default every hit plays at unity, so every existing zone's felt dynamics change. This is -the one genuine fork the feature carries: -- **Option A (Daniel's stated default): flat y=1.** Honors the directive; velocity is inert until a - curve is drawn. *Con:* NOT back-compat — already-saved instances and new captures get flatter - dynamics than today until a curve is drawn. -- **Option B: default = today's linear ramp (y = x/127).** Bit-identical to the shipped engine; the - editor's flat y=1 is one drawn state, not the default. *Con:* contradicts the verbatim *"by - default … full level"* (the default line is a diagonal, not flat). -- **Lean: Option A (flat y=1)** — it is what Daniel asked for and the feature's point is opt-in - velocity dynamics — but flagged loudly as a **shipped-behavior change**, not a silent regression. - Option B is the safe fallback and costs only the default curve's seeded control points. Whichever - wins, the *stored* default is a curve the editor draws and the core evaluates; the options differ - only in which curve is seeded. - -**Pure module — `velocity_curve` (REAPER/LICE-free, unit-tested).** Mirror of `envelope_edit` / -`card_drag`. Two responsibilities: -- **Evaluation:** `eval(velocity 0–127) → amp scalar 0–1` — a bezier through the control points, - clamped to the 0–127 × 0–1 box, **monotonic in x** by construction (each velocity has exactly one - output). Called at note-on, never per frame. -- **Editing:** add / move / delete control points, each clamped into the box and **x-ordered** (a - point cannot cross its neighbours in x — the same monotonic grammar as the envelope nodes); a - point hit-test (point → which control point, pick radius) and a pixel-delta → clamped-point - inverse map (mirror of `envelope_edit`'s inverse map). The flat identity curve (per R10-F1) is a - named constructor. - -Both halves are **unit-tested** at the boundaries: a known curve + known velocity asserts the eval -output; a known drag asserts the clamped point set + the box/order constraints; add/delete assert -the point count and ordering invariants. - -**Voice-engine application point — `Voice::start()`, off the per-frame path.** Confirmed from -source: `Voice::start(int note, int velocity, …)` (`sampler_core.cpp:252`) computes -`velocityGain_ = velocity / 127.0` **once at note-on** (line 261); the per-frame render then just -multiplies the cached scalar (`advanceFrame`, line 408: `gain = amp * velocityGain_`). The transfer -curve slots in at exactly that line — `velocityGain_ = curve.eval(velocity)` at note-on — **off the -audio-thread-hostile per-frame path** (evaluated once per voice, no new `process`-thread work, no -allocation). The curve reaches the voice the same way the AHDSR/keyTrack params do: on the zone's -`SampleData::play` bundle (resolved from the stored `PerformanceZone` at keymap build), read by the -voice at `start()`. The pure `velocity_curve` core owns the eval; the voice reads it. - -**The curve-editor UI — Sample view, adjacent to the envelope overlay.** Lives on the **Sample -view** (the r9 home face), a compact band next to the hero-waveform envelope overlay — the two are -the same grammar (a drawn 2-D curve with draggable handles), and amp-over-time beside -amp-over-velocity reads naturally. Draws through the **L1 kit** like every S-VIEW surface: a -bordered box (X = velocity 0–127, Y = amp 0–1), the bezier traced in an accent hue, small draggable -node markers per control point (hover/drag-lit via kit states), add-point on empty-space click, -delete on modifier-click / drag-off. The shell (`reasampler_editor.cpp`) does the LICE draw + mouse -routing; **all geometry/hit-test/clamp math lives in the pure `velocity_curve` module**. On the -Zone view the same editor appears in the per-zone param panel (one curve per zone). Additive and -bit-identical for existing projects only under R10-F1 Option B; under Option A (the lean) it is -additive-but-behavior-changing. - -**Wave-plan slot (concurrency-aware).** The curve lands on `PerformanceZone`, so it is **blocked by -Wave 1 track T-KEYTRK** (which owns the `PerformanceZone` schema + zones-payload version bump). The -`velocityCurve` field must sequence as a **LATER additive payload bump AFTER T-KEYTRK merges** — the -two are sequential additive extensions of the same zones-payload record, not simultaneous ones, so -they never collide on one payload version. It is **not** blocked by T-STATE (that owns the -per-instance `ComponentState` v5→v6 envelope bump for preview velocity — a different struct on the -independent envelope version axis). Concretely: a **follow-on foundation track** (pure -`velocity_curve` + the `Voice::start` application point + the additive `PerformanceZone` field + -payload bump), gated on T-KEYTRK; plus a **Wave 2 shell-integration item** (the Sample-view + -Zone-panel curve-editor UI through the L1 kit), gated on the foundation track and composing with the -S-VIEW-2 Sample face + S-VIEW-3 envelope overlay. - -### The Sample-face recomposition — full-width hero, knob deck, curve popup (r11; Daniel, 2026-07-27) - -> **Additive S-VIEW revision (r11) — the layout spec for the Wave B editor rebuild.** Recomposes -> the landed S-VIEW-1..10 Sample face after Daniel's DAW pass: **every linear slider becomes a -> small radial knob** grouped into a fenced **knob deck**, the two **mode toggles shrink** from -> full-width control rows to compact group-header segments, and the **inline velocity-curve box -> is replaced by a miniature curve preview button** that summons a **full-size popup editor** — -> freeing the **hero waveform to run full width**. The view recomposition is otherwise **parameter- -> preserving** — existing params, their persisted seams, and the VST3 class UID are all unchanged. -> **Exception (landed in FB1):** r11 added a post-mixer **master gain** control (MASTER group, -> −∞…+24 dB dB-taper, per-sample ramped in the processor) backed by a new `masterGainLinear` -> field in `ComponentState`, bumping the **envelope version v7→v8**; pre-v8 blobs lift to unity -> gain. The **knob primitive** itself -> (minimal arc ~6→4 o'clock, needle indicator, vertical drag) is a **separate in-flight track**; -> this section specs the layout that consumes it. Directives (Daniel, 2026-07-27): radial knobs -> replacing all sliders, grouped "in a reasonable way" with **the envelope controls grouped -> together intuitively**; pitch-engine and Gate/Trigger toggles **not full-width**; the velocity -> curve as a **small square preview button right of the preview-velocity control** opening a -> popup with the full-size editor, **right-click removing a control point** in the popup; the -> waveform preview **full-width**. Product framing: `docs/product/midi-playback.md` §Addendum r11. - -**Inventory contract — every landed Sample-face element has a named home (nothing silently -dropped):** - -| Landed element (S-VIEW-1..10) | r11 home | -|---|---| -| Title band: name + live readout, Browse + Zone nav buttons | Unchanged | -| Hero waveform + S11 markers (start/loop/loop-fill) + "(decoding…)" placeholder | Unchanged, now **full width** (the velCurve carve-out is gone) | -| Envelope overlay polyline + draggable nodes (S-VIEW-3/F2) | Unchanged, over the full-width hero | -| Inline velocity-curve box (trace, node drag, click-add, Alt-click delete, drag-off delete) | **Popup editor** — all interactions preserved, + new right-click delete; summoned from the mini preview button | -| Fenced root spectral strip + root marker drag | Unchanged (cluster left, now remainder-width) | -| Preview-trigger button | Unchanged (cluster) | -| Preview-velocity horizontal slider | **Radial knob** in the cluster ("Vel"), same persisted `previewVelocity` seam (envelope-v6, untouched) | -| Mono \| Stereo toggle | Unchanged (cluster, right-anchored) | -| Mode row (Gate\|Trigger, full-width toggle) | **Compact toggle in the AMP ENVELOPE group caption row** | -| Pitch eng row (Varisp\|Preserve, full-width toggle) | **Compact toggle in the PITCH group caption row** | -| Attack / Hold / Decay / Sustain / Release sliders (Gate) | **Knobs in the AMP ENVELOPE group** | -| Length % / Fade in / Fade out sliders (Trigger) | **Knobs in the AMP ENVELOPE group** (time-ordered: Fade In · Length % · Fade Out) | -| Pitch env row (Off\|On, full-width toggle) | **Compact toggle in the PITCH ENV group caption row** | -| P.Attack / P.Decay / P.Depth sliders | **Knobs in the PITCH ENV group** | -| Key track slider | **Knob in the PITCH group** | -| Empty state ("pick a capture") + S13 drop-hint banner | Unchanged | - -**Band order (top → bottom) — hero becomes the elastic band.** Title (26, unchanged) → **hero -(full width between kPad margins, ELASTIC: absorbs all height left after the fixed bands, floor -150px)** → cluster (52, unchanged height) → **knob deck (fixed-height, bottom-anchored; ~92px -per deck row)**. The old fixed-150 hero + rest-of-window slider stack inverts: the control -surface is now the fixed band and the waveform grows with the window. At the unchanged 840×620 -default this yields a ~430px hero (see fork R11-F1 for the height call). - -**The knob deck — three fenced groups, left → right.** Each group is a hairline-bordered -`bg/panel` box with a **caption row** (~20px: micro-caps caption left; the group's **compact -mode toggle right-anchored in the caption row** — this is where the not-full-width toggles -live) over a **knob row** of fixed cells. Knob cell: **48w × 58h** — 28px knob centered, 12px -`Font::Micro` label beneath in `text/dim`; **the label swaps to the live value during -hover/drag** (no third line, no permanent value clutter). Toggle segments reuse the Mono/Stereo -grammar (~44–52px per segment, 18px tall, Active segment accent-primary). Groups: - -- **AMP ENVELOPE** — caption toggle: **Gate | Trigger** (`kPlayMode`). Knobs, Gate: **Attack · - Hold · Decay · Sustain · Release**; Trigger: **Fade In · Length % · Fade Out** (a deliberate - time-order reorder of today's row order — left-to-right matches the drawn envelope). The group - **reserves the 5-cell Gate width** so a mode flip repopulates in place and never reflows the - neighboring groups (Trigger simply leaves two cells blank). This is the "envelope controls - grouped together as a unit" directive: the group IS the envelope, its toggle picks the shape, - and the hero's envelope overlay is the same params drawn large (two surfaces, one model — - unchanged from S-VIEW-F2). -- **PITCH** — caption toggle: **Varisp | Preserve** (`kPitchEngine`). Knob: **Key Track** - (0–200%, the S-VIEW-6 scalar). Key tracking is repitch math, so it lives with the engine that - applies it. -- **PITCH ENV** — caption toggle: **Off | On** (`kPitchEnvEnable`). Knobs: **P.Attack · P.Decay - · P.Depth**. Its own fenced envelope unit, mirroring AMP ENVELOPE's grammar at smaller scale. - When Off, the three knobs draw `Disabled` (kit state) rather than vanish — stable geometry. - -Group gaps 12px; deck side margins kPad. Sum at these metrics ≈ 610px wide; **below that width -the pure module wraps whole trailing groups to a second deck row deterministically** (the deck -grows, the elastic hero shrinks toward its floor — the 560px `checkSizeConstraint` minimum -forces PITCH ENV onto row two, which is acceptable at the floor). - -**The cluster band (root + preview + curve button).** Root strip keeps the left side but becomes -**remainder-width** (min ~200px) instead of a fixed 55%; the right side is a fixed-width -right-anchored run: **Preview button (64w) · Vel knob cell (48w, the radial preview-velocity -knob) · curve preview button (28×28, immediately right of the Vel knob — per the directive) · -Mono | Stereo (right-anchored, unchanged)**. The **curve preview button** is a hairline-bordered -`bg/cell` square with the zone's live velocity curve traced in miniature (1px secondary-accent -trace, no node markers at this scale); Hover lifts it, and it draws **Active (accent-primary -border)** while its popup is open. It re-renders live as the popup edits the curve. - -**The popup curve editor.** Summoned by left-click on the curve preview button. A **centered -sheet over the Sample face**: a 0.50-alpha `bg/base` wash over the whole window (lighter than -Browse's 0.82 — this is a focused sub-editor, the Sample face stays legible behind it), then a -`bg/panel` + hairline sheet, **width clamp(60% of window, 360..520), height clamp(55%, 260..380)**. -Inside: a ~22px title row ("VELOCITY → AMP" micro-caps left, a **Close (×) button** 18×18 right), -and the **full-size curve box** filling the remainder using the existing `curveBoxFromRect` -inset/mapping-box grammar (one coordinate formula, as landed). Interactions are **identical to -the landed inline editor** — node drag with mouse-up commit, empty-space click adds + grabs a -point, Alt-click delete, drag-off delete (the box+24px drag-off margin stays inside the sheet, -so it cannot collide with dismissal) — **plus the NEW right-click delete (issue 3c): right-click -on a node removes it**, committing immediately through the same path as Alt-click, with the -existing `deletePoint` endpoint guard making endpoint right-clicks a safe no-op. Right-click -becomes the *primary* delete affordance; Alt-click and drag-off remain as landed alternates -(nothing dropped). **Dismiss:** Close click, click on the wash outside the sheet (only when no -drag is in flight), or Esc. Popup state (open flag + target zone) is editor-local, never -persisted. The `dragStartMap_` rollback contract is unchanged. - -**Module architecture (r11 delta — house pattern preserved).** - -- **Pure NEW `knob_deck`** — group boxes, caption rows, compact-toggle rects, knob-cell rects, - deterministic group wrap, and hit-test (point → control id + element kind). Mirror of - `action_bar` / `param_slider`; consumes the same shell-owned control-id descriptors; - engine-free, LICE-free, unit-tested (layout at reference widths, wrap at the 560 floor, - hit-test at cell/toggle boundaries). -- **Pure NEW `curve_popup`** — sheet/close/box geometry from the window size + the - outside-sheet dismissal test. Mirror of `overflow_menu`; unit-tested at the size clamps. -- **The knob primitive** (value↔needle-angle map, arc geometry, vertical-drag delta→value) is - the **separate in-flight track**; `knob_deck` treats a knob cell as a rect and defers - value↔angle to the primitive. Layout-level contract on it: vertical drag with resolution at - least matching the retired sliders; a Shift fine-drag is optional polish (mirroring the - envelope-node note), not required. -- **`param_slider` retires from the Sample face** (and from the Zone panel under fork R11-F2); - its toggle-segment helpers may be reused for the compact toggles or subsumed into `knob_deck` - — implementer's call at build. The preview-velocity control becomes a knob cell bound to the - same persisted `previewVelocity` (seam untouched). -- **Shell (`reasampler_editor.cpp`):** re-lays `computeSampleBands` (elastic hero, fixed deck), - draws groups/knobs/mini-button/popup through the **L1 kit** by palette role, routes - right-click (verify `WM_RBUTTONDOWN` reaches the child wndproc — see must-verify) and Esc. - Knob drags commit on mouse-up exactly as slider drags did (live invalidate, `commitAndReload` - on release); the knobs and the hero's envelope nodes stay two surfaces on one param model. - -**Spec'd aesthetic defaults (decided here; Daniel can veto at smoke test):** label↔value -swap-in on knob cells; Trigger knob time-order (Fade In · Length % · Fade Out); popup wash -0.50; group captions AMP ENVELOPE / PITCH / PITCH ENV; disabled-not-hidden PITCH ENV knobs. -The two genuinely open aesthetic calls are forks **R11-F1** and **R11-F2** in the fork ledger -below. - -### Engineering prerequisite 1 — drop-to-FX bug (routed to implementation, NOT a design call) - -**Symptom (Daniel):** dropping a capture onto a track's FX chain does not instantiate + init -ReaSampler 9000 — the audio-to-arrange drop works, but the "instrument init never fires." - -**RESOLVED (GA post-launch DAW-fix pass, 2026-07-28).** The root cause was the injection -mechanism, not the gesture wiring: `TrackFX_SetNamedConfigParm(..., "vst_chunk", )` -is silently unappliable for VST3 — REAPER's VST3 wrapper cannot apply unframed raw component-state -bytes there (the write parm returns true but the instance stays at defaults). The fix: -`instrument_drop` builds a Steinberg-format `.vstpreset` file image (header + 'Comp' chunk = -serialized component state, class ID from `reasampler_uid.h`); `instrument_drop_win` writes a -transient temp file and applies it via `TrackFX_SetPreset(track, fx, path)`, which the SDK -documents as accepting full `.vstpreset` paths for VST3 plug-ins. FX hotspot prefix-set extended -to `tcp.fx*`/`mcp.fx*`/`fx_*` (embed strip tokens excluded). `reasampler_uid.h` split out of -`reasampler_vst.h` as an SDK-free header so `instrument_drop` (pure module) can derive the -class-ID hex string without the VST3 SDK. -**Verify in DAW:** drag a capture onto a track's FX button → a ReaSampler 9000 instance appears on -that track already playing that capture (one Ctrl-Z removes it). **VERIFIED.** - -### Engineering prerequisite 2 — default window size for 1080p (routed to implementation) - -**Symptom (Daniel):** the editor window is too small by default; assume a 1080p minimum screen. - -**SDK sweep (done this pass — mechanism verified):** the VST3 editor size is set by the -`IPlugView`/`CPluginView` contract. `getSize()` returns the view's `rect` (set via `setRect` — the -default the host opens at); `checkSizeConstraint()` is where a minimum is enforced; `onSize()` -handles host resizes; `canResize()` already returns `kResultTrue`. **Verified in -`vendor/vst3sdk/public.sdk/source/common/pluginview.h`.** Today `ReaSamplerEditor`'s constructor -sets `ViewRect(0, 0, 560, 400)` — the undersized default. **The fix is a one-line default change** -(a larger initial `ViewRect` sized for the new three-band Sample face on a 1080p display) **plus -an optional `checkSizeConstraint` minimum** so the host cannot shrink the window below a usable -floor. **This is not a platform limitation — the mechanism exists and is trivial.** The exact -default dimensions are a build-time value to set against the Sample face's band heights (hero -waveform + control strip want materially more than 400px tall; a ~840×560 or larger default is the -starting point, tuned at build). **Routing:** flagged for **staff-engineer**; no product fork. -**Verify in DAW:** the editor opens at the new default on a 1080p screen showing the full Sample -face without scrolling, and cannot be resized below the constraint floor. - -> Build detail for this sub-phase (module architecture) moved to CONTEXT-ARCHIVE.md. - -### Precision / invariant implications - -- **Read-only bank consumer (unchanged).** Key-tracking, preview velocity, envelope-node edits, and - every marker/mode control are the instrument's **performance map / editor state** (D-B) — held in - the instrument's own VST3 component state, **never written to `Sample` or the bank.** -- **Additive, back-compat component state.** Three additive fields land: `keyTrack` (per- - `PerformanceZone`, default 100%) and — sequenced after it on the same zones-payload axis — - `velocityCurve` (per-`PerformanceZone`, r10, default the R10-F1 curve) inside the zones payload, - and `previewVelocity` (per-instance) as a new top-level `ComponentState` field via an **envelope - bump to v6** on the independent envelope axis. Every older blob lifts on read (absent `keyTrack` → - 100%, absent `velocityCurve` → the R10-F1 default curve, absent `previewVelocity` → the chosen mid - default), so already-saved instances restore cleanly. **Playback back-compat carries a caveat for - the velocity curve:** under R10-F1 Option A (flat y=1 default) an already-saved zone with no - stored curve now plays every velocity at unity — **not** bit-identical to the linear `velocity/127` - it played before; under Option B (linear default) it is bit-identical. This is the one non-back- - compat surface in S-VIEW and is the substance of fork R10-F1. `keyTrack` and `previewVelocity` - remain fully bit-identical on lift. No existing field changes. -- **RT discipline (unchanged).** The preview-trigger fires a note through the existing voice engine - via the off-audio-thread commit path (`commitAndReload` idiom); no new `process`-thread work, - no allocation on the audio thread. -- **VST3 class UID unchanged.** A view reorganization + additive param is **not** a compat event; - saved instances rebind and restore. The UID stays the S-NAME-1/S18 forever-locked identity. -- **Capture ≠ placement ≠ playback (unchanged).** The preview-trigger plays; it never captures, - never inserts a timeline item. The three acts stay distinct. - -### Open questions / forks (Daniel / Phase S team) - -*(S-VIEW-F1 and S-VIEW-F2 are SETTLED — Daniel 2026-07-27 — and folded into the spec above: -preview velocity **persists** via envelope-v6 `ComponentState`; the envelope overlay's nodes are -**draggable** via the pure `envelope_edit` module. They are no longer open questions.)* - -- **S-VIEW-F3 — Browse modal presentation. SETTLED (2026-07-27): full-window overlay** — landed - in S-VIEW-1/S-VIEW-5 (Browse renders as a full-window modal over Sample). Recorded here so the - ledger matches PLAN.md; no longer open. -- **R10-F1 — velocity-curve default (Daniel, 2026-07-27).** **Option A: flat y=1** (Daniel's - verbatim default — every velocity plays at full level; NOT back-compat with today's linear - `velocity/127`, so existing zones' dynamics change) vs. **Option B: linear y = x/127** (bit- - identical to the shipped engine; contradicts the verbatim "full level" default). **Lean A**, - flagged as a deliberate shipped-behavior change, not a silent regression. This is the only - non-back-compat surface the velocity-curve feature introduces. **Daniel's call.** -- **R11-F1 — hero height vs. default window (r11; Daniel, 2026-07-27).** With the knob deck - collapsing ~312px of slider rows into ~92px, the elastic hero at the unchanged 840×620 - default runs ~430px tall — waveform-dominant (Simpler-like; envelope-node drags gain vertical - precision). Alternative: shrink the default to ~840×520 (hero ~330px, a tighter face). - **Lean: keep 840×620 + elastic hero** — the freed space going to the waveform is the point of - the recomposition, and no default-size churn. Purely aesthetic. **Daniel's call.** -- **R11-F2 — Zone-panel parity (r11; Daniel, 2026-07-27).** Convert the Zone param panel to the - same knob deck + curve-preview-button/popup (one control grammar everywhere; retires - `param_slider`'s slider rows outright) vs. leave Zone on the landed slider rows (smaller Wave - B, but the same params render as knobs on Sample and sliders on Zone — a grammar fork). - **Lean: parity** — one grammar, and the two surfaces already share one storage site (S15-F2). - Cost: Wave B scope grows by the Zone panel re-lay. **Daniel's call.** - -### Must-verify before build (S-VIEW) - -- **Editor size mechanism** — `getSize`/`setRect`/`checkSizeConstraint`/`onSize`/`canResize` - **verified present** in `vendor/vst3sdk/public.sdk/source/common/pluginview.h`; confirm the exact - min-size enforcement point (`checkSizeConstraint`) behaves under REAPER's host at build. -- **Drop-to-FX** — `TrackFX_AddByName` / `TrackFX_SetPreset` / `GetThingFromPoint` all - **verified present** in `reaper_plugin_functions.h`; injection via `TrackFX_SetPreset` + - `.vstpreset` image (not `vst_chunk`) — **RESOLVED, GA DAW-fix pass 2026-07-28.** -- **Preview note through the voice engine off-thread** — confirm the existing `commitAndReload` - / off-thread reload idiom is the right seam to fire a one-shot preview note without touching - `process` on the UI thread; no torn state on the atomic voice-engine pointer. -- **Preview-velocity persistence seam (S-VIEW-F1)** — the envelope-v6 `ComponentState` field is the - prescribed seam (verified against `src/vst/sample_map.h` + `reasampler_processor.cpp` - getState/setState this pass); confirm at build that appending a `float previewVelocity` after the - assignment marker keeps the v5→v6 lift clean and the zones-payload byte offsets unchanged. -- **Envelope node-edit inverse map (S-VIEW-F2)** — confirm the pure `envelope_edit` pixel→param - inverse map produces params the sliders' own min/max already permit (so the two surfaces can - never diverge), and that the monotonic time-node constraint holds at the segment boundaries. -- **Envelope-overlay time base** — confirm the seconds→frames resolution the overlay draws against - matches the voice engine's live-rate resolution so the drawn shape lines up with the waveform. -- **Velocity-curve application point (r10)** — confirm `Voice::start()` (`sampler_core.cpp:252`) is - the sole velocity→gain site and that replacing `velocityGain_ = velocity / 127.0` (line 261) with - `velocityGain_ = curve.eval(velocity)` keeps the eval at note-on only, off the per-frame render - path (line 408 `gain = amp * velocityGain_` unchanged). No new `process`-thread work or allocation. -- **Velocity-curve payload sequencing (r10)** — confirm the additive `velocityCurve` field on - `PerformanceZone` lands as a zones-payload bump AFTER T-KEYTRK's `keyTrack` bump (not simultaneous), - so the two sequential extensions of the same record never collide on one payload version number. -- **Velocity-curve eval monotonicity (r10)** — confirm the pure `velocity_curve` bezier is monotonic - in x over the 0–127×0–1 box (one output per velocity) and that control-point edits stay clamped + - x-ordered at the boundaries. -- **Right-click routing (r11)** — confirm `WM_RBUTTONDOWN`/`WM_RBUTTONUP` reach the editor's child - wndproc (today only left-button + move are handled) before committing to right-click node delete; - same sweep for Esc/`VK_ESCAPE` key routing while the curve popup is open (the Browse search box - already takes keyboard input, so the focus path exists — confirm the popup sees it). -- **Deck wrap + hero floor (r11)** — confirm the pure `knob_deck` wrap is deterministic at the - 560×460 `checkSizeConstraint` floor (PITCH ENV onto row two) and the elastic hero's 150px floor - holds with a two-row deck at minimum height. -- **Knob primitive contract (r11)** — confirm the separately-built knob primitive and `knob_deck` - agree on the knob-cell rect and the value↔angle map (one formula each side of the seam), so the - drawn needle and the drag hit-test can never drift — the same one-formula discipline as - `curveBoxFromRect`. - -## Instance-usage detection — the un-prunable guarantee (pS-usage, 2026-07-28) - -> **Additive sub-phase of Phase S.** Adds a new safety seam between the VST3 instrument -> and the extension's prune path: a live ReaSampler 9000 instance holding a capture -> makes that capture un-prunable. Build detail and module architecture: CLAUDE.md -> §Architecture. The pure core (`sample_usage`) and its tests (`sample_usage_tests`) are -> REAPER-free; the REAPER-facing shell (`usage_scan`) is read-only at prune-scan time. - -### The guarantee - -A capture held by any live ReaSampler 9000 FX instance in the project **can never be -deleted by `BANK_PRUNE_FOLDER`**. If the prune cannot determine with certainty which -captures are held — because any usage record is unreadable or ambiguous — the prune -**aborts entirely (deletes nothing)**. Over-protection (prune skips a reclaimable file -or refuses to run) is the accepted residual; under-protection (deleting a file an -instance may still be playing) is a data-loss bug. - -### The wire: `rsusage_` - -Each VST3 instance holds a **per-instance GUID** persisted in `ComponentState` v11 -(`instanceGuid` field; pre-v11 blobs mint the guid on first publish). At the tail of -every `reloadInstrument` call (off audio thread) the processor publishes its held -`SampleRefs` paths to the ext-state key `rsusage_` in the `"reasampler"` -namespace via `reaper_bridge::writeUsageExtState`. That entry point is **prefix-guarded** -— it accepts only `rsusage_`-prefixed keys and refuses all others, so the read-only-bank -invariant is structurally enforced. - -**Direction:** the instrument writes usage keys; the extension reads them. This is the -one sanctioned instrument→ext-state write (a deliberate exception analogous to -`assignment_request` on the other wire), and it never touches the bank, view, tail, or -any other extension-owned key. - -### Liveness — no teardown clearing, no challenge/response - -Usage records are **never cleared by the instrument** at teardown: REAPER destroys the -plugin instance when an FX chain is set offline (including Design View's CPU-park), so -a terminate-time clear would strip the record of an instance that still exists in the -project. Liveness is decided extension-side at prune-scan time by cross-referencing the -usage records against the live FX enumeration (`usage_scan`). - -### The prune-scan fold - -At prune-scan time `usage_scan` (the REAPER-facing shell): - -1. Enumerates every `rsusage_*` ext-state key and decodes each record. A present-but- - unreadable record sets `abortPrune` — halting the prune, deleting nothing. -2. Enumerates every ReaSampler 9000 FX instance in the project: all tracks (master - included), normal + record/input chains, FX containers recursively, and take FX. - Truncated/partial enumeration → `abortPrune`. -3. Folds via `sample_usage::foldUsageRecords` / `usageHeldPaths` (pure, provable - without a DAW): a record counts iff its publishing track still hosts at least one - instance (offline FX included); a record with no track context counts while any - instance exists; and when records exist but zero instances were identified, - **every** record's paths are protected (identity-failure net — a matcher failure - must never degrade toward delete). - -The resulting held-paths set feeds `prune_reconcile::mergeReferenced`, which unions it -into the bank-referenced set. The orphan computation is therefore -`(owned ∩ present) − (bankRefs ∪ liveInstanceHolds)`. - -### Collision safety (FX copy / track duplication) - -A persisted GUID is copyable. Two design mechanisms close the copy gap in the -fail-safe direction: - -- **`ownerNonce`** — a per-lifetime nonce minted fresh in memory at instance creation, - never persisted. Proves "exactly this incarnation wrote the key last." -- **`unioned` flag** — a sticky multi-writer poison. Once a same-track sibling is - detected, the key enters union-forever mode: holds only accumulate, never drop. - -The publish plan (`planUsagePublish`) resolves every collision toward over-protect: -same-nonce + not-unioned → clean replace; same-track foreign nonce or unioned → union; -cross-track foreign nonce → remint under a fresh key. All three directions over-protect -at worst; none can under-protect. - -### Fail-safe summary - -| Situation | Outcome | -|---|---| -| Normal: record readable, instance live | Held paths added to `referenced` | -| Record readable, instance gone (stale key) | Paths excluded — not protected | -| Records exist, zero instances identified | ALL records' paths protected | -| Any record unreadable | Prune aborts — deletes nothing | -| Same-track copy detected | Union of holds; `unioned` flag set forever | -| Cross-track copy detected | Remint under fresh key | - -### `actions` integration - -`BANK_PRUNE_FOLDER` checks `PruneReport.abortedUnreadableUsage`; when set it halts -before any deletion and prints the offending `rsusage_*` key names with instructions -(the keys name which FX instances need attention). This is the only user-visible surface -of an abort — the dry-run path shows the same abort signal before any confirm step. - -### Deferred follow-up - -**Persist instance identity (TODO.md):** the per-instance `ownerNonce` is minted fresh -each incarnation and is NOT persisted. After save→reopen an instance cannot recognize its -own prior-session usage record — it looks foreign, so the instance unions and marks the -record `unioned` (append-only) forever. Net effect: after any reopen, prune stops -reclaiming captures an instance once held but no longer uses. Safe (never deletes a live -capture), but the bank folder grows without bound. The fix — persist the nonce in -`ComponentState` so an instance recognizes its own last-session record and does a -clean-replace — is deferred because a persisted nonce is inherited by a Ctrl+D in-place -FX duplicate, and a divergent clone must still be detected and protected fail-safe without -reintroducing the sibling-drop bug. - ---- - -# Look-and-feel — visual design language (Phase L) - -> **New pillar, own lettered phase, taken up by a parallel team.** Phase L is the -> whole-system look-and-feel effort — a shared LICE drawing kit and the surfaces that -> adopt it — that replaces the flat "temple os" drawing (opaque `LICE_FillRect` blocks + -> raw GDI `DrawTextA`) with a modern, sleek 2026 dark synth look across ReaSampler -> (the extension's docked bank panel) and ReaSampler 9000 (the Phase S VST editor + embed -> strip). Namespaced **`L` (Look-and-feel)**, orthogonal to and ungated by the -> M/D/B/R/V/S pillars. Product framing, the settled decision record (DS-1/DS-2/DS-3 all -> SETTLED 2026-07-26), palette, the three visual directions, and the toolkit assessment: -> `docs/product/visual-design-language.md`. Same standing discipline: **verify every LICE/ -> WDL/SWELL API name/signature against `vendor/WDL` before use.** When a point lands, -> doc-keeper moves it to `COMPLETED.md`. - -## What it is - -A **shared LICE-based drawing kit** (palette + type scale + component-draw layer) and the -surfaces that consume it. The kit is the **one source of drawing** for the whole system — -a button, row, slider, or waveform looks identical in the bank panel, the embed strip, and -the VST editor because it is the same kit function (the one-source-multiple-views instinct, -applied to drawing). The current "temple os" look is the system drawing at the **floor** of -LICE (flat fills, GDI text, no gradients/AA/rounded/hover); the kit lifts every surface to -LICE's actual ceiling — which REAPER's own themed UI and SWS prove is a modern dark UI. - -## Settled decisions (Daniel, 2026-07-26 — reasoning in `docs/product/visual-design-language.md` §6) - -- **DS-1 — toolkit: LICE + WDL free game, no external frameworks.** Draw with **LICE** - directly (gradients via `LICE_GradRect`, AA rounded via `LICE_RoundRect`/`LICE_Line`, - cached AA text via `LICE_CachedFont`). **Reuse any useful WDL/vwnd piece** — skin/image - helpers, draw idioms, a specific control (e.g. `virtwnd-listbox` for a long scroll list) - — where it beats re-deriving; "don't reinvent the wheel." **Reject external frameworks** - (iPlug2 / JUCE / VSTGUI — they re-open the settled bare-SDK+LICE build shape to solve a - look problem that is not a toolkit-ceiling problem). **Caution, not a ban:** keep - hit-test **geometry** in pure CTest-covered modules — do not import vwnd's retained-mode - object model wholesale (its controls own their hit-test internally, which would move - geometry into untestable shell code and undercut the pure/shell split). -- **DS-2 — visual direction: Direction B ("Neon Console") + Direction C's spectral - keyboard strip. SETTLED 2026-07-26, REVISED 2026-07-26 (Daniel) — three-accent pastel - accents AND REAPER-grey neutrals (palette-only).** *Neutral surfaces (revised):* the - neutral ladder moved **from near-black up into REAPER's mid-grey theme family** so the - dock reads as *part of REAPER, not a black slab* — `bg/base` ≈ `#2b2b2b` (REAPER chrome), - `bg/panel` ≈ `#333333`, `bg/cell` ≈ `#3a3a3a` (REAPER track bg), `line/hairline` ≈ - `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈ `~#a0a0a0`+. Elevation-ladder - discipline unchanged (base < panel < cell by a few %, micro-gradient + inner - highlight/shadow carry elevation, not hard borders). *Accent layer (revised):* changed - from **one electric cyan** to **three pastel accents**: `accent/primary` = **pastel lime - green** (`~176,224,152`, the - live/active/selected signal), `accent/secondary` = **pastel teal** (`~132,214,208`, - categorical role A), `accent/tertiary` = **pastel purple** (`~194,170,232`, categorical - role B); `accent/hot` is a lighter pastel-lime tint (`~200,236,178`). These are *starting* - values — the implementer locks final hex against the theme module's WCAG tests, staying - **within the pastel intent** ("soft-side" floor: the most pastel value that still clears, - not re-saturated toward neon). The **spectral (hue-mapped) keyboard strip** — signature - surface, glow-on-active as a static drawn state — is now a **pastel** sweep anchored on the - three accents (pastel-lime low → pastel-teal mid → pastel-purple high). **A stylish/bundled - font upgrade was considered and DECLINED (Daniel, 2026-07-26)** — no font bundling or - redistribution; the kit keeps its **current** cached-font face and no new typeface is - specified (§3.1). The kit palette stays **abstract** (role→color, one constants block — - now three accent roles), so the direction is a single-file change. *Tight pairs flagged for - the implementer (re-verify against the GREY ladder, not near-black):* **`text/dim` on - grey** (mid-grey-on-mid-grey — lands ~4:1 at start, must be lifted toward `~#a8a8a8`+ to - clear AA 4.5:1 body on `#333`/`#3a3a3a`, the classic floor failure); **the three pastels - as state indicators / active fills on `bg/cell`** (the pastel cushion shrank from ~15:1 on - near-black to ~6:1–7:1 on grey — still clears 3:1 but must be re-checked; if any drops - below floor, nudge that hue slightly deeper within the pastel intent); body text **on a - pastel fill** (light-grey on a light pastel can drop below AA 4.5:1 — use dark labels on - pastel fills or darken the fill); and **secondary vs. tertiary** distinguishability (both - cool/desaturated — verify they read as distinct categories). Full reasoning: - `docs/product/visual-design-language.md` §2.1 + §4 + §6. -- **DS-3 — dock-panel scope: a thorough layout redesign, not a light re-skin.** L2 lays out - the *full* button/affordance inventory — including M11's action-trigger buttons + - keybinding-help labels — intuitively, uncluttered, and useful, then applies the kit. - Sequenced after M11 merges so it designs against the actual landed button set. - -## Palette + the "punch" rule (Daniel's standing taste) - -Dark, modern, **visual punch over conservative contrast — now pastel, on REAPER-grey.** The -palette is defined by **role**, not hardcoded hue: `bg/base`, `bg/panel`, `bg/cell`, -`line/hairline`, `text/primary`, `text/dim`, `accent/primary`, `accent/secondary`, -`accent/tertiary`, `accent/hot`, `warn`. **Neutral surfaces (DS-2 revised, 2026-07-26):** -the neutral ladder sits in **REAPER's mid-grey theme family**, not near-black, so the dock -reads as *part of REAPER* — `bg/base` ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈ -`#3a3a3a`, `line/hairline` ≈ `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈ `~#a0a0a0`+. -A modern dark UI is built from **elevation layers**, not borders — surfaces gain a -**micro-gradient** (`LICE_GradRect`, a few percent lighter at the top) + a 1px inner -top-highlight / bottom-shadow (the vwnd trick) instead of flat fills; the grey ladder keeps -this discipline (base < panel < cell by a few %, subtle steps like REAPER, not hard -outlines). The three accents carry categorical meaning: **primary (pastel lime) = -live/active/selected**, **secondary (pastel teal) + tertiary (pastel purple) = supporting -categorical distinctions** (kinds, not intensity — primary is always "what's live now"). -**WCAG-floor discipline (pastel + grey re-read):** moving neutrals *up* into grey makes two -pairs *harder* — (1) **`text/dim` on `bg/panel`/`bg/cell`** is the classic -mid-grey-on-mid-grey floor failure (must be lifted light enough to clear AA 4.5:1 body on -the greyest surface it draws on), and (2) **the three pastels as state indicators on grey** -have a shrunken contrast cushion (~6:1–7:1 vs. ~15:1 on near-black — still clear 3:1 but -re-verify; nudge a hue slightly deeper within the pastel intent if it drops below floor). -Text-on-a-pastel-fill (AA 4.5:1) remains a tight pair. Take the *most pastel* value that -still clears the floor **on grey** — approached from the **soft side**, never re-saturated -toward neon "to be safe," except where the grey floor forces a small deepening. The `warn` -role (red/amber) is reserved *only* for byte-deleting or clip states (prune, delete). -Because the palette is role-based in one constants block, the settled-and-revised -B+pastel+grey+spectral direction (DS-2) is one file. - -## "Speed is the selling point" — a design constraint, not a tagline - -The UI must **feel instant, and no decoration may cost that.** Sub-frame hover/press/drag -feedback repainted immediately on the input message (instant acknowledgment *is* the -perception of speed); zero-jank via the preserved double-buffer discipline (draw to -`LICE_SysBitmap`, single `BitBlt`); region-scoped `InvalidateRect` during a drag -(build-time residual). **No decorative animation** — no tweens/fades/pulses; the only -permitted motion is a level/meter readout following the audio directly (as the embed strip -already does). Direction C's glow/bloom is a **static drawn state, never a pulse.** The -"fast" feeling is typography + hover + no-jank, not motion. - -> Build detail for this phase (kit architecture, L2/L4/L5 dock-panel layout contracts, L7 implementation detail, the L3 gate, LICE/WDL API surface) moved to CONTEXT-ARCHIVE.md. - -## L7 capture ordering · card metadata · selection styling (ordering model · overlay · tertiary-border selection) - -L7 is the **next ungated dock-panel pass over the same `bank_panel` grid** L4–L6 built. Unlike -L4/L5 (pure layout over existing actions), **L7 (1) is a persisted-model change** — it adds an -explicit per-sample display order with gap-preserving sparse placement to the persisted bank -state and its JSON round-trip. (2) and (3) are draw-only. **No new capture/placement behavior;** -the "capture ≠ placement" principle is untouched. Drawn through the L1 kit in the DS-2 -grey-neutral + three-accent-pastel palette; **no palette/font decision is re-opened.** Ungated -by Phase S; independent of the L3 gate. Sequences AFTER L6. - -**Research baseline (dev, confirmed at spec time).** The grid draws in **`BankIndex` insertion -order**: `BankIndex` holds `std::vector samples_` in insertion order (`all()`), and -`bank_grid::computeCellRects` tiles exactly one contiguous rect per sample left-to-right, -top-to-bottom — **no explicit ordinal, no sparse slots today.** `Sample` already carries -`lengthSeconds`, `lengthBeats`, and `captureTempo` (BPM at capture) but **no time-signature -field.** The selected cell is drawn (`bank_panel::drawThumbnail`) as -`InteractionState::Active` (accent-fill surface) + an `accent/primary` border + an *inverted* -waveform (`bg/base`), with focus as a `text/primary` double-line ring. Drag today: press on a -selected cell arms a drag; crossing a threshold begins it; leaving the client rect hands off to -OS drag-out (`drag_out::decideGesture`); a drop on a tab / the other region moves (or copies on -Ctrl) the samples to that bank. **There is no same-bank in-grid reorder today.** - -### Drag disambiguation (F3 — SETTLED 2026-07-27) - -In-grid reorder must coexist with the existing internal bank-move/copy drag and OS drag-out. -**Precedence (one clean rule, evaluated live during the drag):** -1. **Pointer leaves the client rect → OS drag-out** (unchanged; `drag_out::decideGesture` wins - first — the existing invariant #4 boundary). -2. **Else drop lands on a tab / the OTHER region's bank → move/copy** (unchanged; Ctrl = copy). -3. **Else drop lands within the SAME bank's own grid → reorder-to-slot** (new). - - Onto an **empty** slot → place there. - - Onto an **occupied** slot, **no modifier** → insert-before-and-shift-tail (the default, - above). - - Onto an **occupied** slot, **Alt held** → **REPLACE** the occupant (below). - -So: leave-client wins → else other-bank wins → else same-bank-grid = reorder. The precedence is -encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); the shell reads the -live pointer + focused region + client rect + **modifier state (Alt)** and calls it. This keeps -the reorder gesture from ever stealing an intended bank-move or OS-drag, and keeps a same-bank -in-grid drag from being mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it -reorder meaning). - -**Alt+drop-onto-occupied = REPLACE (SETTLED 2026-07-27).** Holding **Alt** at drop over an -occupied slot **replaces the occupant** instead of inserting-and-shifting. Replace semantics, -precisely: -- The replaced sample is **removed from THAT bank's index only** — same semantics as the existing - remove-from-bank verb (`BankBook::removeSample` index-only). **The file stays on disk;** the - owned-manifest and Phase R prune govern its bytes. If the replaced sample's last reference - disappears, that is exactly the existing cross-bank-reference story (`hashReferencedElsewhere` - reports whether the hash is still referenced elsewhere; prune later reclaims a now-orphaned - owned file). Replace introduces **no new deletion authority** — it never touches the disk. -- The dragged sample then takes the vacated slot (the slot's position is preserved; only its - occupant changes). -- **Pool case (un-evacuable pool — SETTLED rule).** The pool's privileges (un-deletable, - un-renamable, **un-evacuable**, never zero banks) are enforced in `bank_book`. **Alt+Replace is - allowed in the pool only when it does not violate a pool privilege.** Concretely: replacing a - pool entry is an index-only removal of that pool sample; it is **permitted** as long as it does - not empty the pool below the pool's floor and does not remove the *last* reference in a way the - pool's rules forbid. The consistent rule the model enforces: **the replace's index-removal step - is the same operation as remove-from-bank, and it must pass the same pool-privilege guard that - remove already applies — if remove-from-pool would be rejected for that sample, Alt+Replace over - it is rejected too** (the drop falls back to a no-op; the pool-privilege guard is reused - as-is — confirmed at build). No special pool-only replace path; one rule, guarded by the - existing pool invariants. - -**Drop-result cursor cues (SETTLED 2026-07-27 — REAPER-idiomatic special cursors).** During a -drag the cursor must indicate **what the drop will do**, following REAPER's idiomatic use of -distinct action cursors. The cue set: -- **Reorder-to-slot** (within the same bank's grid) — a move/reorder cursor. -- **Move/copy to another bank or tab** — the move (or copy, when Ctrl is held) cursor, matching - the existing internal-drag semantics. -- **OS drag-out** (pointer left the client rect) — the OS copy/drag cursor (owned by the OS drag - loop once handed off). -- **Replace** — a distinct replace cursor, shown **only when Alt is actually held over an - occupied slot** (i.e. only when precedence resolves to the Alt+replace case). It must not appear - over an empty slot or when Alt is not held. - -**Shell mechanism:** the shell sets the cursor via Win32/SWELL `SetCursor`. SWELL stock cursors -were chosen at build (Reorder→`IDC_SIZEALL`, Move→`IDC_HAND`, Copy→`IDC_UPARROW`, -Replace→`IDC_SIZEWE`; no custom cursor load/synthesis required). **The *decision* of which cue -applies stays in the pure gesture/disambiguation helper** — the same helper that resolves -precedence returns the resolved gesture (reorder / move / copy / os-drag-out / replace), and the -shell maps that pure result to a cursor. No cue logic in the shell; the shell only owns the -`SetCursor` call and the cursor resources. - -## Precision / invariant implications (what Phase L does NOT change) - -- **The pure/shell split holds.** All layout/hit-test stays in pure CTest-covered geometry - modules; the kit's *draw* half is shell, its *geometry* half is pure — even where a WDL - piece is reused (DS-1). No hit-test math moves into untestable code. -- **RT discipline untouched.** The kit is draw-thread only; nothing here touches `process` - or any off-thread reload handoff (a Phase S concern surfaced at L3). -- **Read-only-over-bank untouched.** This is look-and-feel; no data-ownership change. -- **The capture/placement load-bearing principle is untouched.** Phase L draws; it does not - capture, place, or mutate the bank. -- **VST3 class UID / component-state contract unchanged** (Phase S concern; noted for L3). -- **Windows-only (D5).** Font/GDI/HFONT choices assume Windows; no cross-platform font - fallback concern. - -## Non-goals / guardrails - -- **No external UI framework.** iPlug2 / JUCE / VSTGUI are rejected (DS-1). LICE + reused - WDL pieces are the toolkit; reject any path that pulls in a new framework. -- **No hit-test geometry in untestable shell code.** Even when reusing a WDL piece, layout/ - hit-test math stays in pure CTest-covered modules (DS-1 caution). Reject a control whose - adoption would move geometry into the shell without a pure test seam. -- **L2 does not restructure the panel bones.** The vertical-split / grid / tab structure is - sound and stays; L2 designs the *layout of the button inventory around it* (DS-3). A - ground-up structural rework of landed Phase-B panel structure is out of scope. -- **L3 does not build on dev until Phase S lands there.** The gate is explicit; do not chase - Phase S files on dev. -- **A visual refresh is not a compat event.** VST3 class UID, command-id strings, ext-state - namespaces, and component-state contracts are unchanged by Phase L. -- **Verify LICE/WDL/SWELL surfaces** against `vendor/WDL` before use. - ---- - -# Structural reorganization — reorg spec (Phase Q — Quality) - -> **New pillar, own lettered phase, and the LAST structural pillar.** Phase Q is a **pure -> structural refactor** of `src/` — more encapsulation, granular namespaces, `core/`/`shell/`/ -> `app/` subdirectories — against a stated quality bar (*"mtytel Vital is my code reference for -> quality"*), to bring the codebase "into the realm of something I can stand to look at." It -> **ships no feature and changes no behavior**: the test suite passing unchanged is the proof of -> correctness. Namespaced **`Q` (Quality)** — M/D/B/R/V/S/L taken; `Q` names the end (the quality -> bar), the reorg being the means. Product framing, the Vital-grounded target shape, the -> grep-verified SOLID audit (the evidence base), and the fork record (Q-1..Q-6): -> `docs/product/code-organization.md`. When a point lands, doc-keeper moves it to `COMPLETED.md`. -> -> **Phase Q opens with a pre-restructure audit (Q-W0 — added 2026-07-27).** Before any structural -> point (Q-W1+), Phase Q runs a **functional + DSP quality audit** that produces a written, triaged -> findings report — a functional-correctness/algorithm-quality complement to the SOLID/naming audit -> below. **Q-W1 is gated on Q-W0's triage being complete and Daniel signing off on each finding's -> disposition** (fix-now vs. document-and-defer). Spec: §"The pre-restructure audit wave (Q-W0)" -> below. **STATUS (2026-07-28): Q-W0 COMPLETE and SIGNED OFF — all 59 dispositions approved; the -> Q-W1 sub-gate is satisfied once the six approved fix-now remediations land (in flight on -> `pq-w0-fixes`). The audit's plan reshape is folded into this spec: Q-W2 6→8 seams, NEW wave -> Q-W2v (parallel with Q-W2), Q-W3 3→4 hoists + riders, Q-W5 + the ext-state-loop dedupe.** - -## What it is - -A directory + namespace + file-split reorganization that makes ReaSampler's **already-real, -CMake-enforced** architecture **legible in the code's shape**. The pure/shell split exists (30 -pure static libs, each with its own test executable, none linking a REAPER SDK) but is invisible: -all 45 files sit in **one flat `src/`**, all 37 headers in **one flat `reasampler` namespace**, -four modules have grown into god-modules, and the JSON parser is copy-pasted across four models. -Phase Q gives the existing subsystem grouping — model / view / capture / audio / ui / reclaim / -version — a **structural home** (directories + namespaces), splits the four god-modules along -validated seams, and extracts the duplicated JSON into one pure module. Nothing about the -architecture *changes*; it becomes *visible*. This is why the phase can be zero-runtime-cost and -CTest-green throughout: the seams already exist in the link graph; Phase Q draws them where a -reader sees them. - -## The pre-restructure audit wave (Q-W0) - -Phase Q **opens** with `Q-W0` — a thorough **static/functional audit** that runs before any -structural point (Q-W1+) moves a single file. It is the *functional-correctness and -algorithm-quality* complement to the grep-verified SOLID audit (§"The evidence base" in -`docs/product/code-organization.md` §2) and naming audit (§2b): those ground *where responsibilities -live* and *what things are called*; Q-W0 grounds *does the code do the right thing, and does it do it -well.* It exists because the structural reorg is the wrong moment to discover a bad algorithm — a -reinvented wheel or a numerically-fragile DSP path should be eliminated or consciously documented -**before** it is relocated, re-namespaced, and split, not carried forward untouched into a tidier -tree. Bringing the code "into the realm of something I can stand to look at" is not only a matter of -shape; it is also a matter of the code being *functionally sound*. - -**STATUS (2026-07-28): Q-W0 is COMPLETE — audit run, triage done, sign-off given.** Four parallel -tracks (T1 DSP, T2 architecture, T3 env-coupled constants, T4 sizing + placement), **59 -findings**; report: `docs/product/code-quality-audit.md`, appendices: -`docs/product/audit-notes/q-w0-t{1..4}-*.md`. Daniel approved **every disposition as proposed** on -2026-07-28. Six findings are fix-now *in Q-W0* — **T1-01** (linked-lag stereo splice alignment), -**T1-03** (Preserve prime bound + immediate `freezeTail()`), **T1-09** (`declickR_` dead-state -removal, riding the `sampler_core` edits), **T2-01(a)** (provenance wire-cursor hardening -backport), **T3-01** (gain-ramp seconds), **T3-03** (fade-ceiling seconds) — in flight on branch -`pq-w0-fixes`; **the Q-W1 sub-gate is satisfied once they land** (each with its module's CTest -target green; the audible DSP fixes with a stated before/after listening check). All other -fix-nows are assigned to the wave that already opens their file and are recorded in the wave specs -below. **The Q-11 question is answered:** the correlation-aligned SOLA pitch engine is **sound — -no technique replacement (phase-vocoder / WSOLA) warranted**; every pitch finding is a bounded -in-technique fix or a documented operating limit. - -**Audit scope — the named surfaces:** - -1. **DSP / audio, close eye on pitch.** Assess *algorithm quality* — correctness, artifacts, - numerical robustness, interpolation quality, and reinvented-wheel-vs.-established-technique — on: - - `src/vst/pitch_shift` — the hand-rolled OLA pitch-preserve engine: window/overlap choice, phase - coherence, transient and formant behavior, buffer-edge handling. **The highest-priority DSP - surface** (Daniel: "a close eye on the Pitch stuff"). - - `sampler_core` — repitch ratio math, interpolation order/quality, loop-point-aware sustain - crossfade, and voice-stealing correctness (clicks/discontinuities on steal). - - `peaks` — envelope min/max binning correctness. - - `wav_trim` — the realtime-tail decay-scan threshold + truncate plan. - - the capture / tail paths — any DSP-adjacent arithmetic in capture range/tail handling. -2. **Architecture smells.** Duplicate code, reinvented wheels, poor abstractions, and leaky - pure/shell boundaries (a `core/` file reaching a REAPER/host type; geometry or algorithm math - sitting untestable in a shell instead of a pure module). This overlaps the SOLID audit's territory - but targets the *functional* smell, not the responsibility-placement smell — Q-W0 reports what - §2/§2b did not. -3. **Env-coupled-constant domain-modeling smells (explicit category).** ANY value stored in a - frame / rate / DPI / tick-coupled domain that should instead be stored **rate-free and resolved at - the point of use** is a domain-modeling smell — *store rate-free, resolve at use*, **not** "rescale - by the rate." This is grounded in the load-bearing invariant that wall-clock times are stored as - rate-free **SECONDS** resolved against the live project rate (`sample_map`), with **NO hardcoded - sample rates in `src/`** (Daniel's standing ruling). There was a prior incident on exactly this — - envelope times stored in the frame domain — which is why it is a first-class audit category, not a - footnote. Sweep at least: envelope times, loop points, fade lengths, tail lengths, and any UI - geometry constant that silently bakes in a DPI or rate. - -**Deliverable + acceptance:** a **written findings report** exists covering the named surfaces; -**every finding is triaged** into *eliminate-before-restructure* (fix-now) or *document-and-defer* -(with a one-line rationale so the deferral is a decision, not an omission). Fix-now findings are -remediated **in Q-W0**, or folded into the downstream wave that already opens the file (recorded per -finding) — they are **not** silently deferred into the structural waves. Any behavior-changing -remediation lands with the module's CTest executable green, and where a DSP path changes audibly, a -stated before/after listening or null check. **The gate to begin Q-W1 is: triage complete and Daniel -signed off on every disposition.** - -**Relationship to the structural waves:** Q-W0's findings may **add or reshape** downstream -Q-W1..Q-W6 points (e.g. an algorithm rewrite that changes a module's shape, or a domain-modeling fix -that changes a payload). Those reshapes are folded in before Q-W1 begins. Q-W0 is thus both the entry -point and a scoping input to the rest of the phase. - -**Report home (Q-10 — SETTLED, Daniel 2026-07-27): a committed doc.** The findings report lives as a -committed doc under `docs/product/` — `code-quality-audit.md`, alongside the SOLID/naming audit that -already lives in `docs/product/code-organization.md`. It sits beside the existing audit, travels with -the tree, and each finding's disposition is reviewable in one place. A tracked issue list was set -aside — the audit is a one-shot pre-reorg sweep, not an ongoing backlog. The Q-W0 "Triage + report" -step writes this file. See `docs/product/code-organization.md` §2c.3. - -**Pitch-remediation depth (Q-11 — SETTLED, Daniel 2026-07-27): defer to findings.** How deep the OLA -pitch-preserve remediation goes is decided by what Q-W0 finds, not pre-committed. Default is -**document-and-defer**; only if the audit surfaces artifacts that matter is a **bounded fix** (tune -window/overlap/edge handling) weighed **before** a **technique replacement** (phase-vocoder / WSOLA). -A technique replacement reshapes `pitch_shift`, would spill a downstream Q-wave point, and is -therefore **a Daniel decision at triage time, not an automatic Q-W0 action**. See §2c.4. - -## The quality bar — Vital (read from its actual `src/` tree) - -Vital (`github.com/mtytel/vital`) groups its synth by **subsystem** — `common/` `synthesis/` -`interface/` `plugin/` — with `synthesis/` further subdivided by function (`synth_engine/ -modulators/ filters/ effects/ producers/ framework/ lookups/ utilities/`). The grouping principle -is **layered functional architecture**: the directory tree *is* the architecture diagram; you read -the subsystem map off the folders. ReaSampler adopts the *pattern* (directory = architecture), -adapted to its own most load-bearing invariant — the pure/shell split — as the top level (see -below). Vital is GPLv3; the borrowed artifact is the **structural pattern**, not code. - -## Settled decisions (Q-1/Q-10/Q-11 settled 2026-07-27; Q-5/Q-6/Q-8/Q-9 + the audit's §4 forks settled 2026-07-28 — REC history kept; see `docs/product/code-organization.md` §6 and `docs/product/code-quality-audit.md` §4) - -- **Q-1 — namespace letter. SETTLED: `Q` (Quality).** Point-id family `Q1..Qn`, wave prefixes - `Q-W0` (the pre-restructure audit) then `Q-W1..Q-W6` (the structural reorg; + **`Q-W2v`**, the - VST god-module wave added at the Q-W0 sign-off, 2026-07-28). `O` (Organization) - was set aside: the glyph reads ambiguously against zero in - point ids, and "Organization" undersells a phase measured against a *quality* bar. -- **Q-2 — JSON extraction in scope + first. REC: yes.** The 4× duplicated `Parser` is the largest - DRY+SRP violation and is entirely off the hot paths — the ideal safe, high-leverage opener - (Q-W1). -- **Q-3 — directory shape. REC: `core/`/`shell/`/`app/` top-split, subsystem dirs beneath.** Top - level by the pure/shell discipline (so the invariant is *structural*, not merely conventional), - subsystem grouping one level down. Preferred over pure-Vital subsystem-first because ReaSampler - has a pure/host split Vital lacks and that split is the invariant most worth protecting - structurally. -- **Q-4 — sub-namespace to match sub-directory. REC: both.** `reasampler::model`/`view`/`capture`/ - `audio`/`ui`/`reclaim`/`version`/`json`. Directory and namespace agree; a symbol's home is - unambiguous from either. -- **Q-5 — god-module split granularity. REC: to the audit's named seams, no finer.** Well-factored, - not atomized. **SETTLED (Daniel, 2026-07-28): seams-by-responsibility with the Q-W0 T4 seam - lists adopted** — `bank_panel` 6→8 seams (+ `panel_layout`, `panel_drag`; T4-01), - `capture_orchestrator` further split with `capture_batch` (T4-02) — **and the ~600-line file - ceiling is an acceptance criterion on every split wave**: seams are the method, the ceiling is - the bar; arbitrary bisection to hit the number is rejected. One documented exception: - `sampler_core.cpp` stays whole (T4-14/T4-27). -- **Q-6 — OCP registration-table. REC: in scope, last (most droppable if narrowing).** **SETTLED - (Daniel, 2026-07-28): in scope, last wave, as planned.** -- **Q-7 — naming rides the relocation waves, no dedicated naming wave. REC: yes** (forced once - Q-3/Q-4 settle — a rename is near-free during relocation, near-pure-churn standalone). -- **Q-8 — class/module renames beyond the free namespace fix. REC: fix the two that actively - mislead** — `BankIndex`→`BankModel` (the `bank_model.h`/`BankIndex` file↔class word-mismatch) - and the unified JSON parser → `json::Reader`/`json::Writer` (or `json::Parser`) — **leave the - merely-quirky** (`Book`/`Bank`/`Index`, `Sample`/`AudioSample`, `MinMax`, `KitBox`). Daniel's - to call. **SETTLED (Daniel, 2026-07-28): both renames** — `BankIndex`→`BankModel` (W1) and the - JSON parser minted as `json::Reader`/`json::Writer` (W1). From the audit, additionally: - **`ICaptureBackend` is deleted in Q-W3** (T4-26 — one deriver, zero polymorphic call sites; the - CLAUDE.md/CONTEXT "two backends behind one interface" correction **rides Q-W3's own commit**, - not earlier). -- **Q-9 — align the `capture_realtime` (shell) / `realtime_record` (pure) word-order inversion. - REC: yes, during Q-W3** (a free rider — W3 already hoists the realtime lifecycle). **SETTLED - (Daniel, 2026-07-28): yes** — the pure module takes the stem `capture_realtime`, the shell - takes the suffix (the `drag_out`↔`drag_out_win` model), during W3. -- **Audit §4a — VST placement. SETTLED (Daniel, 2026-07-28): T4-18** — `src/vst/` integrates into - the single `core/`/`shell/` top split as `core/instrument/{engine,map,ui}` + - `shell/instrument/`. One rule, no special case: a file's directory says whether it may touch a - *host* type (REAPER **or** VST3 SDK); the artifact boundary is a link-graph fact the sources - already straddle. The T4-19 artifact-first subtree was set aside. Directory map below updated. -- **Audit §4e — WAV/RIFF consolidation moment. SETTLED (Daniel, 2026-07-28): a named rider on - Q-W3** — one pure **`wav_codec`** owner (chunk walker + layout + build + patch), absorbing the - T4-10 ingest extraction (T2-08 / T4-23 / T4-10). The dedup-by-hash / null-test maintenance - surface gets exactly one implementation. -- **Audit §4f — Q-W2v scheduling. SETTLED (Daniel, 2026-07-28): parallel with Q-W2** (different - artifact, zero file overlap); the serial "Q-W7" alternative was set aside. -- **Audit §4b/§4c/§4d — the Q-W0 fix-now set. SETTLED (Daniel, 2026-07-28): all approved - fix-now** — T1-01 + T1-03 (with T1-09 riding), T2-01(a), T3-01 + T3-03; in flight on - `pq-w0-fixes`; landing them closes Q-W0 and opens Q-W1. - -## The directory + namespace map (Q-3 / Q-4) - -Top-level by the pure/shell discipline; subsystem dirs beneath `core/`; namespaces mirror -directories. - -**`core/` (pure, no REAPER types, unit-tested — `reasampler::`):** -- `core/model/` (`::model`) — `bank_model`, `bank_book`, `owned_manifest`, `provenance` -- `core/view/` (`::view`) — `view_mode_model`, `view_tree`, `lane_keys`, `mode_switch` -- `core/capture/` (`::capture`) — `render_settings`, `batch_capture`, `tail_control`, - `capture_paths`, `wav_trim`, `insert_plan`; **post-Q-W3** `wav_codec` (the one pure WAV/RIFF - owner — audit §4e rider) -- `core/audio/` (`::audio`) — `peaks` -- `core/ui/` (`::ui`) — `theme`, `component_geometry`, `bank_grid`, `tab_strip`, `action_buttons`, - `prune_button` -- `core/reclaim/` (`::reclaim`) — `prune_reconcile` -- `core/version/` (`::version`) — `app_version` -- `core/json/` (`::json`) — **NEW** — extracted parser/serializer, minted as - `json::Reader`/`json::Writer` (replaces the 5 duplicate hand-rolled decoders — the four - `Parser`s + `tail_control`'s, T2-02) -- `core/instrument/` — **T4-18 SETTLED (Daniel, 2026-07-28)** — the VST artifact's pure side - joins the one top split (directory = "may it touch a host type", REAPER *or* VST3 SDK): - - `core/instrument/engine/` — `sampler_core`, `pitch_shift`, `velocity_curve`, `master_gain` - - `core/instrument/map/` — `sample_map` (+ `component_state_io` post-Q-W2v), `bank_sync`, - `bridge_marshal`, `note_entry`, `trigger_seam` - - `core/instrument/ui/` — `editor_geometry`, `keyboard_strip`, `waveform_view`, - `capture_browser`, `browser_scroll`, `param_slider`, `knob_deck`, `curve_popup`, - `envelope_overlay`, `envelope_edit`, `embed_strip` - -**`shell/` (REAPER-facing — subdir by subsystem, namespace as house style prefers):** -- `shell/capture/` — `capture`, `capture_realtime`, `provenance_shell`, `track_guid`, `item_read`; - **post-Q-W3** `capture_orchestrator`, `capture_batch`, `scope_resolve`, `realtime_lifecycle`, - `capture_realtime_finalize` (with the Q-9 stem/suffix rename) -- `shell/panel/` — `draw_kit`; **post-Q-W2** `panel_render`, `panel_thumbnails`, - `panel_audition`, `panel_input`, `panel_layout`, `panel_drag`, `panel_bank_ops`, - `panel_window` (from `bank_panel` — eight seams, T4-01) -- `shell/view/` — `view` -- `shell/persist/` — **post-Q-W5** `session`, `ext_state_io`, `prune_fs` (from `persist`) -- `shell/actions/` — `drag_out_win`; **post-Q-W4** `design_view_actions`, `bank_actions`, - `prune_action` (from `actions`) -- `shell/instrument/` — `reaper_bridge`, `reasampler_embed`, `vst_entry`, `reasampler_vst.h` / - `reasampler_uid.h`; **post-Q-W2v** the editor TUs (`editor_session` / `editor_controls` / - `editor_paint_sample` / `editor_paint_browse_zone` / `editor_input_sample` / - `editor_input_browse_zone` / `editor_platform` — `editor_layout` hoists *pure* → - `core/instrument/ui/`, discharging T2-06) and the processor TUs (`processor_state` / - `processor_reload` / lifecycle+`process()`) - -**`app/`:** `main.cpp` (post-Q-W3: API-pointer ownership + `ReaperPluginEntry` + dispatch only). - -**Collision sweep (before W1):** `Sample` (`::model`) vs `AudioSample` (`::audio` alias) vs the -unified `Parser` (`::json`) must not collide once flattened into granular namespaces; resolve by -subsystem home. - -## The god-module split seams (Q-5 — SETTLED 2026-07-28: T4 seam lists adopted; ~600 ceiling is the bar) - -Split each to the audit-validated seams, no finer — **and every shipped TU lands under the -~600-line ceiling** (the Q-5 settlement: seams are the method, the ceiling is the bar; arbitrary -bisection to hit the number is rejected; the one documented exception is `sampler_core.cpp`). -LOC figures updated to the Q-W0 T4 census (2026-07-28): - -- **`bank_panel.cpp` (3459 LOC → `shell/panel/`, Q-W2 — eight seams, T4-01):** `panel_render` - (draw/paint) / `panel_thumbnails` (compute+cache) / `panel_audition` (preview engine — **hot - path, direct call-through**) / `panel_input` (mouse/key/wheel + new-content detection) / - **`panel_layout`** (toolbar/footer/menu rects + row/cluster builders + region geometry — new - seam) / **`panel_drag`** (the card-drag/hover state machine, pure mirror `card_drag` — new - seam) / `panel_bank_ops` (bank CRUD — the single owner W4 dedupes against) / `panel_window` - (lifecycle + OS drag-out/drop-target). Without the two new seams, `panel_render` (~700) and - `panel_input` (~800) would ship over the ceiling. Split the fat `bank_panel.h` per seam (I). -- **`main.cpp` (1897 LOC → hoist to `shell/capture/`, Q-W3 — four hoists, T4-02):** - `capture_orchestrator` (`RunCapture`/`captureAndIndexOne`/`renderOffline`/single-capture + - realtime/insert action bodies — lands ~450) / **`capture_batch`** (batch family + - `RunRecaptureFromSource` + the two RAII selection guards — new hoist; recapture is - planner-driven like batch and shares the guard machinery) / `scope_resolve` (range/razor/track - resolution + provenance assembly inputs) / `realtime_lifecycle` (state machine + globals). - `FxBypassGuard` moves out but **stays stack RAII** (precision-critical). `main.cpp` → `app/`, - reduced to pointers + entry + dispatch. **Wave riders (SETTLED 2026-07-28):** delete - `ICaptureBackend` + correct the CLAUDE.md/CONTEXT description in the same commit (T4-26); - shared `stampCaptureSample` epilogue dedupe (T2-09); `capture_realtime_finalize` split with - the Q-9 rename (T4-08); `makeUniqueTag` per-session monotonic counter (T1-11); the pure - `wav_codec` consolidation (audit §4e). -- **`actions.cpp` (1016 LOC → `shell/actions/`, Q-W4 — T4-03: seams unchanged, still - sub-600):** `design_view_actions` / - `bank_actions` / `prune_action` (`doBankPruneFolder` — the single file-deletion action). Dedupe - `promptText`/`mintBankId` + bank verbs against `panel_bank_ops`. -- **`persist.cpp` (852 LOC → `shell/persist/`, Q-W5 — T4-04: seams unchanged; the pS-usage - growth landed exactly where this wave isolates it):** `session` (lifecycle/poll + - `BeginLoadProjectState` reload hook) / `ext_state_io` (serialization bridge + GUID minting + - folder relocation) / **`prune_fs`** (prune scan + `deleteOrphanFile` via `SHFileOperationW` — - the isolated single file-deletion authority). **Wave rider (T2-04):** the `GetProjExtState` - grow-loop ×3 dedupes onto `bridge_marshal`'s generalized retry policy — `usage_scan`'s - prune-safety-adjacent copy included. -- **`reasampler_editor.cpp` (3065 LOC — the largest file in the repo → `shell/instrument/`, - Q-W2v — eight TUs, T4-11; split axis = the Sample/Browse/Zone face structure):** - `editor_session` / `editor_controls` / `editor_layout` (**pure-candidate hoist** → - `core/instrument/ui/`, `editor_geometry` the named owner — discharges T2-06's - stranded-layout-math finding) / `editor_paint_sample` / `editor_paint_browse_zone` / - `editor_input_sample` / `editor_input_browse_zone` / `editor_platform`. Rider: adopt the pure - `ThumbnailKey` on the VST side while the editor is open (T2-10). -- **`reasampler_processor.cpp` (1164 LOC → `shell/instrument/`, Q-W2v — three TUs, T4-12):** - `processor_state` / `processor_reload` / lifecycle+`process()` kept whole. **No virtual seam - on the atomic-pointer-swap reload pattern (T4-29).** -- **`sample_map` (970 LOC + 708-line header → Q-W2v, T4-13 ≡ T2-07):** resolution core vs the - **`component_state_io`** binary ComponentState codec (+ matching header split) — the codec - grows every envelope bump (v6→v11 in one quarter); the extension stops linking the whole voice - engine to serialize one preset blob. The `core/wire` LE byte-codec template - (`putLE`/`readLE`, T4-20) lands with it. -- **`sampler_core.cpp` (968 LOC + 762-line header — Q-W2v, T4-14/T4-27): the TU stays WHOLE — - the documented hot-path exception to the ~600 ceiling.** Envelope `tick()`s run - per-voice-per-sample; same-TU definition is what lets the compiler inline the stack (no LTO in - the build); a by-class split is the exact heuristic-(3) dispatch blowout. The header splits - into `zone_params.h` + `sampler_core.h`. Recorded here so nobody "fixes" it later. - -## The JSON extraction (Q-2 / Q-W1) - -Extract one pure **`core/json`** (`::json` — minted as **`json::Reader`/`json::Writer`**, Q-8 -SETTLED 2026-07-28): parser (`parseString`/`parseInt`/`parseKey`/ -`skipValue` + escape) + serialize/emit helpers. Rewire `bank_model`, `bank_book`, -`view_mode_model`, `owned_manifest`, **and `tail_control` (T2-02 — the fifth hand-rolled decoder -the §2 audit undercounted)** onto it and **delete all five hand-rolled decoders**. -Round-trip output must be **byte-identical** to before — this is a structural dedupe, not a format -change. Off all hot paths (serialization runs at save/load, never per frame) — safe to abstract -freely. **W1 siblings (from the Q-W0 sign-off):** the shared `readFileBytes` pure helper -(T2-03); the length-prefixed wire-`Cursor` collapse into one shared wire codec beside -`core/json`, consumed by `provenance` / `assignment_request` / `sample_usage` / -`parseBankGeneration` (T2-01(b) — the structural half; the hardening backport lands in Q-W0); -the **concrete** `ui::Rect` unification + `contains()` + per-role aliases, retiring the -XYWH-vs-LTRB fork — NOT a template (T2-05 ≡ T4-21), with the `clamp01` rider (T4-24) and the -`hitIndex` template only as an opportunistic follow-on (T4-22); the `slot_map` extraction -(T4-05) and optional `view_mode_model` planner split (T4-06) riding files W1 already opens; the -relocation scope grows to the ~20 clean VST pure libs under `core/instrument/` (T4-18). - -## The OCP registration-table (Q-6 / Q-W6) - -Replace the ~350-line hand-written **non-table** action registration blocks (isolated in -`app/main.cpp` after Q-W3) with a **data-driven registration table**, so adding an action edits one -place, not four parallel `Register("command_id"/"gaccel"/"hookcommand")` sites. Unload -mirror-unregisters from the same table. Command-id + display strings stay **byte-identical** -(FOREVER-STABLE, per-channel — the Phase V V4 contract). Also split residual fat headers -(`capture.h`/`persist.h`) alongside their TU splits (I). - -## The naming dimension (Q-7 / Q-8 / Q-9 — grep-verified audit in `docs/product/code-organization.md` §2b) - -Alongside giving symbols a directory + namespace *home* (Q-3/Q-4), Phase Q gives -inconsistently/poorly-named symbols a consistent *name* — same Vital bar, orthogonal to the SOLID -focus. The audit (2026-07-27) is grep-verified; the load-bearing findings: - -- **Already consistent — preserve verbatim:** the geometry-mirror verb vocabulary - (`computeRects`/`compute` + `hitTest`, verified across `bank_grid` / - `mode_switch` / `action_buttons` / `action_bar` / `tab_strip` / `prune_button` / `overflow_menu` / - `footer_bar` / `card_drag` / `component_geometry`) and the uniform `_tests` CTest suffix. -- **Collisions — resolved by the Q-4 sub-namespaces for free:** four `class Parser` - (`bank_model.cpp` / `bank_book.cpp` / `owned_manifest.cpp` / `view_mode_model.cpp`) collapse to - one `json::Parser` in Q-W1; the shared pure-UI rect types `FooterRect` / `ButtonRect` (defined in - `prune_button.h`, reused by `footer_bar.h` under an explicit hand-collision "NAME NOTE") get one - `ui::` owner; `Sample` (`model::`) vs `AudioSample` (`audio::`) de-collide by home. -- **Genuine renames (Q-8/Q-9 — SETTLED, Daniel 2026-07-28: all land):** `BankIndex`→`BankModel` (the `bank_model.h` - file↔class word-mismatch — the worst legibility wart, rec: rename the class so the model family - reads `BankModel`/`BankBook`/`ViewModeModel`); the unified JSON parser named `json::Reader`/ - `json::Writer` at W1 mint; align `capture_realtime`(shell)/`realtime_record`(pure) to the house - shell↔core convention (`drag_out`↔`drag_out_win` is the model) during Q-W3. -- **Sequencing (Q-7):** renames ride the wave that already relocates/splits the file — **no - dedicated naming wave.** Rule: *no rename lands on a file the wave isn't otherwise touching.* W1 - carries the collision + model-class renames; W2 the `panel_*` names; W3 the realtime word-order - fix. Zero-behavior-change like the rest of Phase Q; verified by the module's own test executable. -- **Out of scope (never renamed):** the FOREVER-STABLE contract strings are not C++ symbols — - `command_id` strings, action display names, ext-state namespace (`"reasampler"`/`"reasampler_beta"`) - and keys, the `reasampler:` lane prefix, the VST3 class UID. Renaming a C++ class is orthogonal - to these literals (audit §2b.5). - -## Performance guardrails (HARD CONSTRAINT — Daniel's non-negotiable) - -The reorg must cost **zero runtime.** The two hot paths must keep their exact call/inline shape; -the following are acceptance criteria on every point: - -- **`peaks` envelope compute** (`computeEnvelope` / `lastFrameAboveThreshold` over full PCM): **NO - virtual dispatch, NO `peaks` interface, NO added header→TU indirection.** `computeEnvelope` - stays a **free function on `const std::vector&`** so it inlines as today. Relocate + - namespace only; never wrap in an abstraction. -- **Audition / preview**: `panel_audition` may be its own TU (Q-W2), but the call stays a **direct - call-through, not virtual.** -- **Realtime-capture tick**: keep the idle fast-path a **single pointer test**; - `realtime_lifecycle` (Q-W3) must not change the tick's branch shape. -- **JSON extraction** is **off all hot paths** — safe to abstract freely (why Q-W1 is the safe - opener). -- **`FxBypassGuard`** runs per-capture, not per-frame — keep it **stack RAII** when it moves out - of `main.cpp` (Q-W3); never heap-allocate or virtualize it. - -**Net:** every recommended split falls on a cold path or preserves call/inline shape on the two -hot ones. *A split that would add a hot-path indirection is out of scope — rework it or drop it.* - -## Structural heuristics (Daniel, 2026-07-28 — acceptance criteria phase-wide) - -Three heuristics postdate the original framing and bind every wave. They **generalize** the -three-hot-path performance guardrail above — they do not replace it: - -1. **More directories is a must; more files is good; ~600-line file ceiling.** SRP applies to - namespaces, encapsulation, and file organization alike. The ceiling is the *bar*, the audit's - named seams are the *method*: a file landing over ~600 needs a responsibility seam, not an - arbitrary bisection (bisection-to-hit-the-number is rejected). A documented hot-path - exception (`sampler_core.cpp`, T4-14/T4-27) is legitimate; silent overshoot is not. -2. **Templates are the right tool for compile-time dedup — use them where earned.** The LE - byte-codec `putLE`/`readLE` (T4-20) is earned: compile-time dispatch, zero runtime cost, off - the hot paths. The rect family is NOT (T4-21): the types differ in name only, so one - **concrete** `ui::Rect` — a template there would model nothing. -3. **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts *anywhere* — not just - the three named hot paths; prefer static polymorphism where the types are compile-time-known. - The T4-27 warning is the canonical case: a by-class `sampler_core` split would put virtual - envelope `tick()`s on the per-voice-per-sample path — exactly the blowout this forbids. - -## The GATE (load-bearing — Phase Q is last) - -Phase Q is **gated on the tree being otherwise quiescent.** Daniel's plain readiness target: -**"when Phase S and L3 are finished."** As of 2026-07-27 the outstanding work is precisely -**(1) Phase S** merged to dev (the large second-artifact branch, currently on the phase-s worktree) -and **(2) Phase L L3** merged to dev (the VST restyle, itself gated on Phase S). **L1/L2/L4/L5/L6/L7 -have already landed** (see `COMPLETED.md`) — the earlier "L2 + L3" wording was stale and is -corrected to **L3 only**. **D2** is functionally complete (D2-W1..W3-B landed; its lone open item, -a per-track lane-split panel indicator, is *explicitly deferred*, not a blocking residual). **M9** -(slots) is **abandoned** (Daniel, 2026-07-27) — will not be built. D2 is named here only so that -*reactivating* its deferred panel indicator re-arms the quiescence condition; neither D2 nor M9 -blocks the gate today. -*Why:* Phase Q touches **nearly every file in `src/`** (relocate, re-namespace, split the four -largest TUs, plus the §2b renames); every large in-flight branch (Phase S on its worktree, L3 once -it lands) is diffed against the *current flat layout*, so landing a rename-and-relocate-everything -reorg mid-flight forces every open branch through the worst conflict class — a combinatorial -re-resolution, not a linear one. Phase Q is *last* precisely because it reshapes the ground every -other pillar stands on: landing it early taxes every subsequent phase; landing it last taxes -nothing. Re-confirm quiescence against dev before Q-W1. - -> **M9 disposition — resolved (Daniel, 2026-07-27): abandoned.** M9 is out; it will not be -> reactivated. The gate remains satisfied; no re-arm condition applies. - -## Wave sequencing (each independently landable, CTest-green at every step) - -Big-bang is rejected — the CMake per-module static-lib + per-module test-executable seams make a -file move + namespace change **mechanically verifiable** (`ctest --test-dir build` green or not, at -every commit), a property only an *incremental* reorg uses. Risk-ordered: - -- **Q-W0** — **pre-restructure functional + DSP quality audit** (entry point; see §"The - pre-restructure audit wave (Q-W0)"). Produces a written, triaged findings report; runs **first** - and **gates Q-W1** — no structural point begins until the triage closes and Daniel signs off on - every disposition. Fix-now findings are remediated here or folded into the wave that opens the - file; the report may add/reshape downstream Q-W1..Q-W6 points before they start. **COMPLETE - and signed off 2026-07-28; the sub-gate closes when the six fix-now remediations land - (`pq-w0-fixes`).** -- **Q-W1** — safe opener: `core/json` extract (delete the 5 hand-rolled decoders — T2-02 adds - `tail_control`) + impose the directory/ - namespace layout on the 30 clean pure libs + clean shells (pure relocation, no logic change). - All later waves assume this layout. **Carries the naming collision fixes + the model-class - renames (Q-8), which are free during this relocation.** **Scope grown by Q-W0 (2026-07-28):** - + `readFileBytes` (T2-03), the wire-`Cursor` codec (T2-01(b)), the concrete `ui::Rect` - unification (T2-05 ≡ T4-21) + `clamp01` (T4-24), the `slot_map`/planner riders (T4-05/T4-06), - and the ~20 clean VST pure libs under `core/instrument/` (T4-18). -- **Q-W2..Q-W5 (+ Q-W2v)** — the god-module splits, risk-ordered (`bank_panel` → - `main.cpp` → `actions.cpp` → `persist.cpp`), plus **Q-W2v** (NEW, added at the Q-W0 sign-off: - the VST god-modules — editor eight TUs / processor three TUs / `component_state_io`; - `sampler_core` TU whole, the documented exception), which **runs parallel with Q-W2** - (different artifact, zero file overlap — audit §4f SETTLED). Q-W4 depends on Q-W2 - (`panel_bank_ops` dedupe target); Q-W5 best after Q-W4 (`prune_action` → `prune_fs` routing); - otherwise parallel-safe. - **Q-W2 carries the `panel_*` names; Q-W3 carries the `capture_realtime`/`realtime_record` - word-order fix (Q-9) + the settled riders (`ICaptureBackend` deletion + doc correction, - `wav_codec`, `stampCaptureSample` dedupe, T1-11, `capture_realtime_finalize`).** -- **Q-W6** — OCP registration-table + residual fat-header (I) splits. Depends on Q-W3 (registration - code isolated first). Sequenced last; most droppable if narrowing. -- **Naming (Q-7): no dedicated wave** — every rename rides the wave already relocating/splitting - its file; a rename that would touch an otherwise-untouched file is deferred. - -## Precision / invariant implications (what Phase Q does NOT change) - -- **The pure/shell split is strengthened, never dissolved.** Top-level `core/` vs `shell/` makes - it *structural*; no file crosses the boundary; no `core/` file gains a REAPER type; the CMake - per-module test executables stay green. -- **Every precision invariant holds.** Null test, bit-identical repeats, non-destructive capture, - exact bounds — none is code Phase Q rewrites. `FxBypassGuard` moves but stays stack RAII with - identical behavior. -- **Capture ≠ placement.** No hoisted `Run*` / `capture_orchestrator` path may gain an - `InsertMedia` call during the split. -- **Single file-deletion authority is concentrated, never spread.** `prune_fs` (Q-W5) is the one - module that deletes bytes; the reorg improves this invariant. -- **Relative-paths-only** in the persisted index — untouched. -- **On-the-wire/on-disk contract strings unchanged.** Command-id strings, action display names, - ext-state namespaces, VST3 class UID are byte-identical (per-channel, per Phase V V4). - Re-namespacing C++ symbols is orthogonal to these. -- **No behavior change.** Phase Q is a pure structural refactor; the test suite passing unchanged - is the proof of correctness. A point that changes observable behavior has exceeded its charter. - -## Non-goals / guardrails - -- **No feature, no behavior change.** If a point ships anything user-visible, it is out of scope. -- **No hot-path indirection.** No virtual dispatch or header→TU indirection on `peaks`/audition/ - realtime-tick — ever (a hard acceptance bar, not advice). -- **No design-level (D-letter SOLID) rework.** `main`/`bank_panel` depending on concrete capture - backends is a low-priority Dependency-Inversion concern — **out of scope** (a design change, not - a reorg). Phase Q reorganizes; it does not re-architect interfaces. (Deleting the *dead* - `ICaptureBackend` abstraction in Q-W3 is the opposite move — removing a false interface with - one deriver and zero polymorphic call sites, T4-26 — and is in scope.) -- **No `peaks` data-ownership change.** `peaks` forcing a whole-file `std::vector` copy on - the thumbnail path is noted but **not touched** — reworking it risks the hot path. -- **No big-bang commit.** Every wave is independently landable and CTest-green; reject a change set - that cannot be verified at each step. -- **Do not begin before the GATE.** Re-confirm the tree is quiescent (Phase S + L + D2 merged/closed; - M9 abandoned) before any Q point. **And do not begin any structural point (Q-W1+) before the Q-W0 - sub-gate:** the audit's triage is complete and Daniel has signed off on every finding's disposition. - (Sign-off given 2026-07-28; the sub-gate now closes when the six fix-now remediations land on - `pq-w0-fixes`.) -- **Verify** the CMake `src/` path updates and the SWELL/LICE surfaces still resolve after - relocation, as the existing build already requires. diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 1f23b50..0000000 --- a/PLAN.md +++ /dev/null @@ -1,619 +0,0 @@ -# PLAN.md — ReaSampler milestone roadmap - -Living milestone roadmap for ReaSampler. Derived from CONTEXT.md's 11-step build -order; CONTEXT.md remains the authoritative spec — this file is the tickable -checklist, not a re-statement of the spec. When a point lands, doc-keeper removes -it here and appends it to `COMPLETED.md`. - -**Conventions** -- One checkbox `- [ ]` = one discrete, independently-landable point. -- Each milestone opens with a **Goal** (one line) and a **Verify** criterion - (the acceptance gate; precision invariants pulled in where one applies). -- Verify-in-DAW points require a manual REAPER run; pure points are gated by CTest. -- "See CONTEXT.md §…" points at the authoritative detail — do not duplicate it here. - ---- - -## Open questions to resolve during build -Carried from CONTEXT.md §Open questions — keep visible until each is closed by a -landed milestone. - -- **`parseInt` narrowing hardening:** `src/bank_model.cpp` `parseInt` casts - `int64_t → int` via `static_cast` without a range check; integers that fit - in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate - — add bounds check before the cast when integer-field validation is in scope. -- **Capture send/routing isolation (TODO):** The FX-scope capture neutralizes out-of-scope FX, gain, and pan — but NOT aux **sends**. So a downstream coloring send (e.g. a folder → reverb-track send) still routes and blends the reverb into an item/track capture, past the intended isolation point. A true item-level capture should be taken at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan **and** before out-of-scope aux/parallel sends. The hard part: distinguish **source routing that must be preserved** (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from **coloring sends that must be excluded** (folder→reverb). Repro: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded. Likely approach: snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path — needs a rule for which sends are load-bearing. - ---- - -# Phase D2 — Two-canvas (item-level mode projection; additive to D1) - -> **Phase D2 is functionally complete** — D2-W1, D2-W2, D2-W3-A, D2-W3-B all landed; see `COMPLETED.md`. -> -> **Deferred:** panel UI indicator for per-track lane/mode state (a per-track -> lane-split marker). The mode switch already shows the active mode; no natural -> cheap home for a per-track indicator was found in the bank panel. Explicitly -> deferred — not silently dropped. Can be picked up later if wanted. - ---- - -# Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact) - -> **Landed on dev (merged 2026-07-27); DAW verification pending Daniel's smoke test.** -> S1–S18 and the product-name/binary-rename work are all on dev. The cross-artifact -> ingest relay (S13 bullet) was explicitly DEGRADED and remains deferred — see below. -> Completed material archived in `COMPLETED.md`. Authoritative spec: **CONTEXT.md -> §MIDI-playback instrument — additive phase spec (Phase S)**. Product framing: -> `docs/product/midi-playback.md`. -> -## S13 — cross-artifact ingest relay (deferred) - -> **SPIKE VERDICT (ps-w12, 2026-07-27): DEGRADED — relay deferred.** The instrument's -> REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; a relay would need a new -> instrument WRITE seam into ext-state and an extension-side timer poller servicing a -> drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race -> the S17 spec rejected for alternative (A). Both the read-only-instrument boundary and -> the new poller are load-bearing design calls, so the relay is deferred to a future wave. -> The shipped ingest gesture stays drop-onto-docked-panel (S8). The degrade path (editor -> shows a "drop files onto the ReaSampler bank panel to add them" affordance) landed as -> part of Phase S. See `COMPLETED.md` §S13 for full context. - -- [ ] Cross-artifact ingest relay: the editor hands the dropped path + this instance's - identity to the extension as a bank-ingest request over an agreed seam. DEFERRED — - relay mechanism proved load-bearing to redesign; drop-onto-panel (S8) is the shipped - ingest path. Requires (a) a new instrument WRITE seam into ext-state and (b) an - extension-side timer poller + claim/clear nonce. A future wave when the design is ready. - -## Phase S — product name (ReaSampler 9000) -The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26, -on DAW-testing the S1–S6 instrument). The extension remains **ReaSampler**; the instrument -is **ReaSampler 9000**. Framing + propagation surfaces: -`docs/product/midi-playback.md` §Product name. - -- [ ] **Compat verification (must-DAW-verify before shipping the rename):** the working - assumption is that REAPER **rebinds a saved instance by its VST3 class UID, not by the - module filename**, so a filename rename with an unchanged UID keeps saved projects working - (existing instances still resolve). **This is not yet confirmed from source** — a web - check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's binding is more nuanced - than "UID only" (it can involve an FXID match), so treat UID-rebind as **to-verify, not - asserted fact**. **DAW-verify:** save a project with a ReaSampler 9000 instance under the - old filename, rename the module, reopen — confirm the instance rebinds and restores its - state. If REAPER does key partly on filename, fall back to keeping the current filename - (display-strings-only) and record that as the shipped choice. - -## Phase S — held and optional-forever (noted, not specified) - -- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun), - full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once - Tier 0–1 proves the instrument belongs — **its points are not drawn up here.** -- **Tier 3 — "instrument polish" (optional-forever).** Filters, filter/pitch - envelopes, LFOs, per-voice pan, choke groups, a modest FX slot. A direction to leave - room for, never a commitment. **Note:** S16 lands the *pitch* envelope + the Varispeed/ - Preserve pitch-engine mode early (Daniel's directive) — the Tier-3 "filter/pitch envelopes" - line now means the *filter* envelope + LFOs remainder. -- **Sinc Varispeed-quality upgrade (HELD — WDL_Resampler).** `WDL_Resampler`'s sinc mode - beats the core's 2-point linear interp for **Varispeed** base-repitch quality (see the S16 - WDL finding). An optional per-voice quality toggle (linear default / sinc), RT-suitable but - heavier. Held as a Tier-2/3 quality option — not needed for S15/S16, not scheduled. (A - resampler couples duration, so it is a Varispeed-quality option only, **not** a Preserve - engine.) -- **WDL_SimplePitchShifter swap (HELD — fork S16-F2 route a).** `WDL_SimplePitchShifter` - as a drop-in swap for the `pitch_shift` pure module if the hand-rolled OLA onset latency - or warble proves musically unacceptable. Same `PitchEngine::Preserve` contract behind the - seam. WDL excluded from the shipped build by include-chain (windows.h); held as the - quality/latency alternative. -- **Trigger choke-on-note-off (HELD — fork S15-F1).** A future option for Trigger mode to - *cut* (choke) on note-off or on a same-group re-trigger (hi-hat open/closed). Deliberately - out of S15 scope (Trigger ignores note-off entirely there); a Tier-3 choke-group direction. - -## Phase S — editor view-model redesign (three views: Sample / Browse / Zone) - -> **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Re-partitions the ReaSampler -> 9000 editor from a two-view toggle into a **three-view model where the loaded sample is the -> home** — Sample (default face), Browse (modal picker over Sample), Zone (dedicated keymap -> surface). Adds three performance parameters (key-tracking, preview velocity, and the r10 -> velocity→amp transfer curve) and three visual components (envelope overlay, real piano-key -> pattern, and the r10 velocity-curve editor), and frames two engineering prerequisites -> (drop-to-FX bug, default window size). An **editor** redesign — the S3 voice engine, keymap -> resolution, and read-only-over-bank contract are **unchanged**; component state extends -> additively for key-tracking; VST3 class UID unchanged. Authoritative spec: **CONTEXT.md §Phase -> S — editor view-model redesign (S-VIEW)**. Product framing: `docs/product/midi-playback.md` -> §Addendum r9. When a point lands, doc-keeper moves it to `COMPLETED.md`. -> -> **Depends on Phase S being on dev** (this redesigns Phase S editor shells — -> `reasampler_editor.cpp` + the pure `src/vst/` geometry modules). **Phase L L3 is FINISHED and -> merged** (2026-07-27, commit `c53683e` — `reasampler_editor.cpp` + `reasampler_embed.cpp` -> restyled through the L1 kit). S-VIEW builds the new three-view layout **directly on the current -> L3 look-and-feel as its baseline — one implementation pass, styled correctly from the start.** -> There is **no two-pass "land S-VIEW then restyle through L3"** — L3 already happened; every new -> S-VIEW surface (Sample face, Browse modal, Zone surface, envelope overlay + node handles, -> piano-key strip, preview cluster) is drawn through the L1 kit at build time, inheriting the L3 -> palette and component-draw grammar. Restyle-after is not a concern. - -**Goal:** A three-view editor that makes "pick a capture, tune it, play it" fast, easy, and fun — -Sample is home, Browse is an easy-to-summon modal picker, Zone is the deliberate keymap surface. -**Verify (in DAW):** open on a 1080p screen → full Sample face (hero waveform + envelope overlay + -fenced root + preview-trigger + control strip) with no scroll; Browse opens as a modal over -Sample, select+confirm loads a new capture; Zone opens on its own button, key-tracking + the -piano-key pattern work; drop-a-capture-onto-FX instantiates a playing instance. - -### Phase S editor Wave B — Sample-face recomposition (r11; Daniel, 2026-07-27) - -> Daniel's post-landing DAW pass on the S-VIEW editor: **all linear sliders → small radial -> knobs** grouped into a fenced knob deck (envelope controls grouped as a unit); the **mode -> toggles compact**, not full-width; the **inline velocity-curve box → a miniature curve -> preview button + full-size popup editor** (right-click deletes a node in the popup); the -> **hero waveform full-width**. Authoritative layout spec: **CONTEXT.md §S-VIEW → "The -> Sample-face recomposition (r11)"** (band order, group taxonomy, cell metrics, popup -> geometry, inventory contract — nothing silently dropped). Product framing: -> `docs/product/midi-playback.md` §Addendum r11. All drawing through the L1 kit; all -> layout/hit-test in pure geometry modules. -> -> **S-VIEW-11, S-VIEW-12, and S-VIEW-13 have all landed** — FB1 (merged 2026-07-27, suite -> 55/55) brought the knob deck + master gain + curve popup + full-width hero; FB2 (merged -> 2026-07-28, suite 55/55) brought Zone-panel parity. **Phase S editor Wave B (r11) is -> complete.** See `COMPLETED.md` for both FB entries. - -**Open forks** — **R11-F1** (hero height vs. default window) SETTLED at FB1 build: elastic -hero, 840×620 default kept. **R11-F2** (Zone-panel parity) SETTLED at FB2 build: knob deck -+ curve popup adopted on the Zone panel; `param_slider` slider rows retired on that surface. - -### Phase S editor redesign — forks (Daniel's to call) -*(r9/r10 forks all settled: S-VIEW-F1 and S-VIEW-F2 SETTLED 2026-07-27 — folded into S-VIEW-4 / -S-VIEW-3. F1: preview velocity persists via envelope-v6 `ComponentState`. F2: envelope nodes are -draggable via the pure `envelope_edit` module. R10-F1 SETTLED 2026-07-27 — Option A flat y=1, -folded into S-VIEW-9. S-VIEW-F3 SETTLED — full-window overlay, implemented as Browse renders as a -full-window modal over Sample (landed in S-VIEW-1/S-VIEW-5). **r11 forks all settled: R11-F1 -SETTLED at FB1 build (elastic hero, 840×620 default kept); R11-F2 SETTLED at FB2 build (Zone -panel adopts knob deck + curve popup, `param_slider` slider rows retired on that surface).**)* - ---- - -# Phase Q — Quality (structural reorganization; zero-runtime-cost) - -> **New pillar, own lettered namespace, and — uniquely — the LAST structural pillar.** Phase -> Q is a **pure structural refactor**: it reorganizes `src/` into a healthier shape (more -> encapsulation, granular namespaces, `core/`/`shell/`/`app/` subdirectories) against a stated -> quality bar — *"mtytel Vital is my code reference for quality"* — to bring the codebase -> "into the realm of something I can stand to look at." It ships **no feature and changes no -> behavior**: the test suite passing unchanged is the proof of correctness. Namespaced **`Q` -> (Quality)** — M/D/B/R/V/S/L are all taken; `Q` names the *end* (the quality bar), the reorg -> being the *means*. Authoritative spec: **CONTEXT.md §Phase Q — structural reorganization -> (reorg spec)**. Product framing, the Vital-grounded target shape, the grep-verified SOLID -> audit that is the evidence base, and the settled/recommended fork record (Q-1..Q-6): -> `docs/product/code-organization.md`. When a point lands, doc-keeper moves it to -> `COMPLETED.md`. -> -> **THE GATE (load-bearing — state first; reconciled to reality 2026-07-27).** Phase Q is -> **gated on the tree being otherwise quiescent.** Daniel's plain readiness target: **"when -> Phase S and L3 are finished."** **As of 2026-07-27 both gate conditions are satisfied:** -> Phase S merged to dev (2026-07-27); Phase L L3 (the VST restyle) merged to dev -> (2026-07-27, commit `c53683e`) — **Phase L is complete** (L1/L2/L3/L4/L5/L6/L7 all -> landed, see `COMPLETED.md`). **D2** is functionally complete (D2-W1..W3-B landed; the -> lone open item, a per-track lane-split panel indicator, is *explicitly deferred*, not a -> blocking residual). **M9** (slots) is **abandoned** (Daniel, 2026-07-27) — will not be -> built. D2 is named in the gate only so that *reactivating* its deferred panel indicator -> re-arms the quiescence condition; neither D2 nor M9 blocks the gate today. *Why the gate:* Phase Q -> touches **nearly every file in `src/`** (relocate into subdirectories, re-namespace every -> header, split the four largest TUs, plus the §2b renames). Every large in-flight branch -> (Phase S on its worktree, and L3 once it lands) is diffed against the *current flat -> layout*; landing a rename-and-relocate-everything reorg mid-flight forces every open -> branch through the worst conflict class (every hunk moved, every qualified reference -> changed) — a combinatorial re-resolution, not a linear one. Phase Q is *last* precisely -> because it reshapes the ground every other pillar stands on. Landing it early taxes every -> subsequent phase; landing it last taxes nothing. **The gate is now satisfied — Phase Q -> may begin, entering at the Q-W0 audit wave** (the structural waves Q-W1+ do not begin until -> Q-W0's triage closes and Daniel signs off — see the Q-W0 sub-gate above). -> -> **M9 disposition — resolved (Daniel, 2026-07-27): abandoned.** M9 is out; it will not be -> reactivated. The gate remains satisfied; no re-arm condition applies. -> -> **PRE-RESTRUCTURE AUDIT WAVE (Q-W0 — added 2026-07-27, Daniel's ask; runs FIRST).** Before any -> structural point (Q-W1+) begins, Phase Q now opens with a **functional + DSP quality audit** -> (`Q-W0`) — a thorough static analysis of the code from a *functional-correctness* and -> *algorithm-quality* point of view, complementary to (not a repeat of) the grep-verified -> SOLID/naming audit that already grounds Q-W1..Q-W6. Q-W0 hunts DSP smells (close eye on the pitch -> engine), reinvented wheels, duplicate code, poor-quality algorithms, leaky pure/shell boundaries, -> and domain-modeling smells around env-coupled constants; it produces a written, triaged findings -> report. **The gate to begin Q-W1 is that Q-W0's triage is complete and Daniel has signed off on -> each finding's disposition** (fix-now vs. document-and-defer). Q-W0's findings may add or reshape -> downstream Q-W1..Q-W6 points; fixes that Q-W0 classifies fix-now are remediated in Q-W0 (or folded -> into the wave that already touches the file), **not** deferred silently into the structural waves. -> -> **Q-W0 SIGN-OFF: COMPLETE (Daniel, 2026-07-28).** The audit ran as four parallel tracks (T1 DSP, -> T2 architecture, T3 env-coupled constants, T4 sizing/placement — **59 findings**; report -> `docs/product/code-quality-audit.md`, appendices `docs/product/audit-notes/`), and **all 59 -> findings' dispositions are approved as proposed.** The Q-W1 sub-gate is satisfied **once the six -> approved fix-now remediations land** (in flight on branch `pq-w0-fixes`, Q-W0-scoped): T1-01, -> T1-03, T1-09, T2-01(a), T3-01, T3-03. The audit's §3 plan reshape is **folded into the waves -> below** (Q-W2 6→8 seams; NEW wave **Q-W2v** parallel with Q-W2; Q-W3 3→4 hoists + riders; Q-W5 -> + the ext-state-loop dedupe), and its §4 decision list is settled — see the settlement block -> below. The Q-11 question is answered by the audit: the SOLA pitch engine is **sound — no -> technique replacement warranted**; every pitch finding is a bounded in-technique fix or a -> documented operating limit. -> -> **Settled (Q-1, this-doc):** the phase is **`Q` (Quality)**; point-id family `Q1..Qn`, wave -> prefixes `Q-W0` (the pre-restructure audit) then `Q-W1..Q-W6` (the structural reorg; **+ -> `Q-W2v`**, the VST god-module wave added at the Q-W0 sign-off, 2026-07-28). -> **Settled (Q-10/Q-11, Daniel 2026-07-27):** Q-10 audit-report home = a **committed doc** -> (`docs/product/code-quality-audit.md`, not a tracked issue list); Q-11 pitch-remediation depth = -> **defer to findings** (default document-and-defer; weigh a bounded OLA fix before a technique -> replacement; a technique replacement is a Daniel decision at triage time, not an automatic Q-W0 -> action). -> **Recommended, Daniel's to call (Q-2..Q-9, see -> `docs/product/code-organization.md` §6):** Q-2 JSON extraction in scope + first (rec: yes); -> Q-3 directory shape `core/`/`shell/`/`app/` top-split with subsystem dirs beneath (rec: this -> over pure-Vital subsystem-first — it makes the pure/shell invariant *structural*); Q-4 -> sub-namespace to match sub-directory (rec: both); Q-5 split god-modules to the audit's named -> seams, no finer (rec: yes); Q-6 OCP registration-table as the final wave (rec: in, last); -> **Q-7 naming rides the relocation waves, no dedicated naming wave (rec: yes — forced once -> Q-3/Q-4 settle); Q-8 class/module renames — fix the two that actively mislead (`BankIndex`→ -> `BankModel`; the JSON `Parser`→`json::Reader`/`Writer`), leave the merely-quirky (rec); -> Q-9 align the `capture_realtime`/`realtime_record` shell↔core word order during W3 (rec: yes).** -> -> **SETTLED (Daniel, 2026-07-28 — with the Q-W0 sign-off; the REC record above kept as history):** -> **Q-5 SETTLED** — split to **seams-by-responsibility with the T4 seam lists adopted** -> (`bank_panel` 6→8 seams adding `panel_layout` + `panel_drag`, T4-01; `capture_orchestrator` -> further split with `capture_batch`, T4-02), and the **~600-line file ceiling is an acceptance -> criterion on every split wave** — seams are the method, the ceiling is the bar; arbitrary -> bisection to hit the number is rejected. **Q-6 SETTLED: in scope, last wave, as planned.** -> **Q-8 SETTLED: both renames** — `BankIndex`→`BankModel` (W1) and the JSON parser minted as -> `json::Reader`/`json::Writer` (W1); additionally from the audit, **`ICaptureBackend` is deleted -> in Q-W3** (T4-26 — one deriver, zero polymorphic call sites; the CLAUDE.md/CONTEXT "two -> backends behind one interface" correction **rides Q-W3 itself**, recorded as a rider — the docs -> are not edited before that wave). **Q-9 SETTLED: yes** — align to stem `capture_realtime`, -> shell suffixed, during W3. **VST placement (audit §4a) SETTLED: T4-18** — `src/vst/` integrates -> into the single `core/`/`shell/` top split as `core/instrument/{engine,map,ui}` + -> `shell/instrument/` (Q-3 directory map updated in CONTEXT.md §Phase Q). **WAV/RIFF -> consolidation (audit §4e) SETTLED:** a named rider on **Q-W3** — one pure **`wav_codec`** owner -> (walker + layout + build + patch), absorbing the T4-10 ingest extraction. **Q-W2v scheduling -> (audit §4f) SETTLED: parallel with Q-W2** (different artifact, zero file overlap; the serial -> "Q-W7" alternative set aside). -> -> **HARD CONSTRAINT — performance (see CONTEXT.md §Phase Q, `docs/product/code-organization.md` -> §3).** The reorg must cost **zero runtime.** On the three hot paths — `peaks` envelope -> compute, audition/preview, the realtime-capture tick — **no added virtual dispatch, no -> header→TU indirection, no changed call/inline or branch shape.** `computeEnvelope` stays a -> free function on `const std::vector&`; audition split stays a direct call-through; -> the realtime idle tick stays a single pointer test; `FxBypassGuard` stays stack RAII. This is -> an acceptance criterion on every point: *a split that would add a hot-path indirection is out -> of scope — rework it or drop it.* -> -> **STRUCTURAL HEURISTICS (Daniel, 2026-07-28 — acceptance criteria on every wave; these -> *generalize* the three-hot-path guardrail above, they do not replace it):** -> (1) **More directories is a must, more files is good, ~600-line file ceiling** — SRP applies to -> namespaces, encapsulation, and file organization alike. The ceiling is the *bar*, the audit's -> named seams are the *method*: a file landing over ~600 needs a responsibility seam, not an -> arbitrary bisection; a documented hot-path exception (`sampler_core.cpp`, T4-14/T4-27) is -> legitimate, silent overshoot is not. -> (2) **Templates are the right tool for compile-time dedup — use them where earned** (the LE -> byte codec `putLE`/`readLE`, T4-20), not for name-only unification (the rect family is one -> **concrete** `ui::Rect`, NOT a template — T4-21's ruling). -> (3) **SOLID is great but saved CPU is better** — no dispatch-stack blowouts *anywhere*, not -> just the three named hot paths; prefer static polymorphism where types are compile-time-known -> (T4-27's warning is the canonical case: a by-class `sampler_core` split would put virtual -> envelope `tick()`s on the per-voice-per-sample path). -> -> **NAMING dimension (added 2026-07-27; grep-verified audit in `docs/product/code-organization.md` -> §2b).** Beyond giving symbols a directory + namespace *home* (Q-3/Q-4), Phase Q also gives -> poorly/inconsistently-named symbols a consistent *name*, against the same Vital bar. The audit -> found: four `class Parser` copies collapsing to one `json::Parser` (Q-W1); shared pure-UI rect -> types (`FooterRect`/`ButtonRect`) that the codebase already hand-checks for collision -> (`footer_bar.h`'s "NAME NOTE") — resolved by the Q-4 sub-namespaces for free; the -> `bank_model`/`BankIndex` file↔class word-mismatch (Q-8); and the `capture_realtime`/ -> `realtime_record` shell↔core word-order inversion (Q-9). **Renames ride the wave that already -> relocates/splits the file — no dedicated naming wave (Q-7);** the geometry-mirror -> `compute*`/`hitTest*` verb vocabulary and the `_tests` suffix are already consistent and are -> preserved verbatim. Naming changes are zero-behavior-change like the rest of Phase Q, and the -> FOREVER-STABLE contract strings (command ids, action names, ext-state namespace/keys, VST3 UID) -> are **not** C++ symbols and are never renamed. -> -> **Every point is independently landable and CTest-green at EVERY step.** The CMake -> per-module static-lib + per-module test-executable seams already draw the module boundaries; -> a file move + namespace change is mechanically verifiable — `ctest --test-dir build` is green -> or it isn't. **Green-CTest-at-every-point is an acceptance criterion.** Big-bang is rejected; -> the reorg is risk-ordered waves (W1 safe opener → W2/W2v–W5 god-module splits → W6 OCP finish). -> -> **PHASE STATUS (2026-07-29): all seven waves (Q-W0..Q-W6 incl. Q-W2v) are structurally -> COMPLETE.** Remaining before the phase closes and merges to `dev`: (1) Daniel's in-DAW -> verification batch — the full deferred list across all waves (panel parity, editor/processor -> parity, stereo Preserve listening, null test, bit-identical repeats, capture flows, action -> families, one-op-one-Ctrl-Z, prune fail-safes, save/load/relocation) — now unblocked since the -> tree is stable; (2) the phase-close CLAUDE.md architecture refresh (module map still describes -> some pre-Q homes); (3) the phase-q → dev merge on Daniel's sign-off. See `COMPLETED.md` for -> each wave's full landed narrative. - -## Q-W0 — pre-restructure functional + DSP quality audit (runs FIRST; gates Q-W1) -**STATUS (2026-07-29): audit COMPLETE, triage COMPLETE, sign-off COMPLETE, fix-now -remediations LANDED — Q-W0 is fully closed.** The findings report is committed -(`docs/product/code-quality-audit.md`; track appendices in `docs/product/audit-notes/` — T1 -DSP, T2 architecture, T3 env-constants, T4 sizing/placement; 59 findings). Daniel approved -every disposition 2026-07-28. The six approved fix-now remediations plus seven review riders -landed 2026-07-28 (merge `546927e`) — see `COMPLETED.md`. **Q-W1 has since landed on top of -this closure** (see `COMPLETED.md`). -**Goal:** Before a single structural point moves, perform a **thorough static/functional audit** of -the codebase and produce a **written, triaged findings report**. This is the *functional-correctness -and algorithm-quality* complement to the grep-verified SOLID/naming audit that already grounds -Q-W1..Q-W6 (§2/§2b of `docs/product/code-organization.md`) — it goes deeper on *does the code do the -right thing well*, not *where does the responsibility live*. It hunts, across the whole `src/` tree: -duplicate code, reinvented wheels, poor-quality algorithms (**close eye on the pitch engine**), -numerical-robustness and artifact hazards in the DSP paths, leaky pure/shell boundaries, and -**domain-modeling smells around env-coupled constants** (values stored in a frame/rate/DPI/tick-coupled -domain that should be stored rate-free and resolved at use). Every finding is **classified for -disposition**: *eliminate-before-restructure* (fix-now) vs. *document-and-defer* (with rationale). -Nothing structural (Q-W1+) begins until this wave closes and Daniel signs off on the disposition of -every finding. CONTEXT.md §Phase Q (Q-W0 audit scope + triage). See -`docs/product/code-organization.md` §2c. -**Verify:** A findings report exists and is complete over the named audit surfaces (DSP/audio incl. -pitch; architecture smells; env-coupled-constant domain-modeling smells). **Every finding is -triaged** (fix-now vs. documented-and-deferred, each with a one-line rationale). Fix-now findings are -either remediated in this wave or explicitly assigned to the downstream wave that already touches the -file (recorded per finding); no fix-now finding is left implicit. Any behavior-changing remediation -lands with its module's CTest executable green and (where a DSP path changes audibly) a stated -before/after listening or null check. **The gate to Q-W1 is: triage complete + Daniel signed off.** -**Depends on:** the GATE (tree quiescent). Precedes every other Q wave — Q-W1 depends on Q-W0. - -- [ ] **DSP / audio audit — close eye on pitch.** Assess *algorithm quality* (correctness, - artifacts, numerical robustness, interpolation quality, reinvented-wheel vs. established DSP - technique) across: `src/vst/pitch_shift` (hand-rolled OLA pitch-preserve — window/overlap choice, - phase handling, transient/formant behavior, buffer edges), `sampler_core` (repitch ratio math, - interpolation order/quality, loop-point crossfade, voice-stealing correctness/click behavior), - `peaks` (envelope min/max binning), `wav_trim` (decay-scan threshold + truncate plan), and the - capture/tail paths. **Pitch-remediation depth (Q-11 SETTLED, Daniel 2026-07-27): defer to - findings** — default document-and-defer; only if the audit surfaces artifacts that matter, weigh a - bounded OLA fix (window/overlap/edge tuning) **before** a technique replacement. A technique - replacement (phase-vocoder / WSOLA) reshapes `pitch_shift`, spills a downstream Q-wave point, and is - a **Daniel decision at triage time, not an automatic Q-W0 action** — Q-W0 surfaces the - recommendation, Daniel calls it. -- [ ] **Architecture-smell audit.** Duplicate code, reinvented wheels, poor abstractions, and any - leaky pure/shell boundary (a `core/` module reaching a REAPER/host type, or geometry/algorithm - math living untestable in a shell). Cross-check against — but do not merely restate — the §2/§2b - SOLID/naming findings; Q-W0's contribution is the *functional* smells those audits didn't target. -- [ ] **Env-coupled-constant domain-modeling audit (explicit smell category).** Flag ANY value - stored in a frame/rate/DPI/tick-coupled domain that should be stored **rate-free and resolved at - the point of use** — per the load-bearing invariant that wall-clock times are rate-free SECONDS - resolved against the live project rate (`sample_map`), with NO hardcoded sample rates in `src/`. - Treat this as **domain-modeling** (store rate-free, resolve at use), **not** "rescale by rate." - There was a prior incident here — envelope times stored in the frame domain — so this is a - first-class category, not a footnote. Sweep envelope times, loop points, fade lengths, tail - lengths, and any UI geometry constant that silently assumes a DPI/rate. -- [ ] **Triage + report.** Write the findings report as a **committed doc, - `docs/product/code-quality-audit.md`** (Q-10 SETTLED, Daniel 2026-07-27 — beside the SOLID/naming - audit, not a tracked issue list); classify each finding fix-now vs. document-and-defer with a - one-line rationale; for each fix-now, note whether it is remediated in Q-W0 or assigned to the - downstream wave that already opens the file. Deferred findings carry a documented rationale so they - are a decision, not an omission. -- [ ] **Sign-off gate.** Daniel reviews the triage and signs off on each disposition. Q-W1 does not - begin until this is done; fold any new/reshaped downstream points the audit surfaces into - Q-W1..Q-W6 before starting them. **DONE (Daniel, 2026-07-28): all 59 dispositions approved as - proposed; the §3 plan reshape and §4 decisions are folded into Q-W1..Q-W6 + Q-W2v below.** - -## Q-W1 — safe opener: extract `core/json` + impose the directory/namespace layout on clean modules - -> **Landed on `phase-q` (2026-07-29). Integrated suite 60/60 green.** `core/json` -> (`json::Reader`/`json::Writer`) extracted; the five hand-rolled JSON decoders (incl. -> `tail_control`'s) deleted; the wire-`Cursor` family collapsed into `core/wire`; the shared -> `readFileBytes` helper added; ~50 clean modules relocated into `core/{model,view,capture, -> audio,ui,reclaim,version,json,util,wire}/`, `core/instrument/{engine,map,ui}/`, -> `shell/{capture,panel,view,persist,actions,instrument}/`, `app/main.cpp`; sub-namespaces -> applied; one concrete `ui::Rect` + aliases (LTRB fork + `footer_bar` NAME NOTE retired); -> `slot_map` extracted from `bank_book`; `clamp01` deduped; `BankIndex`→`BankModel`; -> `reasampler_uid.h` relocated to `core/wire/`. See `COMPLETED.md` for the full narrative. -> -> **Skipped/deferred riders:** T4-22 (`hitIndex` hit-test template) — not trivial, deferred as -> an opportunistic follow-on once the rect unification is in use downstream; T4-06 -> (`view_mode_model` planner split) — optional, deferred; T4-09 (`view_lanes` split) — -> deferred (in scope only if a later wave touches `view.cpp` anyway). -> -> **Open residual — `bank_book.cpp` still 737 LOC.** The serialize/deserialize seam is -> identified but blocked on a `nameKey` linkage design decision, escalated to Daniel and -> **pending** as of 2026-07-29. Downstream waves touching `bank_book` should check this -> residual before assuming the split is finished. -> -> An interim `core/namespaces.h` shim covers the six not-yet-split god TUs; each downstream -> split wave (Q-W2 onward) retires its own includes of it as that module splits. - -## Q-W2 — split `bank_panel.cpp` (the biggest extension god-module — 3459 LOC at the Q-W0 census) - -> **Landed on `phase-q` (2026-07-29, merge of `pq-w2-panel`). Integrated suite 61/61 green, -> reviewed-approved.** `bank_panel.cpp` (3459 LOC) split into eight TUs under `shell/panel/`: -> `panel_render` / `panel_thumbnails` / `panel_audition` / `panel_input` / `panel_bank_ops` / -> `panel_window` / `panel_layout` / `panel_drag`, plus per-seam public headers and internal -> `panel_state.h`; audition stays a direct call-through; the one-bank-op-one-undo invariant is -> preserved; ~50 TU-private helpers wrapped in anonymous namespaces (a review follow-up). See -> `COMPLETED.md` for the full narrative. -> -> **Recorded ceiling overages (reviewer-endorsed, preserved as a durable record per CONTEXT's -> "silent overshoot is not legitimate" rule):** `panel_input.cpp` 636, `panel_render.cpp` 613, -> `panel_state.h` 608 — the overage is comment volume; non-comment lines are ~322–369 per file; -> no honest seam remains; bisection was rejected. -> -> **Review note for the Q-W4 planning record:** `panel_bank_ops`'s verbs still embed -> prompts/panel-state nudges — Q-W4's dedupe needs promptless inner verbs (`renameBank(id,name)` -> etc.), not a call-site swap; `promptText`/`mintBankId` are byte-identical twins with -> `actions.cpp` and are the cheapest first dedupe. -> -> **In-DAW verification (panel parity) is PENDING on `phase-q`** — deferred by design, not yet -> performed. - -## Q-W2v — split the VST god-modules (NEW wave — Q-W0 T4 §1.5; runs parallel with Q-W2) - -> **Landed on `phase-q` (2026-07-29, merge of `pq-w2v-vst`). Integrated suite 61/61 green, -> reviewed-approved.** `reasampler_editor.cpp` (3084 LOC, the largest file in the repo) split -> into eight face-axis TUs under `shell/instrument/`, with pure layout hoisted into -> `core/instrument/ui/editor_geometry` (discharges T2-06, newly tested); `reasampler_processor.cpp` -> split into `processor_state` / `processor_reload` / lifecycle+`process()` kept whole (no -> virtual seam, T4-29); `sample_map` split into a resolution core + `component_state_io` codec -> (the extension preset path no longer links the voice engine — link-proven; T4-13 ≡ T2-07); -> `sampler_core.cpp` stays whole with the documented hot-path exception comment (T4-14/T4-27); -> `zone_params.h` split out; `core/wire/bytes.h` (`putLE`/`ByteReader`) lands (T4-20); -> `ThumbnailKey` adopted (T2-10); a golden full-blob v11 fixture pins the codec bytes. The -> `src/vst/` directory is gone. See `COMPLETED.md` for the full narrative. -> -> **Deferred/known:** `component_state_io.h` still includes `sample_map.h`→`sampler_core.h` -> transitively (T2-07's header half — future work); the `engine` namespace is deferred -> (`sampler_core` stays flat `reasampler`); capture-side LE rewires are left for the capture -> family. -> -> **In-DAW verification (editor/processor parity) is PENDING on `phase-q`** — deferred by -> design, not yet performed. - -## Q-W3 — split `main.cpp` (hoist orchestration; leave main = pointers + entry + dispatch) - -> **Landed on `phase-q` (2026-07-29, merge of `pq-w3-main`). Integrated suite 61/61 green, -> reviewed-approved.** `app/main.cpp` reduced 1897 → 653 LOC (pointers + entry + dispatch; the -> remaining bulk is the registration residue Q-W6 dissolves) via four hoists into -> `shell/capture/`: `capture_orchestrator`, `capture_batch`, `scope_resolve`, -> `realtime_lifecycle`; `FxBypassGuard` moved intact as a stack RAII object; the realtime idle -> tick stays a single pointer test; `ICaptureBackend` deleted (T4-26) with the -> CLAUDE.md/CONTEXT-ARCHIVE corrections landed in the same commit; the Q-9 rename done (pure -> `core/capture/capture_realtime`, shell `capture_realtime_shell` + `capture_realtime_finalize` -> split, T4-08); `stampCaptureSample` dedupe (T2-09, divergent time-sig behavior preserved via -> caller arg); `makeUniqueTag` gains a per-session monotonic counter (T1-11 behavior fix — stems -> now `-` / `rt--`; the per-process residual is documented in-code); one pure -> `wav_codec` RIFF owner absorbs `wav_trim` + `ingest`'s WAV build + content hashes, with golden -> hash literals pinned (`wav_codec_tests` replaces `wav_trim_tests`; `capture_realtime_tests` -> replaces `realtime_record_tests`). See `COMPLETED.md` for the full narrative. -> -> **Known open:** `wav_trim.h`'s transitional forwarding shim still has three live includers -> (`sample_map.h`, `editor_session.cpp`, `processor_reload.cpp`) — repoint-and-retire is a named -> follow-up; `ingest.cpp` is trimmed to 567 LOC but keeps the `namespaces.h` shim (`ingest` + -> `view` remain the shim's unowned consumers). -> -> **In-DAW verification (null test, bit-identical repeats) is PENDING on `phase-q`** — deferred -> by design, not yet performed. - -## Q-W4 — split `actions.cpp` + dedupe bank verbs against `panel_bank_ops` - -> **Landed on `phase-q` (2026-07-29, merge of `pq-w4-actions`). Integrated suite 61/61 green, -> reviewed-approved.** `actions.cpp` (1019 LOC) split into `design_view_actions` / `bank_actions` -> / `prune_action`, plus a fourth shared `action_registry` TU, all under `shell/actions/`; -> `promptText`/`mintBankId` deduped against `panel_bank_ops`; bank verbs reshaped to promptless -> inner verbs (one mutation home, two UX skins — panel and actions each keep their exact prior -> UX); command-id suffixes/display phrases verified byte-identical in review; `prune_action` -> stays a clean deletion-authority isolate (no `Undo_*`, no ext-state writes); -> `persistBankOp`/`persistBook` gain null-session guards; `promptText` renamed `promptBankName`. -> See `COMPLETED.md` for the full narrative. -> -> **Review note (🟡, resolved in Q-W6):** two session pointers / a null-session-as-model-rejection -> misreport (unreachable today) was resolved by Q-W6's `bank_ops` lift. -> -> **In-DAW verification (action families, one-op-one-Ctrl-Z, prune fail-safes) is PENDING on -> `phase-q`** — deferred by design, not yet performed. - -## Q-W5 — split `persist.cpp` (isolate the single file-deletion authority into `prune_fs`) - -> **Landed on `phase-q` (2026-07-29, merge of `pq-w5-persist`). Integrated suite 61/61 green, -> reviewed-approved.** `persist.cpp` (853 LOC) split into `session` / `ext_state_io` / `prune_fs` -> under `shell/persist/` + `persist_internal.h`; the file-deletion authority is concentrated — -> `SHFileOperationW`/orphan-remove lives in exactly one anonymous-namespace function in -> `prune_fs.cpp`, verified tree-wide; the prune fail-safe chain stays byte-intact. T2-04's -> `GetProjExtState` grow-loop is unified as a header-only template, with all three hand-rolled -> copies rewired (`usage_scan`'s start cap raised 4KB→64KB, allocation-only, verified -> equivalent). The Q-W1 `bank_book_json` residual lands via a private static `nameKey` -> (Daniel-approved option a) — `bank_book.cpp` is now ~462 LOC. `persist.h` is kept as a compat -> umbrella for parallel safety (retired in Q-W6); deletion-authority wording is scoped precisely -> in headers; the grow-loop gains a defensive NUL. See `COMPLETED.md` for the full narrative. -> -> **In-DAW verification (save/load/undo-reload, ext-state round-trip, folder relocation, prune -> deletion) is PENDING on `phase-q`** — deferred by design, not yet performed. - -## Q-W6 — OCP registration-table + residual fat-header (I) splits - -> **Landed on `phase-q` (2026-07-29). Integrated suite 61/61 green, reviewed-approved.** Action -> registration/gaccel/hookcommand-dispatch/mirror-unregister all iterate one `ActionTableRow` -> table (flat function-pointer dispatch, no `std::function`/virtual); adding a new action now -> touches one table row only; `main.cpp` shrinks 653→404. FOREVER-STABLE suffixes/phrases/ -> retired-ids verified byte-identical row-by-row in review; capture rows derive their -> suffix+phrase from the pure `captureActionTable()` (the parallel-list risk is gone by -> construction). See `COMPLETED.md` for the full narrative. -> -> **Phase-end cleanup riders (landed in this wave):** `bankOp*` verbs + `persistBankOp` lifted to -> new `shell/bank_ops` taking `ReaSamplerSession&` (dissolves the Q-W4 🟡 review note); -> `persist.h` umbrella retired (13 callers repointed); `capture.h`'s realtime seam moved to -> `capture_realtime_shell.h`; the `wav_trim.h` shim + its INTERFACE target deleted; -> **`core/namespaces.h` DELETED** (the interim Q-W1 shim's contract fulfilled — ~26 includers -> rewired); the grow-loop rehomed to `core/wire/ext_state_read.h`; a stale-comment sweep -> (`persist.cpp`/`bank_panel.cpp` refs); CLAUDE.md's persist/bank_book/actions/wav_codec bullets -> corrected in-wave. -> -> **Review-noted follow-on (not landed, deferred):** extending the table pattern to the -> design_view/bank/ingest families' hand-registration; `channelIdFor`'s shared string-store scan -> is correct-by-prefix-disjointness — a suffix-keyed map would make it structural, but isn't -> required; `view_mode_model.h` (748 LOC) remains the largest header (T4-06's planner split -> stays optional/deferred). -> -> **In-DAW verification (all action families, registration/fire/unregister parity) is PENDING on -> `phase-q`** — deferred by design, not yet performed. - -## Phase Q — sequencing -``` -GATE: Phase S + Phase L L3 merged to dev (D2 complete, M9 abandoned) — tree quiescent - ("when Phase S and L3 are finished" — L1/L2/L3/L4–L7 all landed — GATE SATISFIED) - │ - ▼ -Q-W0 (audit + triage + report — COMPLETE; all 59 dispositions signed off 2026-07-28; - │ fix-now remediations LANDED 2026-07-28) - ▼ -Q-W1 (safe opener: core/json ×5 + wire codec + rect unification + relocation incl. ~20 VST - │ pure libs under core/instrument/{engine,map,ui} + riders — LANDED 2026-07-29) - ├─► Q-W2 (split bank_panel — 8 seams — LANDED 2026-07-29) - │ └─► Q-W4 (split actions + dedupe vs panel_bank_ops — LANDED 2026-07-29) - ├─► Q-W2v (VST god-modules — editor 8 TUs / processor 3 TUs / component_state_io; - │ sampler_core TU whole — documented exception — LANDED 2026-07-29) - │ [parallel with Q-W2: zero overlap] - ├─► Q-W3 (split main — 4 hoists incl. capture_batch; + wav_codec, ICaptureBackend deletion, - │ stamp dedupe, T1-11, capture_realtime_finalize — LANDED 2026-07-29) - │ └─► Q-W6 (OCP registration-table — LANDED 2026-07-29) - └─► Q-W5 (split persist; + ext-state-loop dedupe — LANDED 2026-07-29) [best after Q-W4] - -STATUS (2026-07-29): all seven waves (Q-W0..Q-W6 incl. Q-W2v) structurally COMPLETE, 61/61 -integrated suite green. Remaining: Daniel's in-DAW verification batch, the phase-close -CLAUDE.md architecture refresh, and the phase-q → dev merge on sign-off. -``` -Q-W0 ran and closed 2026-07-28 (its six fix-now remediations landed the same day). W1 was the -safe, high-leverage structural opener (all later waves assumed the layout — including the T4-18 -`instrument/` placement — it establishes). The god-module splits (W2, W2v, W3, W5) were -risk-ordered and mostly parallel-safe; **Q-W2v ran parallel with Q-W2** (different artifact, zero -file overlap — audit §4f SETTLED); W4 depended on W2's `panel_bank_ops`, W6 depended on W3's -isolated registration code. Big-bang was rejected — every wave landed independently, -CTest-green throughout. **All seven waves landed on `phase-q` by 2026-07-29 — Phase Q is -structurally complete** (see the phase preamble's PHASE STATUS block for what remains before the -phase closes and merges to `dev`). - -## Phase Q — must-verify-before-build -- **Q-W0 closed before any structural point** — the functional/DSP audit's findings report exists, - every finding is triaged (fix-now vs. document-and-defer, each with rationale), fix-now findings - are remediated or assigned to the wave that opens their file, and **Daniel has signed off on every - disposition.** Q-W1 does not begin otherwise. (CONTEXT.md §Phase Q Q-W0; naming/DSP smell - categories §2c of `docs/product/code-organization.md`.) **Status 2026-07-28: triage + sign-off - COMPLETE (all 59 dispositions); the sub-gate closes when the six fix-now remediations land - (`pq-w0-fixes`).** -- **~600-line ceiling on every split wave** — every TU a split wave ships lands under ~600 LOC, - with `sampler_core.cpp` the single documented exception (T4-14/T4-27). Seams are the method, - the ceiling is the bar; arbitrary bisection to hit the number is rejected (Q-5 settlement, - 2026-07-28). -- **No dispatch-stack blowouts anywhere** — heuristic (3) generalizes the hot-path guardrail - beyond the three named paths: prefer static polymorphism where types are compile-time-known; - templates only where earned for compile-time dedup (T4-20 yes; T4-21's rect NO-template - ruling). -- **Hot-path call/inline shape** — before landing each split, confirm no virtual dispatch and no - header→TU indirection was added on `peaks` envelope compute, audition/preview, or the realtime - tick. `computeEnvelope` stays a free function on `const std::vector&`; - audition stays a direct call-through; the idle tick stays a single pointer test. (CONTEXT.md - §Phase Q, `docs/product/code-organization.md` §3.) -- **Command-id + display strings are FOREVER-STABLE** — a reorg must not change a shipped - `command_id` string, action display name, ext-state namespace, or VST3 class UID. Re-namespacing - C++ symbols is orthogonal to these on-the-wire/on-disk contract strings; keep them byte-identical - (per-channel, per the Phase V V4 contract). (CONTEXT.md §Phase Q.) -- **Name-collision sweep on sub-namespacing** — `Sample` (model) vs `AudioSample` (audio) vs the - unified `Parser` (json), plus the shared pure-UI rect types `FooterRect`/`ButtonRect` (already - hand-collision-checked in `footer_bar.h`), and any other cross-lib name that collides once - flattened into granular namespaces. Resolve by each symbol's new subsystem home before landing - W1. (naming audit §2b.2; `docs/product/code-organization.md` §6 Q-4/Q-8.) -- **Naming stays zero-behavior-change and off the wire** — the §2b renames touch C++ symbols only; - no `command_id` string, action display name, ext-state namespace/key, `reasampler:` lane prefix, - or VST3 class UID is renamed (naming audit §2b.5). A rename that would alter a shipped contract - literal is out of scope, full stop. -- **The GATE** — do not begin any Q point until **Phase S + Phase L L3** are merged to dev and the - tree is quiescent (D2 complete, M9 abandoned). Re-confirm quiescence against dev before W1. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index c0d9ca0..0000000 --- a/TODO.md +++ /dev/null @@ -1,17 +0,0 @@ -# TODO - -Forward-looking follow-ups. Deferred by decision, not oversight — each entry records why it was deferred and what "done" looks like. - -## Persist ReaSampler 9000 instance identity to let prune reclaim de-referenced captures after reopen - -**Context (what shipped — Phase S usage-detection).** Each ReaSampler 9000 instance publishes the captures it holds to project ext-state (`rsusage_` keys, ComponentState v11). The extension's prune reads those records and unions every live instance's held captures into the referenced-set, so a capture any live instance holds can never be pruned. Fail-safe: unreadable/ambiguous usage state aborts prune (deletes nothing). Airtight on safety. - -**The wart.** The per-instance identity token is minted fresh each incarnation and is NOT persisted. After save→reopen, an instance cannot recognize its OWN prior-session usage record — it looks foreign, so the instance defensively unions and marks the record append-only (poisoned). Net effect: after any reopen, prune stops reclaiming captures an instance once loaded but no longer uses. Safe (never deletes a used capture), but the bank folder grows without bound. - -**Intended fix.** Persist the instance identity in ComponentState so an instance recognizes its own last-session record and does a clean-replace instead of union/poison → prune reclaims de-referenced instance-touched captures normally. - -**The constraint the fix MUST handle (why deferred).** VST3 provides no stable per-instance identity, and Ctrl+D / in-place FX duplication clones plugin state. A persisted identity is inherited by an in-place duplicate → two live instances in one project share one `rsusage_` key. Harmless while both hold the same capture; the risk is a divergent clone — the copies load DIFFERENT captures, and last-writer-wins drops the other's held capture from the record, exposing it to prune. The fix must detect a genuine live same-identity collision and protect the union in that case, WITHOUT reintroducing the sibling-drop bug the fresh-per-session token was originally added to prevent. (Whole-project copies are a non-issue — bank files are cloned with the project and ext-state is per-project.) - -**Priority / risk.** Low / deferred. Current behavior is safe; the only cost is unbounded bank-folder growth after reopens. Decided 2026-07-28 to ship the safe version and defer this. - -**Done looks like.** Save → reopen → de-reference a capture from an instance → prune reclaims it. And: in-place-duplicate + diverge + delete-from-bank never deletes a capture a live instance holds. diff --git a/docs/ARCHIVE.md b/docs/ARCHIVE.md new file mode 100644 index 0000000..2669281 --- /dev/null +++ b/docs/ARCHIVE.md @@ -0,0 +1,1065 @@ +# ARCHIVE.md — ReaSampler pre-1.0 history + +Rarely-read backup of completed version-0 work, rolled here when 0 → 1 closed (2026-07-29). Not a source of context for current work: current architecture lives in the per-directory `src/**/CLAUDE.md` files, current plans in `docs/`. Full uncompressed history is in git. + +## Part 1 — Capture roadmap, Design View, versioning, multi-bank, reclaim + +### Milestone 0 — Transition scaffold: reaper_mpeview → ReaSampler +Retired the MPE scaffold (`mpe_model`/`mpe_view`) and stood up the sampler's pure core (`bank_model`, `peaks`) in its place, preserving the pure-core/REAPER-shell split; extension renamed `reaper_reasampler`; forever-stable action-id prefix chosen for the sampler family. + +### Milestone 1 — bank_model + JSON round-trip (pure) +`Sample` metadata struct + `BankIndex` (add/remove/query/tier moves/dedup-by-hash) with lossless JSON round-trip. Relative-paths-only enforced at the model boundary (absolute paths rejected). + +### Milestone 2 — peaks (pure) +Waveform min/max bin computation from raw PCM, dependency-free of REAPER's own peak API; multi-channel envelopes preserved (no silent fold); remainder-bin and short-buffer edge cases covered. + +### Milestone 3 — Offline capture spike (REAPER shell) +`OfflineRenderBackend` renders the time selection to a bank-relative 32-bit float WAV via `Main_OnCommand(42230)` (REAPER always shows its render-progress window — no headless path exists); populates a `Sample` into the in-memory `BankIndex`; non-destructive; unsaved-project state gates on Save-As (no default-location fallback). + +### Milestone 4 — persist (index ↔ project ext state) +`SetProjExtState`/`GetProjExtState` under namespace `"reasampler"`, keys `bank_index` + `project_guid`. **Decision:** project identity is **GUID-primary** with `ReaProject*` as a secondary disambiguator only — this replaced two earlier iterations (GUID-only mis-detected forks sharing a copied GUID; pointer-primary mis-detected reopen/new-project on address recycling). Save-As is **copy** semantics — old project's bank stays intact. + +### Milestone 5 — bank_panel (docked grid) +Docked LICE grid; thumbnails from `peaks` bins, in-memory cache keyed by `(sampleId, drawWidth, bankGeneration)` — **not persisted**, discarded on bank change. Audition via stock `PlayPreview`/`StopPreview`, single-stop-funnel for leak-free lifecycle (read-only, never inserts into arrange). Multi-select + keyboard nav. + +### Milestone 6 — insert (placement) +`InsertMedia` at the edit cursor onto the **currently selected track(s)** (Daniel's directive — not a new track), for multiple selected tracks placing on each then restoring original selection/cursor; one undo block. Conform-to-project-tempo is an explicit separate flag (`&8`) — the stretch-to-time-selection bit (`&4`) is never set on the default path (pure-tested). Non-destructive to the bank. + +### D1 — view_mode_model (pure) +N-mode registry (Arrange + Design seeded) + GUID-keyed membership + folder-tree-aware visibility derivation + park/restore planner (restore returns captured values, never a hardcoded "on") + JSON round-trip. + +### D2 — view shell +Drives REAPER flags per the D1 planner: `B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline, snapshot-before-park via `GetMediaTrackInfo_Value`. Master and `B_MUTE`/`I_SOLO` never touched (review-gated). + +### D3 — persist slice (view state ↔ project ext state) +View section (membership + active mode + snapshots) serialized under `"reasampler"` alongside the bank; active mode reapplied on project open, including saved-while-parked restore from persisted snapshots. + +### D4 — actions +Bindable Design View action family (`src/actions.{h,cpp}`): toggle/activate mode, tag/untag selected tracks, show-both toggle. New `src/track_guid.{h,cpp}` shared GUID formatter. Saved active mode reapplied on load via a load-signal seam in `persist` (drained by `main.cpp`'s timer). + +### D5 — in-window toggle affordance (UI) +Segmented `[ Arrange | Design ]` mode switch in the bank_panel header via new pure module `mode_switch` (mirror of `bank_grid`); per-mode membership count shown. **Not built:** the offlined-FX re-init tooltip caveat — no trace in source, did not survive the later Phase L panel redesign. + +### D2-W1 — view_mode_model lane extension (pure) +Extended D1's planner to item level: lane↔mode mapping, managed-vs-manual lane-ownership index, managed-only lane ops, the auto-tag decision (manual-lane items exempt; pre-existing content ⇒ Arrange), JSON round-trip of the lane index. + +### D2-W2 — shell: lane application + new-content detection +Applies managed-lane ops (`I_FREEMODE`/`I_FIXEDLANE`/`C_LANEPLAYS`) in the DAW, manual lanes never touched. New pure modules `guid_diff` (poll-to-poll GUID diffing) and `lane_keys` (durable `P_LANENAME`-based lane identity, resolving the `I_FIXEDLANE` renumber-fragility risk). New-content auto-tag runs on the bank_panel timer with a first-poll-after-open guard. **Noted limitation at landing:** item-lane show/hide was structurally correct but a provable no-op on real projects until D2-W3 minted `reasampler:`-prefixed named lanes; end-to-end DAW verification was sequenced after D2-W3 for that reason. + +### Milestone 7 — capture action family +Bindable capture actions, source resolvers (master mix/selected tracks/selected items/razor area). **Wet-only decision (Daniel):** REAPER offline render has no true pre-FX dry bit, so approximate-dry action variants were dropped; `CaptureRequest.wetDry` retained as a seam for true dry (later Milestone 10). **Superseded post-landing:** the four wet source-mode actions were replaced by **three FX-scope actions** (item/track/master, later item/track only) with range inferred orthogonally (razor-else-time-selection) — this fixed item captures rendering through the parent FX chain. FX scope: item = item/take FX only; track = item FX + the track's own FX; the out-of-scope chain (ancestors + master) is neutralized to unity via the reusable `FxBypassGuard` RAII (snapshot → neutralize → render → restore). **Master scope was later removed entirely** — capture is item and track only; to capture the master, render a track instead. + +### Milestone 8 — RealtimeRecordBackend +Track-scope tap via `CreateTrackSend` into a hidden temp track (post-fader, `B_MAINSEND=0`) — captures each source track's own post-FX/fader output, chain-independent by construction; no `FxBypassGuard` needed (fixed the spike's master→temp feedback-loop silent-file bug). Timer-driven async `begin`/`tick`/`abort` state machine, project-scoped transport reads, idempotent latched `restore()`. **Decisions:** track scope only this increment (item realtime deferred — needs per-item take isolation); project-close guarded via `ValidatePtr2` (`dropWithoutRestore()` for an already-closed project); no undo block (transient scaffold, fully reversed by `restore()`). Master scope removed entirely (mirrors Milestone 7). + +### T1 — offline tail: auto (default) + manual override +Three-state `TailMode {None, Auto, Manual}`. Auto: 8s-capped tail render + surgical `RENDER_NORMALIZE` (trim-end-only bit, `32768`) trimmed to -72 dB via a derived `RENDER_TRIMEND` ratio — deterministic (bit-identical repeats hold because only trailing silence is trimmed, nothing scaled/faded). Manual: fixed tail clamped to 8s, no trim. None: byte-identical to pre-tail exact-bounds capture. Exposed as a docked-panel **footer toggle** (cycles Off→Auto→Manual) rather than per-action tail variants (the "…with tail" action variants were dropped). + +### T2 — realtime tail (follow-on to T1) +Parallel tail path for the realtime backend (which doesn't drive `RENDER_*`): records an 8s-capped tail window past the range end, then trims via a PCM backward decay-scan to the -72 dB point (new pure module `wav_trim` for header-aware WAV truncation; `peaks` gained `lastFrameAboveThreshold`). Manual records a fixed tail and skips the scan. **Realtime tail is documented non-deterministic** — bit-identical repeats are not asserted for this path (by design, not a defect). + +### T1-followons — Manual fine-adjust UI + per-project tail persistence +Scroll-wheel fine-adjust of Manual tail length in 250ms steps (`kManualStepMs`), clamped [0, 8s]; label format `"Tail: Manual X.Xs"`. Tail setting (mode + Manual length) promoted into `ReaSamplerSession` and persisted under forever-stable ext-state key `"tail_setting"`; absent key falls back to Off / 2s default. + +### D2-W3-A — lane minting + item→lane assignment + persist round-trip +Pure `planLaneMinting`: a track with ≥2 modes' worth of managed-eligible content gets one durable `reasampler:`-named lane minted per involved mode and every managed item (including pre-existing) assigned to its mode's lane; manual-lane items exempt at the source. Shell `applyMintPlan`/`mintManagedLanes` grows `I_NUMFIXEDLANES` (never shrinks user lanes), one undo block, triggered off the auto-tag detection tick. `reconcileManagedLanes` rebuilds the lane-ownership index from durable `P_LANENAME` on project load. Lane-ownership index rides inside the existing `view_state` blob (no new persistence key). **At landing, two behaviors were flagged as REAPER-runtime-only / DAW-verification-pending:** whether lane names stick when written the same tick a track flips to fixed-lane mode, and whether the leftover empty default lane 0 is silent. + +### D2-W3-B — item-level mode actions + W3-A polish +Item-level "move to Design"/"move to Arrange"/"untag" actions mirroring the track-level family, each re-driving mint/apply, manual-lane items exempt, one undo block each. Plus three carried polish items (redundant `I_NUMFIXEDLANES` re-read removed, shared `item_read` seam extracted, reconcile guard for unregistered mode ids). **Deferred (Daniel's decision):** a per-track lane-split UI indicator in the panel — no natural cheap home found; preserved as a backlog note in PLAN.md Phase D2, not silently dropped. + +### Phase V — Versioning & release + +#### V1/V3 — app_version module: version constant, ext-state stamp, show-version action +Pure `app_version` module: CMake-sourced semver (`REASAMPLER_VERSION`) via `configure_file` → `version_generated.h`; ext-state writing-version stamp (numeric triple only, no channel suffix) under key `"version"` written on every save; absent stamp classifies as silent `PreVersioning`; on-demand "show version" action (no startup print). + +#### V4 — beta-in-isolation: fully isolated coexisting binary via compile-time channel flag +`-DREASAMPLER_CHANNEL=beta` produces a fully isolated `reaper_reasampler_beta` binary: its own ext-state namespace (`reasampler_beta`), forever-stable command-id prefix (`CEREBELLUM_REASAMPLER_BETA_`), `"ReaSampler beta: "` action names, channel-qualified dock ident, `-beta` version-display suffix. All channel identity derives from one `REASAMPLER_CHANNEL_IS_BETA` bit via `channelCommandId`/`channelActionName` composition helpers — no scattered `#ifdef`s in shells. Stable build is byte-identical to pre-V4 identity. **Accepted isolation semantics:** a channel reads/writes only its own namespace (no cross-channel migration/fallback); the `reasampler:` lane-name prefix is deliberately NOT channel-qualified. + +### Phase B — Multi-bank + +#### B1 — bank_book (pure) +REAPER-free bank registry wrapping N `BankIndex` instances (pool seeded + privileged: un-deletable/un-renamable/un-evacuable, never zero banks); create/rename/reorder/delete named banks; active-bank id; move/copy a sample between banks (destination collapse-by-hash observed); evacuate; JSON round-trip with legacy `bank_index` migration into the pool. `BankIndex` itself untouched (additive). **Fork R-B, settled 2026-07-24 (Phase-B-wide, retro-touches B1–B4):** every bank index verb — action and panel gesture alike — wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`, SDK-verified to participate in undo), with a `projectconfig` hook triggering deferred session reload so Ctrl-Z/redo visibly restores state in-session. + +#### B-cap — owned-file manifest seam +Capture records each file it creates into an owned-file manifest (pure `owned_manifest` module), persisted under a sibling `"owned_files"` ext-state key, decoupled from `bank_book` (tracks creation, not index membership). Landed early — deferred to Phase R would have meant retroactively reconstructing the manifest, a backfill cliff (fork R-D). + +#### B2 — persist slice (banks ↔ project ext state) +Book serialized under `banks` key (pool folded in as bank-zero), distinct from `view_state`; legacy `bank_index`-only projects migrate to pool + zero named banks on first load, with `banks` authoritative and no `bank_index` written thereafter. + +#### B3 — actions +Bindable multi-bank action set: create/rename/delete/evacuate bank, activate (direct + cycle), move/copy selected samples to a bank, pool/banks full-height toggles. + +#### B4 — bank_panel vertical split (UI) +Pool grid (top) + named-banks tab-page region (bottom); LICE-drawn tab strip (new pure module `tab_strip`, mirror of `mode_switch`) with overflow/scroll (fork 5a); both a "move to bank" menu and drag-between-regions for sample moves, copy as secondary act on the menu (fork 5b), with drop-target highlighting during drag. Delete confirms on non-empty banks, naming evacuate as the alternative. Active-bank indicator resolved at build time. + +#### B5 — sample-remove (the missing sample-level verb) +Surfaces `BankIndex::remove` through `bank_book`: drops a sample's index entry from a bank/the pool, index-only, non-destructive to the file (orphaned until prune); pool contents removable while pool-container privileges hold; no confirm dialog (recoverable via the R-B batched undo). **Fork R-A, settled 2026-07-24:** remove scope is **this-bank only** by default and only surfaced affordance; an `all-banks` scope stays a latent, unshipped parameter in the action signature. + +#### Milestone 10 — provenance (re-capture from source) +Reshaped from an earlier, broader M10. **Cut (Daniel):** the null-test verification action and the true pre-FX-dry mechanism — both dropped (`docs/product/provenance.md` §What was cut). **Kept, forks P1=a/P2=a settled 2026-07-23** (P3/P4 moot under P2=a): `Sample.provenance` populated on resample-from-sample with parent id + a **thin `rsprov1` capture-recipe fingerprint** (scope, exact range, tail, rate/channels, track GUIDs, FX-chain identity — a drift/repro fingerprint, explicitly NOT a serialized chain to restore); "re-capture from source" action regenerates the file in place from the source's current state (bank-only, never places into the timeline), reporting drift if the source changed. Because re-capture is bank-only, provenance stays pure per-sample bank metadata and `bank_model`/`view_mode_model` remain decoupled — dual-canvas compliance needed no new coupling. + +#### Phase B open questions — all resolved +All five forks settled by Daniel 2026-07-23 (persistence-key fold, delete-drops-members + evacuate verb, move-as-default gesture, distinct active-bank/shown-tab with an indicator, LICE tabs + both move affordances); B5 forks R-A/R-B settled 2026-07-24. Recorded in `docs/product/removal-and-prune.md` §Fork R-A/R-B. + +### Phase R — Reclaim (file lifecycle: the prune path) + +> **Boundary (load-bearing):** remove creates orphans; prune reclaims them. No operation other than prune deletes a file; prune deletes only files no index references. + +#### R1 — prune-reconcile core (pure) +`pruneOrphans(present, referenced, owned) → (owned ∩ present) − referenced`, filesystem-free; referenced-set unioned across the whole book so a file kept alive by any bank (including via a copy) is never an orphan; a present-but-unowned (hand-dropped) file is never reclaimed. + +#### R2 — prune shell + persist wiring (filesystem I/O, thin) +Enumerates the resolved current bank folder, unions `book().referencedPaths()` + the owned-file manifest, feeds the pure core, produces a dry-run report (count/bytes/file list). No deletion this wave. Forever-stable `BANK_PRUNE_FOLDER` action registered (dry-run only in R2). + +#### R3 — deletion + action (the destructive step, guarded) +Dry-run → `ShowMessageBox` confirm → staleness-intersect (`pruneDeletePlan`: confirmed ∩ fresh pure-core output) → delete exactly that plan; zero ext-state writes, no undo point (file deletion is not REAPER-undoable). `bank_panel` prune button dispatches the same registered action (no duplicate logic). **Forks settled 2026-07-24:** R-C (deletion mechanism) — trash-preferred (Windows `SHFileOperationW`+`FOF_ALLOWUNDO`), unlink fallback where no portable trash surface exists (macOS/Linux); R-D (orphan attribution) — owned-file manifest, seam landed early at B-cap rather than reconstructed at prune time; R-E (trigger) — manual action + panel button only, dry-run-first, no background sweep, no "delete-and-prune-now" convenience shipped. + +#### Milestone 11 — polish (wave 1) +Batch capture (`batch_capture` pure module, one sample per selected item/razor area, mixed-result aggregation, single persist per batch); action trigger buttons + keybinding-help labels (`action_buttons` pure module, live-binding labels via `kbd_getTextFromCmd`); conform-on-insert closed as already-shipped (verified-extant, no new code). **Cut (Daniel, 2026-07-26):** resample-and-mute-source — superseded by the Design View mode projection (park/hide inactive-mode content), which made a mute-after-capture workflow redundant. + +#### Milestone 11 — polish (wave 2 / completion) +Native OS drag-out: pure `drag_out` module (gesture-boundary decision at panel-edge, path-list assembly with dedupe/missing-file skip) + `drag_out_win` shell (Windows OLE `DoDragDrop`/`CF_HDROP`; macOS/Linux via `SWELL_InitiateDragDropOfFileList`). Structurally copy-only — `DROPEFFECT_MOVE` never offered, no source-deletion path exists; prune remains the sole file-deletion authority. Completes Milestone 11. + +## Part 2 — Look-and-feel, and the ReaSampler 9000 instrument + +### Phase L — Look-and-feel (system-wide visual design language) +Own lettered namespace (`L`), orthogonal to and ungated by the M/D/B/R/V/S pillars — a +parallel team's whole-system look-and-feel effort answering Daniel's post-DAW-test verdict +("this looks like temple os… does Cockos have a toolkit?"). The then-authoritative spec was +CONTEXT.md §Phase L. Product framing + settled decisions: `docs/product/visual-design-language.md`. +**Settled decisions (Daniel, 2026-07-26):** **DS-1** — toolkit is LICE + WDL free game, no +external frameworks (iPlug2/JUCE/VSTGUI rejected); reuse a WDL/vwnd piece where it beats +re-deriving; keep hit-test geometry in pure CTest-covered modules. **DS-2** — visual +direction Direction B ("Neon Console") + Direction C's spectral keyboard strip, **REVISED +2026-07-26 (palette-only):** neutral ladder moved from near-black into REAPER's mid-grey +family (`bg/base #2b2b2b` / `bg/panel #333333` / `bg/cell #3a3a3a` / `line/hairline #4a4a4a` +/ `text/primary #dcdcdc` / `text/dim #a8a8a8`), and a single electric-cyan accent replaced by +a **three-accent pastel system** (`accent/primary` lime `#B0E098` = live/active/selected, +`accent/secondary` teal `#84D6D0`, `accent/tertiary` purple `#C2AAE8` = categorical +distinctions); a bundled-font upgrade was considered and **declined** (no font +redistribution). **DS-3** — dock-panel scope is a thorough layout redesign, not a light +re-skin; sequenced after M11 merged. **Phase L is complete: L1–L7 and L3 all landed.** + +#### L1 — shared LICE drawing kit (the foundation) +`theme`/palette module (role→color, the DS-2-revised grey ladder + three pastel accents, +WCAG contrast-floor helpers/tests, `theme_tests`); `component_geometry` (button/slider/ +list-row geometry + hover hit-test, `component_geometry_tests`); `draw_kit` shell +(`fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()` +over four `LICE_CachedFont`s, full interaction-state model, double-buffer preserved). GDI +`DrawText` retired from `bank_panel` — the single biggest "temple os → modern" lever. + +#### L2 — dock-panel layout redesign +`action_bar` pure module (task-grouped clusters: Capture/Placement/Maintenance, label + +keybinding sub-rects, overflow, hit-test; `action_bar_tests`). `bank_panel` redesigned with +the full M11-aware button inventory placed by cluster, prune kept footer-set-apart + +`warn`-colored, entire panel drawn through the L1 kit with a single `KitColor→LICE_pixel` +boundary (`draw_kit`'s `toLice`). + +#### L4 — dock-panel button layout enhancement +Three-zone re-home: top toolbar (capture/placement/maintenance — **Maintenance restored to +the top toolbar per Daniel's directive during build**, revising the original "capture + +placement only" spec), bottom toolbar (Design View tagging/switching), footer (narrow +`[Arrange|Design]` toggle · Tail button as a proper kit button · Prune set apart). New pure +`footer_bar` module (`footer_bar_tests`); `action_bar` gained `ActionCluster::Tagging`/ +`Switching`. Pure re-home of existing actions — no new actions, no capture/placement change. + +#### L5 — dock-panel button refinements +Rare capture variants (Batch Items/Batch Razor/Capture RT) pulled into a right-anchored +More (⋯) overflow menu (`overflow_menu` pure module + `TrackPopupMenu`); short button faces +with the `ReaSampler:` prefix dropped; a **custom LICE-kit hover-delay tooltip** (`tooltip` +pure module, sourced from the registered action phrase, not `kbd_getTextFromCmd`) chosen +over attaching a SWELL tooltip to non-child LICE rects; four Item/Track × Arrange/Design tag +buttons with opposite-mode-only enablement (`mode_enable` pure module); Activate-Arrange/ +Activate-Design/Toggle buttons removed (footer toggle is the sole mode-switch affordance; +Show Both stays); cluster-gap spacing widened 16→24px. + +#### L6 — toolbar polish (in-DAW feedback pass on L5) +Single-row button faces (keybinding sub-row removed, toolbar height 40→28px); keybinding +folded into the hover tooltip (`"phrase — binding"`); Cancel RT moved into the overflow menu; +visible top-bar cluster order tidied (Capture → Maintenance → Placement). Icons considered +and deferred. + +#### L7 — capture ordering, card metadata, and selection styling +Persisted deterministic capture order via a per-`Bank` id→slot `SlotMap` in `bank_book` +(gap-preserving, JSON rides inside the existing `banks` blob, pre-L7 migration seeds dense +order); `BankBook` gains `reorderSample`/`replaceSample`/`orderedSampleIds`/`reconcileSlots`. +One sanctioned `Sample` change: `captureTimeSigNum`/`captureTimeSigDenom` stamped via +`TimeMap_GetTimeSigAtTime`. `card_drag` pure module (gesture precedence: leave-client→OS +drag-out, other-bank→move/copy, same-bank→reorder/Alt-replace; SWELL stock cursors per +gesture) and `card_meta` pure module (bars.beats.subdivisions + s.ms, blank when unstamped). +Sparse-grid render with gap cells; selection restyle drops inverted-fill for a normal cell + +`accent/tertiary` purple border. **M9** (capture-to-slot-N MIDI-bindable capture) stays +explicitly deferred — L7 eases a future revival but ships no slot-numbered actions or MIDI +bindings. + +#### L3 — VST editor + embed-strip restyle +Brought `reasampler_editor.cpp` + `reasampler_embed.cpp` onto the L1 kit: cached-font +`text()` replacing raw GDI `DrawTextA`; the shells' **local pre-L1 palette** (forest-green +`kColBackground`/`kColCardBg`/`kColThumb`/… constants, arrived off-kit — the "born in the +kit" coordination contract did not occur) retired in favor of the L1 `theme` roles; capture +browser/toggles/ADSR+pitch sliders/zone bars/list rows/waveform all restyled through the kit; +pastel spectral keyboard strip + zone bars (active zone lifts to `accent/primary` + a static +glow, no animation). Beta title band stays a **textual-only** distinction (no channel- +specific accent color — closed the one open L3-readiness fork). VST3 class UID unchanged (a +visual refresh is not a compat event); all `src/vst/` geometry modules stayed pure — L3 +touched only the two draw shells. **L3 was the last Phase L point — Phase L is complete.** + +### Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact) +Landed on `dev` (merged 2026-07-27); **DAW verification pending Daniel's smoke test** at the +time of this record. The then-authoritative spec was CONTEXT.md §MIDI-playback instrument +(Phase S). Product framing: `docs/product/midi-playback.md`. + +#### S3 — pure sampler core (voice engine / envelope / keymap / repitch) +The REAPER-free and VST3-free core — the heart of the phase, mirroring `bank_model`/`peaks`/ +`view_mode_model`. Polyphonic voice allocation with bounded stealing; ADSR envelope math +asserted against a known signal; repitch/interpolation from root note with loop-point-aware +sustain; keymap model (key ranges/zones, (note,velocity)→sample query, chromatic-from-root +as the degenerate Tier-0 case). Core boundary enforced plain-data-only (test target links +neither SDK). + +#### S4 — Tier 0: "the bank plays" (single sample, chromatic) +Wired the S3 core into the VST3 shell: `process` marshals MIDI note-on/off/velocity, drives +the core, writes to the output bus (block-granular timing at this tier). Live-state seam +reads the bank + selected sample's root note off the bridge, resolving WAV paths the same +project-relative way `persist` does (shared `capture_paths::projectDirOfRpp`; bank JSON +parsed via the shared `bank_book` path, not a spike string-scan). Minimal LICE sample-picker +list in the editor; the pick is instance-owned component state, never written to the bank. +Chromatic-from-root, 16-voice polyphony, amp envelope, velocity→volume; load/decode off the +audio thread via lock-free atomic pointer swap (graveyard-reclaim) — `process` never +allocates. + +#### S5 — Tier 1: "a keymap" (zoned multisamples, per-sample root notes) +Keymap editor in the LICE editor assigns bank samples to key ranges, each with its own root +note (from S2 intrinsics, overridable in the performance map). Zoned playback resolution — +one sample per key-region, repitched from its own root. Keymap persisted as the instrument's +own VST3 component state (D-B split: bank carries file-fact defaults, instrument owns the +performance choice); the live bank seam stays read-only. + +#### S6 — embedded TCP/MCP UI (D-D) +`IReaperUIEmbedInterface` implemented so a compact keymap/level strip draws inline in the +TCP/MCP, reusing the same LICE surface as the main editor; clean open/close/resize lifecycle. +Scheduled (not deferred) per D-D, sequenced last as polish over the Tier-0 need. + +#### S7 — stereo channel mode (mono | stereo) +Per-instance `ChannelMode {Mono, Stereo}` as an S3-core extension (not a shell hack): +`SampleData` carries 1- or 2-channel decoded PCM, `Voice::renderFrameStereo` + +`VoiceEngine::render(left,right,n)` share one read head/envelope tick; mono stays the exact +degenerate case (byte-identical). Cross-mode decode policy: mono source + stereo mode → +dual-mono; stereo source + mono mode → downmix. Toggle lived in component state v4 (v3 + +one byte), never written to the bank. `setBusArrangements` accepted only the mode's +arrangement, with `restartComponent(kIoChanged)` on a runtime mode change — verified against +the vendored Steinberg SDK. **Superseded later:** the GA post-launch pass (recorded in Part 3 +scope) deleted the dynamic mono↔stereo bus renegotiation in favor of a permanently stereo +output bus with `ChannelMode` becoming decode-only — do not read S7 as the final channel-mode +shape. + +#### S8 — ingest through the bank (one gesture: capture/import into bank + assign) +Goal: the extension owns ingest (arrange/Media-Explorer/drop-target access); the instrument +stays a read-only bank consumer. Recorded **honest SDK limits** at spec time: Media Explorer +exposes only `OpenMediaExplorer` + `MediaExplorerGetLastPlayedFileInfo` — no enumerate- +selected-files, no drop-handler registration — so ME import is single-file, pull-on-action, +not push/drop; REAPER exposes no drag-drop registration API, so panel drop handling rides +ReaSampler's own HWND (`WM_DROPFILES`/`IDropTarget`), and a drop onto the VST3 editor window +was flagged as an unproven cross-artifact spike (see S13). **All four checklist items are +recorded unchecked (`[ ]`) in the source** despite the Phase S banner asserting S1–S18 landed +on dev — no confirming text elsewhere in this range resolves the discrepancy (see Open +questions). + +#### S9 — bank-generation change-detection +A monotonic bank-generation counter (new `ext_keys.h` constant, forever-stable spelling) +stamped into `"reasampler"` ext-state, bumped on any bank-content mutation that changes +instance playback (capture add, recapture-in-place, remove, move/copy). The instrument polls +it off the audio thread on a UI/timer cadence and calls `reloadFromBank()` on change, reusing +S4's atomic pointer-swap handoff (no `process`-thread work, no glitch). Pre-S9 projects +default to generation 0. + +#### S10 — capture-first editor: browser + guided single-capture setup +**Policy reversal (revising S4):** the "first sample plays" fallback is removed — a fresh +instance with no stored selection plays **nothing** and the editor shows an explicit +"pick a capture" empty state; loading only happens on an explicit pick (or S13 drop / S8 +ingest). Default editor face reframed (Daniel, 2026-07-26) as a **capture browser** +(peak-thumbnail cards, name, root/key badge, bank filter) feeding a guided single-capture +setup (root note, play-mode basics, level), demoting the full keymap editor to an opt-in +**S10-Z Zones panel**. New pure `keyboard_strip` + `capture_browser` modules planned (mirrors +of `mode_switch`/`editor_geometry`); drag-state machine on the LICE `IPlugView`. **All +checklist items for S10 (and its S10-Z sub-section) are recorded unchecked (`[ ]`) in the +source** — see Open questions; the ±1 nudge-button row and `zoneHitTest` were slated for +retirement here regardless of check-state. + +#### S11 — waveform view with draggable loop points +Selecting a zone/capture shows its waveform (via the existing `peaks` module over the +already-decoded PCM); loop-start/loop-end markers drag to set the sustain loop, snapping to +the nearest zero-crossing; a sample with no loop shows a "no loop" state. Loop points are a +performance-map override on the zone, seeded from the S2 bank intrinsic (D-B split — the +bank fact is never written back). **Boundary note (S10 reframe):** this waveform draw is the +same one S10's picked-capture view uses, built once. **All S11 checklist items are recorded +unchecked (`[ ]`) in the source** — see Open questions. + +#### S12 — editor scale + ergonomics +**Boundary note:** the "sample list" S12 was to scroll/search became S10's capture browser; +S12 narrowed to (a) scroll + type-to-filter search layered over that browser and (b) numeric +entry + ADSR, and additionally absorbed the S15/S16 control surfaces (deferred to here per +spec). **Landed (all three checklist items `[x]`):** pure `browser_scroll` module (scroll/ +search geometry, `browser_scroll_tests`); pure `note_entry` module (`parseNoteEntry` accepts +decimal or note-name input, clamps to [0,127], `note_entry_tests`); pure `param_slider` +module (control-panel stack/toggle/slider geometry + point→control routing, +`param_slider_tests`) driving AHDSR (hold-stage addition) plus the S15/S16 controls (Gate/ +Trigger toggle, Trigger %-length/fades, Varispeed/Preserve toggle, AD pitch-envelope +enable/attack/decay/depth) on the selected zone's `ZonePlayParams`, committed via +`commitAndReload`. **Standing ruling (Daniel), enforced here:** wall-clock envelope times are +stored as rate-free **seconds** (zones payload v5), resolved to frames at keymap build +against the live project rate — no hardcoded sample rates in `src/`. + +#### S13 — drop-to-load (partial landing; relay deferred) +**Spike verdict (ps-w12, 2026-07-27): DEGRADED.** The cross-artifact relay would require a +new instrument WRITE seam into ext-state plus an extension-side timer-poller/claim-nonce +handshake — the same race the S17 spec rejected — so it was explicitly deferred (remains in +`PLAN.md` §S13); the shipped ingest gesture stays drop-onto-docked-panel (S8). **Landed:** +the editor child window accepts `WM_DROPFILES`/`IDropTarget` (Windows-only, D5) and, since no +relay exists, shows a transient banner + persistent empty-state affordance directing the user +to drop onto the ReaSampler bank panel instead — no file is ever ingested from the editor +drop, and no timeline item is ever inserted. + +#### S15 — sampling modes: Trigger vs Gate +Per-sample/per-zone performance choice (D-B), an S3-core extension. **Gate** grows ADSR to +AHDSR (`holdFrames`, `==0` is exactly pre-S15 behavior — back-compat, no type rename), +sustain loop points apply. **Trigger** is a one-shot: plays `[start, start+lengthFraction· +(frames−start))` with equal-power fade-in/fade-out, ignores note-off entirely, no sustain +loop; choke-on-note-off explicitly out of scope (fork S15-F1, held). Both modes gain a +modifiable **start point** (playback offset). Mode + params live in the performance map; +truncated/older blobs default to Gate/hold=0/start=0 (today's behavior unchanged). Editor +control surface deferred to S12 (spec-sanctioned). + +#### S16 — pitch engine modes (Varispeed vs Preserve) + pitch envelope +Per-zone `PitchEngine {Varispeed, Preserve}`. Varispeed = today's coupled pitch/duration +path. Preserve = duration-preserving repitch via a hand-rolled pure OLA `pitch_shift` module +(house pattern, `pitch_shift_tests`; `WDL_SimplePitchShifter` excluded by an include-chain +conflict with `windows.h`, held as a future quality/latency swap behind the same contract). A +per-voice AD pitch envelope (`enabled=false` default) multiplies the Varispeed ratio or adds +to the Preserve shift amount. **S16-F1: default engine is Preserve** (Daniel: "I want +duration-preserving repitching"), per-zone togglable back to Varispeed. Editor exposure +deferred to S12. + +#### S17 — drop-and-load: drag a capture onto a track's FX button +Dragging a single capture over a track's TCP FX button lights it as a drop zone; dropping +instantiates ReaSampler 9000 on that track with the capture already loaded and selected. +Third `drag_out` gesture `InstrumentDrop` (only arms for a **single** capture — multi-payload +over REAPER UI falls through to `OsDrag`, per a resolved open question). Mechanism: +`TrackFX_AddByName` + a Steinberg-format `.vstpreset` built by the new pure `instrument_drop` +module from the instrument's own `sample_map::serializeComponentState` (one serializer, two +artifacts — the byte format cannot drift), applied via `TrackFX_SetPreset`. **The originally +planned `TrackFX_SetNamedConfigParm` "vst_chunk" write proved silently unappliable for VST3** +and was replaced by the `.vstpreset` + `TrackFX_SetPreset` path (GA DAW-fix pass, 2026-07-28). +FX-button hotspot resolved via `GetThingFromPoint` (`"fx_chain"`/`"fx_N"` info strings), not +home-grown geometry. Batched into one undo point; never inserts a timeline item. +**VERIFIED (GA DAW-fix pass, 2026-07-28).** + +#### S18 — VST3 channel isolation (beta ReaSampler 9000 pairs only with beta extension) +Extends V4's beta/stable split to the VST3 instrument: a second FOREVER-STABLE class UID +(`REASAMPLER_PROC_UID_BETA_1..4`) alongside the existing stable UID, selected at compile time +by the single `REASAMPLER_CHANNEL_IS_BETA` bit (one class per binary, both UIDs frozen +forever); binary name forks via CMake `OUTPUT_NAME` (`reasampler_9000` / `reasampler_9000_beta`); +display name/version-string channel-aware via `app_version::vstPluginName()`. Pairing-surface +invariant recorded structurally (all wire keys live under the channel-derived +`kProjExtNamespace()`, so a key that forgets to isolate is impossible by construction) rather +than newly coded. + +#### S-VIEW-BUG-1 — drop-to-FX bug fix (Wave 1) +Root cause: the pure `instrument_drop` FX-hotspot classifier matched only `fx_*` strings, so +a drop on the TCP (`tcp.fx`) or MCP (`mcp.fx`) FX button fell through to `OsDrag` (arrange- +as-audio) instead of instantiating the instrument. Fix: predicate widened to match `fx_*` / +`tcp.fx` / `mcp.fx`, unit-tested at each hotspot string; shell unchanged. **DAW-confirmation +recorded as pending** Daniel's post-merge smoke test at the time of this entry. + +#### S-VIEW-SIZE-1 — 1080p default window size (Wave 1, interim) +Default editor `ViewRect` bumped from 560×400 to **840×560** with a `checkSizeConstraint` +minimum floor, verified against the vendored `pluginview.h` mechanism. **Explicitly interim** +— recorded as the correct starting point, not the final tuned value; final tuning deferred to +a later wave (T-SHELL) once the three concrete Sample-face band heights exist. + +--- + +## Part 3 — Editor redesign, GA fixes, and the Phase Q structural reorganization + +### Phase S — editor view-model redesign (three views: Sample / Browse / Zone) +Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9). Retired the flat Browser|Zones +toggle for a three-view model — Sample (home/default), Browse (full-window modal overlay over +Sample, S-VIEW-F3 settled), Zone (dedicated surface, own button). Landed across three waves +(pure modules → shell wiring → velocity-curve editor UI); **S-VIEW complete — all ten points +landed**, integrated suite green. + +#### S-VIEW-1 — three-view navigation model +Flat toggle retired; three-view model landed. Empty state surfaces Browse as the dominant +call-to-action; fresh instance stays silent (S10 reversal preserved). S-VIEW-F3 settled and +implemented: Browse is a full-window modal overlay over Sample. + +#### S-VIEW-2 — Sample view (the new main face) +Composed the home face: hero waveform with the S11 markers (moved from Browse), a fenced root +affordance, the Mono/Stereo toggle (moved from Browse), and the "modes-and-down" control strip +(Mode / Pitch engine / AHDSR|Trigger / AD pitch env, moved from Zone's param panel — single- +capture one-zone storage per S15-F2). Reference grammar: Simpler / Phase Plant. + +#### S-VIEW-3 — envelope overlay + draggable nodes +Amp envelope (AHDSR for Gate, fade/%-length for Trigger) drawn as a curve over the Sample +waveform at accurate wall-clock time. New pure `envelope_overlay` (params+frame-length→polyline) +and `envelope_edit` (node hit-test + pixel-delta→clamped-param inverse map) modules, both +unit-tested. Breakpoints are draggable (S-VIEW-F2 settled): X→segment time, Y→level on +level-breakpoint nodes (sustain drags both axes), monotonic-in-time + range-clamped. The +Trigger frames↔fraction conversion was extracted to a new pure `trigger_seam` module. + +#### S-VIEW-4 — preview-trigger + velocity knob +A button fires the sampler at the loaded capture's root note through the live voice engine +(off the audio-thread commit path) plus an adjacent velocity knob. Preview velocity PERSISTS +(S-VIEW-F1 settled): `previewVelocity` on `ComponentState`, envelope v5→v6, round-tripped via +`getState`/`setState`; zones payload untouched, older blobs lift to a mid default. + +#### S-VIEW-5 — Browse reduced to choosing (modal over Sample) +Search + bank tabs + captures grid + scroll + selection retained; confirm/cancel added +(double-click loads). Large waveform preview, Mono/Stereo toggle, root keyboard-strip (all +moved to Sample), and loop-point labels + track-root message (cut) removed. Renders as a +full-window modal overlay (S-VIEW-F3 settled). + +#### S-VIEW-6 — key-tracking parameter +Per-`PerformanceZone` scalar on keyboard pitch tracking around the root (100% = 12-tone-ET, 0% += no tracking, 200% = double); additive, defaults 100% (bit-identical). Key-track math lives in +the pure sampler core, applied in both Varispeed and Preserve repitch; zones payload v6. +Surfaces on the Zone param panel + Sample control strip. + +#### S-VIEW-7 — piano-key pattern on the keyboard strip +Alternating white/black (bright/dark per palette) key pattern overlaid on the pastel spectral +fill via a pure `isNaturalKey` predicate (12-tone, unit-tested) on `keyboard_strip`. Shared by +the Zone strip and Sample root affordance. + +#### S-VIEW-8 — Zone view retained + wired ++Add Zone / Delete, the per-zone keyboard strip (piano pattern), the Low/High/Root numeric-entry +legend, and the per-zone param panel all retained; key-tracking control added. Nothing from the +prior Zones view dropped. + +#### S-VIEW-9 — velocity→amp transfer curve (pure core + engine application) +New pure `velocity_curve` module: eval via a Fritsch–Carlson monotone cubic Hermite spline (no +overshoot outside [0,1]) + control-point editing (add/move/delete x-ordered + box-clamped) + +hit-test + pixel-delta→clamped-point inverse map. Additive `velocityCurve` field on +`PerformanceZone`, zones-payload v7; default = **flat y=1** (R10-F1 settled, Option A, Daniel +2026-07-27: "any velocity plays at full level"); older ≤v6 blobs lift to flat y=1. Applied at +`Voice::start()`, replacing `velocityGain_ = velocity/127.0` with `curve.eval(velocity)`. +**Default is a deliberate non-back-compat behavior change** — existing zones' soft hits play at +full level after upgrade; flagged and accepted by Daniel. + +#### S-VIEW-10 — velocity-curve editor UI (shell, Sample + Zone views) +Draggable transfer-curve editor rendered through the L1 kit in both the Sample view (curve box +beside the hero band) and the Zone per-zone param panel: `eval` polyline over a 0–127 × 0–1 box +with draggable control points — add on empty-click, move, Alt-click delete interior, drag-off-box +delete with a warn-state affordance. Coordinate math added to `velocity_curve` (`pixelFromPoint`/ +`pointFromPixel`); reads/writes the existing `velocityCurve` field — zones payload stays v7, no +schema change. Mirrors the S-VIEW-3 envelope-node interaction grammar (snapshot-at-grab → +off-audio-thread commit). + +#### Phase S — product name (ReaSampler 9000) +The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26, on +DAW-testing the S1–S6 instrument); the extension remains **ReaSampler**. Propagated across the +VST3 class display-name string, the `IPlugView` editor title band, the S6 embed-strip label, and +Phase S docs. **Do not change the VST3 class UID** — already-saved-project instances key off it. +**S-NAME-1 settled (Daniel 2026-07-26):** the built binary filename (CMake `OUTPUT_NAME`, e.g. +`reasampler_9000.vst3`) was renamed alongside the display strings so the on-disk name matches; +the VST3 class UID stayed unchanged as the compat anchor. + +S-VIEW-1 through S-VIEW-10 rolled up as fully landed (Wave 1 core + Wave 2 shell + Wave 3 +velocity-curve editor UI). + +### Phase D2 — Two-canvas (item-level mode projection; additive to D1) +Extends D1's track-level mode projection to **item level** via REAPER 7 fixed lanes: on a track +present in both stances, each mode owns a fixed lane — the active mode's lane shows/plays, the +inactive mode's is hidden/silenced — so a Design take and an Arrange take share a track/time +position without colliding on the view. Nothing in D1 changes; runtime floor rises to **REAPER +7** for this sub-phase (no version-gate branch — simply unavailable below v7). D2-W1 (pure lane +extension), D2-W2 (shell: lane application + new-content detection), D2-W3-A (lane minting + +item→lane assignment + persist round-trip), and D2-W3-B (item-level mode actions + polish) have +all landed — **Phase D2 is functionally complete.** **Deferred, not dropped:** a per-track +lane-split marker in the panel UI — the mode switch already shows the active mode; no natural +cheap home for a per-track indicator was found. + +### Milestone 9 — slots (MPC-style) +**Abandoned (Daniel, 2026-07-27) — will not be built.** Goal had been "capture to slot N" / +"insert slot N", MIDI-bindable, with slot state persisted via the index. + +### Post-S-VIEW DAW-fix pass + voice-system redesign (2026-07-27) +Merged to dev; integrated suite 52/52 green. Found during Daniel's post-S-VIEW DAW testing +(fixes) and a subsequent voice-redesign directive — not PLAN-tracked. + +- **Envelope node editing:** every Gate stage (A/H/D/S/R incl. a visible in-bounds Release) and + Trigger's zero-fade-out node are now fully grabbable in both modes; `envelope_edit` uses + param-domain schematic scaling (8px minimum node separation enforced by the forward map in + `envelope_overlay`), all nodes clamped in-canvas. Previously only a subset was editable. +- **Gap-free waveform render:** `columnMinMax` moved to `peaks`; `waveformColumnCount` moved to + `component_geometry`; dock panel and VST editor now share one gap-free per-column min/max + algorithm via `draw_kit::drawWaveform`. +- **Radial Knob primitive:** `param_slider` gained a `Knob` control kind (7→5 o'clock arc, needle, + vertical-drag) — foundation for the later Wave B control deck. +- **Zone-bleed fix 3a:** `reconcileSingleCaptureZones` added to `sample_map`, fixing stale + full-range zone shadowing so the engine plays the zone the editor draws; called on load + (`setState`), bank-assign, and capture-confirm. +- **Voice-system redesign (Daniel directive):** `sampler_core` gains user-parameterized voice + count (1–32, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` + Retrigger/Legato toggle), an isolated `PreviewCard` (dedicated preview voice, never steals + from/into the MIDI pool), two-tier panic (CC123 release / CC120 immediate hard-stop incl. + Trigger one-shots). Processor sums `PreviewCard` alongside engine+drain; `retireIdleDrain()` + retires fully-idle drain snapshots on the UI-timer cadence; voice-param setters rebuild the + engine via the drain-slot swap (no bank re-read, no WAV re-decode, ringing tails not cut). + `ComponentState` envelope bumped **v6→v7** (voiceCount/voiceMode/monoTrigger bytes); pre-v7 + blobs lift to `{16, Poly, Retrigger}` — reproduces pre-Phase-S behavior exactly. + +### FB1 — Sample-view recomposition + master gain (r11; 2026-07-27) +Merged to dev; integrated suite 55/55 green. Closes S-VIEW-11 + S-VIEW-12 from the Wave B plan +(S-VIEW-13 stays open, deferred to FB2). Recomposed the Sample face after Daniel's post-landing +DAW pass. Three new pure modules: `knob_deck` (group-box/caption-row/compact-toggle/knob-cell +geometry, deterministic whole-group wrap), `curve_popup` (centered-sheet geometry, width/height +clamps, outside-sheet dismissal test), `master_gain` (dB↔linear taper math for a new −∞…+24dB +post-mixer master gain, norm 0 = true silence, unity ≈0.714 normalized — shared by the editor +knob and the processor multiply so they cannot drift). Sample control strip rebuilt as five +fenced groups: AMP ENVELOPE (Gate|Trigger toggle), PITCH (Varisp|Preserve + Key Track), PITCH ENV +(Off|On), VOICE (Voices/Poly|Mono/Retrig|Legato), MASTER (Gain). Hero waveform now runs +full-width/elastic (840×620 default preserved, floor 150px — **R11-F1 settled** at build, no +window-size change required). New 28×28 curve preview button opens a `curve_popup`-geometry +centered sheet; right-click on a node deletes it (endpoint-guarded). New persisted +`masterGainLinear` field, `ComponentState` **v7→v8**, pre-v8 blobs lift to unity gain; processor +applies it as a per-sample ramp (no zipper noise). + +**Decision:** persistence was **NOT zero-change**, contrary to the original r11 spec description +of a pure view recomposition — CONTEXT.md's "zero component-state change" clause was corrected to +record the v8 bump and the master gain field. + +### FB2 — Zone-panel parity (r11; 2026-07-28) +Merged to dev; integrated suite 55/55 green. Closes S-VIEW-13 / R11-F2. Zone param panel rebuilt +on the same `knob_deck` + `curve_popup` grammar as the Sample face (FB1); `param_slider`'s linear +slider rows retired on the Zone panel — the `Knob` primitive is now the only live consumer of +that half of `param_slider`. Zone-authoring affordances (+Add Zone/Delete, per-zone piano-key +strip, Low/High/Root legend) preserved unchanged; VOICE and MASTER stay Sample-only +(per-instance). No new pure modules required. **This completes the r11 editor recomposition +(Phase S Wave B)** — S-VIEW-11/12/13 all landed, no open r11 or Wave B items remain in PLAN.md. + +### GA post-launch DAW-fix pass (2026-07-28) +Merged to dev; integrated suite 55/55 green. Six fixes found during GA DAW testing, not +PLAN-tracked. + +- **Preserve pitch engine rewritten to SOLA:** `pitch_shift`'s dual-tap OLA (taps hard-locked half + a window apart — fixed relative phase caused anti-phase cancellation on many source + frequencies, spectral garbage on repitched notes) replaced by **correlation-aligned SOLA + splices**: one active read tap chases the write head at the shift ratio, each splice jump + refined by a cross-correlation search so the new read point is waveform-aligned, then old and + new taps crossfaded with a raised-cosine amplitude-complementary fade. Clean pitch shift past + +24st; a repitched pure sine stays a single tone. +- **Voice takeover declick:** `Voice::start` gained a `takeoverDeclick` parameter (gated on the + envelope complement) removing the click at mono retrig/fallback and poly at-cap steal + boundaries. The "steal-all" DAW symptom was diagnosed as no-loop sample exhaustion under + Preserve (voices playing to silence, not being stolen) — the engine steals exactly one voice + per note-on as designed. +- **Stereo bus pin + channel-mode auto-default:** output bus made **permanently stereo** — the + dynamic mono↔stereo bus renegotiation (which hard-panned dual-mono) is deleted. `ChannelMode` + becomes decode-only (downmix vs dual-mono on WAV decode); channel mode auto-defaults from the + loaded capture's channel count via pure `channelModeFor()` unless the user has explicitly + toggled it (explicit toggle latches the preference). `ComponentState` **v8→v9**: + `channelModeExplicit` bool added; pre-v9 blobs treat the stored mode byte as an explicit + preference (no auto-override). +- **Drop-to-FX injection fix:** injection switched from `TrackFX_SetNamedConfigParm(..., + "vst_chunk", )` (silently unappliable for VST3 — REAPER's wrapper cannot accept + unframed component-state bytes) to a Steinberg-format `.vstpreset` file whose 'Comp' chunk is + the serialized component state, applied via `TrackFX_SetPreset`. FX hotspot now prefix-matches + `tcp.fx*`/`mcp.fx*`/`fx_*` (embed strip tokens excluded). `reasampler_uid.h` split out of + `reasampler_vst.h` as an SDK-free header so `instrument_drop` can derive the class-ID hex string + without the VST3 SDK. +- **Drag-out arm fix:** `SetCapture` moved to the drag-arm branch of `handleClick` in `bank_panel` + so a first straight-out drag (pointer leaving the client rect before a second click) correctly + arms and fires the OS drag-out. +- **Panel mode-toggle repaint:** the Design/Arrange mode-toggle actions now call + `bankPanelInvalidate()` after applying the mode switch, so the panel footer reflects a mode + change fired from the Actions list or a keybinding, not only a button click. + +### GA2 fix pass (2026-07-28) +Merged to dev; integrated suite 55/55 green. Two fixes found during GA DAW testing, not +PLAN-tracked. + +- **Preserve pitch engine — ring prime:** `pitch_shift` now primes its ring buffer with the + actual upcoming source at note-on (was zero-filled): gap-free frame-0 onset (the ~25ms Preserve + onset latency is gone — Preserve now speaks on frame 0, matching Varispeed), clean repitch + across the full C1–C8 range, and a real-content-bounded tail (the prior last-window + tail-truncation that clipped the decay is gone). +- **Takeover declick rev-2/3 — bounded blend, supersedes the GA fix:** the declick mechanism is + now a bounded blend (`out*(1-w) + ref*w`, w decaying from 1.0 over the blend window) applied at + every takeover boundary — mono retrig/fallback, poly at-cap steal, and **preview re-trigger**. + This supersedes the GA pass's `(1-amp)` envelope-complement gate, which zeroed the compensation + on Trigger and zero-attack restarts, leaving an audible click. The preview card is now covered + by the same mechanism. + +### Preview via real MIDI note path + self-contained playback (pS; 2026-07-28) +Merged to dev; integrated suite 55/55 green. Architecture corrections from DAW testing, not +PLAN-tracked. + +- **`PreviewCard` retired.** Preview now injects a synthetic note-on at the loaded capture's root + note into the main `VoiceEngine` (the same path host MIDI uses), so preview obeys + polyphony/mono/voice-stealing/envelopes. `sampler_core` no longer has a `PreviewCard`; the + processor no longer sums a separate preview voice. The unity-Varispeed-bypass demotion (added + in GA2 for the PreviewCard) is removed — GA2's primed shifter speaks on frame 0 anyway. +- **Self-contained playback.** ReaSampler 9000 no longer depends on the extension being loaded to + play. `ComponentState` bumped v9→**v10** with a `SampleRefs` table: per referenced sample, the + instance owns a project-relative path + decode intrinsics (root note, loop points, channels, + displayName) — NOT a copy of the audio. `reloadInstrument()` decodes directly from those refs + (bank-free — plays with the extension absent). The bank/bridge is now a browser source only: + loading a capture copies its reference into the instance's `SampleRefs`. The reopen-heal + apparatus (retry timer, editor-gated poll-to-play) is removed. Pre-v10 blobs lift to empty refs + and re-save self-contained; a bounded legacy lift covers migration. + +### Q-W0 fix-now remediations — closes Q-W0 (2026-07-28) +Merged to `phase-q` (commit `546927e`). Landed six Daniel-approved fix-now remediations from the +Q-W0 pre-restructure functional + DSP quality audit — T1-01 (linked-lag stereo SOLA + follower +self-heal fallback), T1-03 (playable-span prime bound), T1-09 (declick dead-state removal), +T2-01a (provenance wire-cursor hardening backport), T3-01 (rate-derived gain ramp), T3-03 +(rate-derived fade ceiling) — plus seven review riders. **Closes Q-W0 entirely** (audit, triage, +sign-off, and remediation all complete) and opens Q-W1. + +### Phase Q — 1.0 structural reorganization +Reorganized all of `src/` into `core/` (pure, subsystem-namespaced: `model`/`view`/`capture`/ +`audio`/`ui`/`reclaim`/`version`/`json`/`util`/`wire`/`instrument/{engine,map,ui}`), `shell/` +(REAPER/host-facing: `capture`/`panel`/`view`/`persist`/`actions`/`instrument`/`bank_ops`), and +`app/` (`main.cpp`), splitting several god-modules along the way under a soft ~600-line-per-TU +ceiling. Suite reached 60/60 at Q-W1, 61/61 for the remaining waves. + +#### Q-W1 — safe opener: `core/json` extraction + directory/namespace layout (2026-07-29) +Merged to `phase-q`, 60/60 green. Extracted pure `core/json` (`Reader`/`Writer`), deleting the +five hand-rolled JSON decoders (`bank_model`/`bank_book`/`view_mode_model`/`owned_manifest`/ +`tail_control`) — round-trip byte-identical to before. Collapsed the wire-`Cursor` family into +`core/wire`; added a shared `readFileBytes` helper; extracted `slot_map` from `bank_book`. +Relocated ~50 clean modules into the `core/{model,view,capture,audio,ui,reclaim,version,json, +util,wire}` / `core/instrument/{engine,map,ui}` / `shell/{capture,panel,view,persist,actions, +instrument}` layout; `main.cpp` moved to `app/`. Unified `ui::Rect` + `contains()` (the +XYWH-vs-LTRB fork retired, `footer_bar.h`'s "NAME NOTE" collision workaround gone); `clamp01` +deduplicated. Naming riders: survivor parser minted as `json::Reader`/`Writer`; `BankIndex` +renamed `BankModel`. `reasampler_uid.h` relocated to `core/wire/`. Interim `core/namespaces.h` +shim added for six not-yet-split god TUs (each downstream split wave retires its own includes). +**Riders explicitly skipped/deferred:** T4-22 (`hitIndex` hit-test template), T4-06 +(`view_mode_model` planner split), T4-09 (`view_lanes` split). **Open residual:** `bank_book.cpp` +still 737 LOC — the serialize/deserialize seam identified but blocked on a `nameKey` linkage +design decision, escalated to Daniel and pending as of 2026-07-29 (resolved in Q-W5). + +#### Q-W2 — split `bank_panel.cpp` (2026-07-29) +Merged (`pq-w2-panel`), 61/61 green, reviewed-approved. Split the largest extension god-module +(3459 LOC at the Q-W0 census, 8+ responsibilities) into eight TUs under `shell/panel/`: +`panel_render`, `panel_thumbnails`, `panel_audition` (direct call-through, never virtual — +preview idle path unchanged), `panel_input`, `panel_bank_ops`, `panel_window`, plus two new seams +(Q-5 settled reshape, Daniel 2026-07-28) `panel_layout` (toolbar/footer/menu rects + row/cluster +builders + region geometry) and `panel_drag` (the card-drag/hover state machine, mirroring the +pure `card_drag`) — without which `panel_render` (~700) and `panel_input` (~800) would have +shipped over the ceiling. `bank_panel.h` split alongside (Interface Segregation). +`panel_bank_ops` becomes the single home for bank-CRUD verbs that Q-W4 dedupes `actions.cpp` +against. **Recorded ceiling overages (reviewer-endorsed, comment-volume driven, non-comment lines +~322–369):** `panel_input.cpp` 636, `panel_render.cpp` 613, `panel_state.h` 608 — no honest seam +remained, bisection rejected. **Review note for Q-W4:** `panel_bank_ops`'s verbs still embedded +prompts/panel-state nudges; Q-W4's dedupe needed promptless inner verbs, not a call-site swap. +**DAW-smoke-tested and passing (Daniel, 2026-07-29); the full panel-parity verification batch +was not executed.** + +#### Q-W2v — split the VST god-modules (2026-07-29) +Merged (`pq-w2v-vst`), 61/61 green, reviewed-approved. Closed the audit's structural scope gap — +the VST artifact's god-modules had no owning wave. Split `reasampler_editor.cpp` (3065 LOC, +largest file in the repo) into eight TUs along the Sample/Browse/Zone face axis: `editor_session`, +`editor_controls`, `editor_layout` (pure-candidate hoist into the existing pure home, +`editor_geometry` — discharges T2-06's stranded-layout-math finding), `editor_paint_sample`, +`editor_paint_browse_zone`, `editor_input_sample`, `editor_input_browse_zone`, `editor_platform`. +Split `reasampler_processor.cpp` (1164 LOC) into `processor_state` / `processor_reload` / +lifecycle+`process()` (kept whole, no virtual seam added to the atomic-pointer-swap reload +pattern). Split `sample_map` into resolution core vs the `component_state_io` binary codec (+ +header split, T4-13 ≡ T2-07) — the extension's preset-blob path stops linking the whole voice +engine to serialize one blob. **`sampler_core.cpp` stays whole (968 LOC) — a documented, +deliberate exception to the ~600 ceiling** (per-voice-per-sample envelope ticks need same-TU +inlining to let the compiler inline the stack; no LTO in the build; a by-class split would blow +out the dispatch); its header splits into `zone_params.h` + `sampler_core.h`. The `core/wire` LE +byte-codec template (`putLE`/`readLE`) lands here with its biggest consumer. The `src/vst/` +directory is gone — all VST sources now live under `core/instrument/` and `shell/instrument/`. +**Deferred/known:** `component_state_io.h` still transitively includes `sample_map.h`→ +`sampler_core.h`; the `engine` namespace is deferred (`sampler_core` stays flat `reasampler`); +capture-side LE rewires left for the capture family. **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full editor/processor-parity +verification batch was not executed.** + +#### Q-W3 — split `main.cpp` (2026-07-29) +Merged (`pq-w3-main`), 61/61 green, reviewed-approved. Reduced `main.cpp` (1897 LOC at the Q-W0 +census) to API pointers + `ReaperPluginEntry` + dispatch by hoisting four TUs (T4-02 reshape, +settled with Q-5, Daniel 2026-07-28 — the planned three left `capture_orchestrator` over the +ceiling): `capture_orchestrator`, `capture_batch` (fourth hoist, landing `capture_orchestrator` +~450), `scope_resolve`, `realtime_lifecycle`. `FxBypassGuard` moved out but stays a stack RAII +object (precision-critical); the realtime idle tick stays a single pointer test. Q-W0 riders +landed in this wave: **`ICaptureBackend` deleted** (T4-26) — one deriver, zero polymorphic call +sites; `OfflineRenderBackend` becomes concrete; the CLAUDE.md/CONTEXT "two backends behind one +interface" description corrected in the same commit. Shared `stampCaptureSample` capture-epilogue +dedupe (T2-09); `makeUniqueTag` fixed with a per-session monotonic counter (T1-11 — same-second +batch captures previously collided silently). Naming rider (Q-9, settled — Daniel 2026-07-28): +pure module takes the stem `capture_realtime`, shell takes the suffix (mirroring +`drag_out`/`drag_out_win`) — renamed from `realtime_record`; `capture_realtime_finalize` split in +the same surgery. WAV/RIFF consolidation rider (audit §4e, settled): one pure `wav_codec` owner +(walker+layout+build+patch) absorbing `ingest.cpp`'s WAV/PCM build helpers (ingest drops to ~500, +gains a test target). `wav_codec_tests` replaces `wav_trim_tests`; `capture_realtime_tests` +replaces `realtime_record_tests`. **Known open:** `wav_trim.h`'s transitional forwarding shim +still has three live includers (`sample_map.h`, `editor_session.cpp`, `processor_reload.cpp`); +`ingest.cpp` trimmed to 567 LOC but keeps the `namespaces.h` shim. **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full +null-test/bit-identical-repeats/capture≠placement verification batch was not executed.** + +#### Q-W4 — split `actions.cpp` + dedupe bank verbs against `panel_bank_ops` (2026-07-29) +Merged (`pq-w4-actions`), 61/61 green, reviewed-approved. Split the two unrelated command-id +families in `actions.cpp` (1016 LOC at the Q-W0 census — T4-03, no reshape needed) into +`design_view_actions`, `bank_actions`, `prune_action` (keeping the `doBankPruneFolder` deletion +authority contract intact, routing to `persist`'s `prune_fs` after Q-W5). Deduped +`actions.cpp`'s own `promptText`/`mintBankId` and bank verbs against the Q-W2 `panel_bank_ops` +single owner — bank verbs reshaped to **promptless inner verbs** (one mutation home, two UX +skins: panel and actions each keep their exact prior UX); `promptText` renamed +`promptBankName`; `persistBankOp`/`persistBook` gain null-session guards. `prune_action` verified +a clean deletion-authority isolate (no `Undo_*`, no ext-state writes). Command-id +suffixes/display phrases verified byte-identical in review (FOREVER-STABLE). **Review 🟡 +(resolved in Q-W6):** two session pointers / a null-session-as-model-rejection misreport +(unreachable today). **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full per-wave +verification batch was not executed.** + +#### Q-W5 — split `persist.cpp` (isolate the single file-deletion authority into `prune_fs`) (2026-07-29) +Merged (`pq-w5-persist`), 61/61 green, reviewed-approved. Split `persist.cpp` (852 LOC at the +Q-W0 census, 5 responsibilities) into `session` (lifecycle+poll, `BeginLoadProjectState` reload +hook), `ext_state_io` (ext-state ↔ JSON serialization bridge + GUID minting + folder relocation), +and **`prune_fs`** (prune scanning + `deleteOrphanFile` via `SHFileOperationW`) — the split +**concentrates** the byte-deleting authority into one obvious module (verified tree-wide as +exactly one anonymous-namespace function), never spreading it. Q-W0 rider (T2-04, settled): +generalized the `GetProjExtState` grow-loop retry policy into a header-only template, rewiring all +three hand-rolled copies (`usage_scan`'s start cap raised 4KB→64KB, allocation-only, verified +equivalent; the grow-loop gains a defensive NUL). **Resolves the Q-W1 open residual:** the +`bank_book_json` split lands via a private static `nameKey` (Daniel-approved option a) — +`bank_book.cpp` is now ~462 LOC. `persist.h` kept as a compat umbrella across the in-flight waves +(retired in Q-W6). **DAW-smoke-tested and passing (Daniel, 2026-07-29); the full +save/load/undo-reload/relocation/prune verification batch was not executed.** + +#### Q-W6 — OCP registration table + residual fat-header (I) splits (2026-07-29) +Merged to `phase-q`, 61/61 green, reviewed-approved. Replaced the ~350-line hand-written +non-table action registration blocks (isolated in `app/main.cpp` after Q-W3) with a data-driven +`ActionTableRow` registration table (flat function-pointer dispatch, no `std::function`/virtual); +unload mirror-unregisters from the same table; `main.cpp` shrinks 653→404. Capture rows derive +their suffix+phrase from the pure `captureActionTable()` (parallel-list risk gone by +construction). FOREVER-STABLE suffixes/phrases/retired-ids verified byte-identical row-by-row in +review. Split residual fat headers: `persist.h` umbrella retired (13 callers repointed); +`capture.h`'s realtime seam moved to `capture_realtime_shell.h`; the `wav_trim.h` shim + its +INTERFACE target deleted. Phase-end cleanup riders: `bankOp*` verbs + `persistBankOp` lifted to +new `shell/bank_ops` taking `ReaSamplerSession&` (dissolves the Q-W4 🟡 review note); **`core/ +namespaces.h` DELETED** (the interim Q-W1 shim's contract fulfilled, ~26 includers rewired); the +grow-loop rehomed to `core/wire/ext_state_read.h`; a stale-comment sweep +(`persist.cpp`/`bank_panel.cpp` refs); CLAUDE.md's persist/bank_book/actions/wav_codec bullets +corrected in-wave. **Review-noted follow-on, not landed:** extending the table pattern to the +design_view/bank/ingest families' hand-registration; `view_mode_model.h` (748 LOC) remains the +largest header, its planner split stays optional/deferred. **DAW-smoke-tested and passing (Daniel, +2026-07-29); the full per-wave verification batch was not executed.** + +**All seven Phase Q waves (Q-W0 through Q-W6) are recorded as structurally complete on +`phase-q`.** Every wave from Q-W2 onward originally carried the same recurring caveat verbatim: +in-DAW behavioral-parity verification checked off as PENDING/deferred by design. **Daniel has +since DAW-smoke-tested Phase Q and confirmed it passing (2026-07-29); the full per-wave +verification batch described above was not executed.** + +--- + +## Part 4 — The version-0 plan of record + +Distilled from `PLAN.md` (the tracking checklist itself, not `COMPLETED.md`) at the 0→1 roll. +Parts 1–3 above narrate what landed, drawn from `COMPLETED.md`; Part 4 preserves the plan-of-record +layer — open questions, forks, and settled/recommended decisions — as it stood in `PLAN.md`, +including entries that were still live deferrals rather than finished history at roll time (see +the roll report for the full enumeration of those). + +### Open questions carried from CONTEXT.md (still open at the 1.0 roll) +- **`parseInt` narrowing hardening.** `bank_model.cpp`'s `parseInt` casts `int64_t → int` via + `static_cast` without a range check; integers that fit in int64 but exceed `INT_MAX` are + implementation-defined. Hardening candidate — add a bounds check before the cast when + integer-field validation is next in scope. +- **Capture send/routing isolation.** The FX-scope capture neutralizes out-of-scope FX, gain, and + pan, but NOT aux **sends** — a downstream coloring send (e.g. folder → reverb track) still + routes and blends into an item/track capture past the intended isolation point. The hard part: + distinguishing source routing that must be preserved (a MIDI send whose destination synth IS an + item's true audio source) from coloring sends that must be excluded (folder → reverb). Repro + case and a likely snapshot/mute-sends approach are recorded in `PLAN.md`; no fix has landed. + +### Phase D2 — two-canvas closure +D2 (item-level mode projection, additive to D1) is functionally complete — D2-W1, D2-W2, D2-W3-A, +D2-W3-B all landed. One item is explicitly **deferred, not dropped**: a per-track lane/mode-state +panel indicator. No natural cheap home was found in the bank panel; the mode switch already shows +the active mode. Can be picked up later if wanted. + +### Phase S — MIDI-playback instrument (ReaSampler 9000) +Landed on dev 2026-07-27 (S1–S18 plus the product-name/binary-rename work); `PLAN.md` records DAW +verification as pending Daniel's smoke test at that point. The then-authoritative spec was +CONTEXT.md §MIDI-playback instrument (Phase S); product framing `docs/product/midi-playback.md`. + +**S13 — cross-artifact ingest relay: DEFERRED, not built.** Spike verdict (ps-w12, 2026-07-27): +DEGRADED. The instrument's REAPER bridge (`reaper_bridge`) is deliberately read-only; a relay +would need a new instrument WRITE seam into ext-state plus an extension-side timer poller +servicing a drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race +the S17 spec rejected for alternative (A). Both the read-only-instrument boundary and the new +poller are load-bearing design calls, so the relay is deferred to a future wave; the shipped +ingest gesture stays drop-onto-docked-panel (S8). The editor's degrade-path affordance ("drop +files onto the ReaSampler bank panel to add them") landed as part of Phase S. + +**Product name.** ReaSampler 9000 (Daniel, 2026-07-26, on DAW-testing the S1–S6 instrument). The +extension stays ReaSampler. + +**Compat verification — must-DAW-verify before shipping the rename, unchecked in `PLAN.md`.** The +working assumption is that REAPER rebinds a saved instance by VST3 class UID, not module filename, +so renaming the module with an unchanged UID keeps saved projects working. This was **not +confirmed from source** — a web check surfaced a JUCE/VST3-replace-VST2 case suggesting the +binding may be more nuanced (possible FXID involvement) — treat as to-verify, not asserted fact. +Required DAW check: save a project with a ReaSampler 9000 instance under the old filename, rename +the module, reopen, confirm rebind + state restore. Fallback if REAPER keys partly on filename: +keep the current filename (display-strings-only rename). + +**Held and optional-forever (noted, not specified — no PLAN points drawn up):** +- Tier 2 "expressive" (HELD) — velocity layers, round-robin, full ADSR, per-sample tuning/gain + trim, sustain loops; the next depth increment once Tier 0–1 proves the instrument belongs. +- Tier 3 "instrument polish" (optional-forever) — filters, filter/pitch envelopes, LFOs, per-voice + pan, choke groups, a modest FX slot; a direction to leave room for, never a commitment. S16 + landed the *pitch* envelope + Varispeed/Preserve pitch-engine mode early (Daniel's directive), + so the Tier-3 line now means the *filter* envelope + LFO remainder. +- Sinc Varispeed-quality upgrade (HELD — WDL_Resampler) — beats the 2-point linear interp for + Varispeed base-repitch quality; an optional per-voice quality toggle, RT-suitable but heavier; + a Varispeed-quality option only, never a Preserve engine. +- WDL_SimplePitchShifter swap (HELD — fork S16-F2 route a) — drop-in swap for `pitch_shift` if the + hand-rolled OLA onset latency or warble proves musically unacceptable; same + `PitchEngine::Preserve` contract. WDL excluded from the shipped build by include-chain + (windows.h). +- Trigger choke-on-note-off (HELD — fork S15-F1) — a future option for Trigger mode to cut on + note-off or a same-group re-trigger (hi-hat open/closed); deliberately out of S15 scope. + +**Editor view-model redesign (S-VIEW, three views: Sample / Browse / Zone) — landed.** +Re-partitioned the editor from a two-view toggle into a three-view model with the loaded sample as +home (Sample default face, Browse a modal picker over Sample, Zone a dedicated keymap surface); +added key-tracking, preview velocity, and the r10 velocity→amp transfer curve, plus an envelope +overlay, real piano-key pattern, and velocity-curve editor components. Built directly on the +post-L3 look-and-feel as its baseline — no separate restyle-after pass. All r9/r10 forks settled +2026-07-27: S-VIEW-F1 (preview velocity persists via envelope-v6 `ComponentState`) and S-VIEW-F2 +(envelope nodes draggable via `envelope_edit`) folded into S-VIEW-4/S-VIEW-3; R10-F1 (flat y=1 +default) folded into S-VIEW-9; S-VIEW-F3 (full-window overlay) implemented as Browse rendering as +a full-window modal over Sample. + +**Wave B — Sample-face recomposition (r11, 2026-07-27) — landed.** All linear sliders replaced by +radial knobs in a fenced knob deck; mode toggles compact, not full-width; the inline +velocity-curve box replaced by a miniature curve-preview button + full-size popup (right-click +deletes a node); hero waveform full-width. S-VIEW-11/12/13 landed as FB1 (knob deck + master gain ++ curve popup + full-width hero, merged 2026-07-27, suite 55/55) and FB2 (Zone-panel parity, +merged 2026-07-28, suite 55/55). Phase S editor Wave B (r11) is recorded complete in `PLAN.md`. +Forks settled: R11-F1 (hero height vs. default window) at FB1 build — elastic hero, 840×620 +default kept; R11-F2 (Zone-panel parity) at FB2 build — knob deck + curve popup adopted on the +Zone panel, `param_slider` slider rows retired there. + +### Phase Q — Quality (1.0 structural reorganization): the decision record +(The wave-by-wave landing narrative is recorded in Part 3 above, distilled from `COMPLETED.md`. +What follows is `PLAN.md`'s own decision/fork record — the settled and recommended calls that +shaped the reorg — kept here because it does not otherwise survive verbatim in the landed-narrative +form.) + +Phase Q was named the last structural pillar — namespaced `Q` (Quality; M/D/B/R/V/S/L all already +taken) — and framed as a pure structural refactor: no feature, no behavior change, the test suite +passing unchanged as the proof of correctness. Product framing, the Vital-grounded target shape, +the grep-verified SOLID audit, and the fork record (Q-1..Q-6) live in +`docs/product/code-organization.md`. + +**The gate.** Phase Q was gated on the tree being otherwise quiescent — Daniel's plain readiness +target: "when Phase S and L3 are finished." Satisfied 2026-07-27: Phase S merged; Phase L complete +(L1–L7); D2 functionally complete with its one deferred item (per-track lane indicator) explicitly +not blocking; M9 (slots) abandoned 2026-07-27 (Daniel) and will not be reactivated — named in the +gate only so that reactivating D2's deferred indicator would re-arm quiescence. + +**Q-W0 (pre-restructure functional + DSP audit) ran first**, per Daniel's ask, complementary to +the grep-verified SOLID/naming audit already grounding Q-W1..Q-W6. Four parallel tracks (T1 DSP, +T2 architecture, T3 env-coupled constants, T4 sizing/placement) produced 59 findings +(`docs/product/code-quality-audit.md` + `docs/product/audit-notes/`); Daniel approved every +disposition 2026-07-28. The Q-11 question (pitch-technique replacement) was answered by the audit +itself: the SOLA pitch engine is sound — no technique replacement warranted; every pitch finding is +a bounded in-technique fix or a documented operating limit. + +**Settled decisions:** Q-1 phase name/id family (`Q`, `Q-W0..Q-W6` + `Q-W2v`) settled this-doc. +Q-10 audit-report home = a committed doc (`docs/product/code-quality-audit.md`), not a tracked +issue list. Q-11 = defer to findings (a bounded OLA fix weighed before a technique replacement, +which would be a Daniel decision at triage time, not automatic). Q-5 settled to seams-by- +responsibility with the T4 seam lists adopted (`bank_panel` 6→8 seams, `capture_orchestrator` +further split with `capture_batch`); the ~600-line ceiling is an acceptance criterion on every +split wave, not a bisection target. Q-6 settled in scope, last wave. Q-8 settled both renames +(`BankIndex`→`BankModel`; JSON parser → `json::Reader`/`Writer`) plus, from the audit, +`ICaptureBackend` deleted in Q-W3 (one deriver, zero polymorphic call sites — the CLAUDE.md/ +CONTEXT correction rides Q-W3 itself). Q-9 settled: align to stem `capture_realtime`, shell +suffixed, during W3. VST placement (audit §4a) settled: `core/instrument/{engine,map,ui}` + +`shell/instrument/` under the single `core/`/`shell/` top split. WAV/RIFF consolidation +(audit §4e) settled as a Q-W3 rider: one pure `wav_codec` owner (walker+layout+build+patch). +Q-W2v scheduling (audit §4f) settled: parallel with Q-W2 (different artifact, zero file overlap). + +**Recommended-and-adopted (Q-2..Q-9, `docs/product/code-organization.md` §6):** Q-2 JSON +extraction in scope and first; Q-3 `core/`/`shell/`/`app/` top-split with subsystem dirs beneath +(over pure-Vital subsystem-first — makes the pure/shell invariant structural); Q-4 sub-namespace +matches sub-directory; Q-7 naming rides the relocation waves, no dedicated naming wave (forced +once Q-3/Q-4 settle). + +**Hard constraint — zero runtime cost.** No added virtual dispatch, no header→TU indirection, no +changed call/inline or branch shape on the three hot paths: `peaks` envelope compute, +audition/preview, the realtime-capture idle tick. An acceptance criterion on every point. + +**Structural heuristics (acceptance criteria on every wave):** (1) more directories a must, more +files good, ~600-line ceiling as the bar with the audit's named seams as method — a documented +hot-path exception (`sampler_core.cpp`) is legitimate, silent overshoot is not; (2) templates +earned for compile-time dedup (the LE byte codec) but not for name-only unification (the rect +family stays one concrete `ui::Rect`, no template); (3) SOLID is great but saved CPU is better — +no dispatch-stack blowouts anywhere, prefer static polymorphism where types are compile-time-known. + +**Sequencing (as planned; all landed 2026-07-29):** GATE → Q-W0 → Q-W1 (safe opener) → +{Q-W2 → Q-W4; Q-W2v parallel with Q-W2 (zero file overlap); Q-W3 → Q-W6; Q-W5 best after Q-W4}. +Big-bang was rejected; every wave independently landable and CTest-green throughout. + +**Must-verify-before-build checklist** (all satisfied by the 2026-07-29 landing): Q-W0 closed +before any structural point (triage + Daniel sign-off on every disposition); ~600-line ceiling per +split TU with `sampler_core.cpp` the sole documented exception; no dispatch-stack blowouts +anywhere; hot-path call/inline shape unchanged (`computeEnvelope` stays free-function, audition +stays direct call-through, idle tick stays a single pointer test); command-id/display-string/ +ext-state-namespace/VST3-UID contracts left byte-identical by any rename; name-collision sweep +resolved before W1; naming changes zero-behavior-change and off the wire; the GATE re-confirmed +against dev before W1. + +--- + +## Part 5 — Spec provenance (pre-build reasoning) + +Distilled from `CONTEXT-ARCHIVE.md` ("ReaSampler spec provenance", 1230 lines) ahead of its +deletion. That file held build-detail sections moved verbatim out of `CONTEXT.md` once each +phase landed, in original document order — each section was the unedited original text, kept +for provenance rather than as a living reference. Most of it was pre-implementation scaffolding +whose value fully expired once the code shipped, so it is summarized rather than transcribed +here: recurring per-phase **"Module architecture (preserve the pure/shell split)"** sketches +(superseded by the nineteen per-directory `CLAUDE.md` files the shipped tree now carries), +recurring **"REAPER API surface (verify all signatures)"** / **"REAPER / Steinberg API surface"** +/ **"LICE / WDL API surface"** checklists (superseded by root `CLAUDE.md`'s standing instruction +to verify every cited API name against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`), a +**"Data model (sketch — refine in code)"** for the original single-artifact tool (the shipped +`Sample`/`BankModel` shapes are the truth now), and a numbered **"Build order"** for that same +original tool. These appeared, in the same shape, ahead of the Phase B (original capture tool), +D1/D2 (Design View), M (multi-bank), R (removal-and-prune), and D3 (VST3 spike) build-detail +sections, plus a "Kit architecture (the pure/shell split)" sketch ahead of Phase L — all dropped +for the same reason. What follows preserves only the design reasoning and settled constraints +that do not survive verbatim in `CLAUDE.md`, `CONTEXT.md`, or Parts 1–4 above. + +### WDL pitch/resample surface — the S16 viability assessment +A prior sweep had dismissed `WDL_SimplePitchShifter` (`vendor/WDL/WDL/simple_pitchshift.h`) as +"wrong tool" because it is duration-preserving — but Daniel's directive made duration-preserving +*the requirement* for the Preserve pitch mode, so the dismissal was replaced with a real +viability read of the header. Findings: the vendored WDL tree's *entire* pitch/resample surface +is exactly two headers, `resample.h` (`WDL_Resampler`, a sinc/linear resampler — held as an +optional Varispeed-quality upgrade only, since a resampler couples duration) and +`simple_pitchshift.h` (`WDL_SimplePitchShifter`, time-domain OLA, duration-preserving — the S16 +Preserve-engine candidate under fork S16-F2 route (a)); there is no elastique-class +formant-preserving shifter anywhere in the tree, and REAPER's own elastique is a separate +zplane license unavailable to the project without a new third-party dependency (JUCE/rubberband/ +signalsmith), which was not proposed. `WDL_SimplePitchShifter` was assessed RT-viable *with a +pre-warm discipline* (run silence through once at voice-allocation so its OLA ring and queue +reach steady state before the audio thread ever calls it) but with a real cost: an inherent +~half-window onset latency (~25 ms at a 50 ms window) and basic ("SimpleWindowed"-class) quality +with audible warble on large transpositions and no formant preservation. The recommendation was +route (a) (`WDL_SimplePitchShifter`, low-cost) or route (b) (a hand-rolled pure `pitch_shift` +module) as the Preserve engine, with the pitch-envelope modulation hand-rolled over whichever +engine won. **What shipped:** a hand-rolled correlation-aligned SOLA `pitch_shift` (route (b)) — +see `CLAUDE.md`'s GA post-launch DAW-fix pass — with the WDL swap recorded as a still-HELD +fallback in `PLAN.md`'s carried-forward list (Part 4 above) should the hand-rolled engine's +latency/warble ever prove unacceptable. + +### ReaSampler 9000 — the UX overhaul (S10–S13) findings +Daniel DAW-tested the S1–S6 instrument and the verdict was that it *worked* but the UX was +unacceptable — "this is supposed to be better than ReaSamplOMatic5000" (RS5K). The S1–S6 editor +was spike-grade: a clickable sample list, zone rows each carrying seven tiny ±1 nudge/delete +mini-buttons, text-only labels, no keyboard visualization, no waveform, no drag interaction, no +scrolling for long lists — setting a zone from C1 to C4 by clicking "+" thirty-six times was the +headline catastrophe. The overhaul was then **reframed around the actual workflow, not a +keymap** (a revision superseding the original keymap-first S10 plan): most instances play a +*single* capture, so the metric became **time-to-first-note**, not zone-table completeness. The +settled hierarchy: (1) primary flow is one capture, fast; (2) a **fresh instance is silent** — +nothing auto-selected, no auto-play of sample #1 (a deliberate reversal of the earlier S4 +"first sample plays" convenience, recorded as a reversal, not a regression); (3) a **capture +browser** (scannable cards with peak thumbnails, name, root/key badge, bank-filterable) replaces +the "giant list of item blocks" anti-pattern; (4) graphic, descriptive controls with a guided +single-capture setup fast path; (5) multi-zone keymap editing is **demoted to an opt-in "Zones" +panel** — "most of the time the zones won't be used." **What "better than RS5K" meant, +concretely:** match RS5K's genuine strengths (drag-a-file-on-it loading, a visual note-range +control, a draggable waveform with loop markers, ADSR sliders) while exploiting its real +weaknesses — RS5K is one-sample-per-instance (forces track sprawl) and has no multi-zone view in +a single instance, where ReaSampler 9000 is multi-zone-in-one-instrument by design with an +opt-in Zones panel RS5K structurally lacks. **Sequencing rationale:** S10 (the browser + guided +setup + silent-by-default fix) was recommended first because it carries the *entire* felt UX +wound and the time-to-first-note metric, ahead of both S11/S12 (which lean on S10's browser) and +ahead of S7 (stereo capture) — "it works, the UX is awful" points at the editor as the live +wound, not the engine, even though no hard dependency runs either way. + +### Drop-and-load — dragging a capture onto a track's FX button (S17 spec) +The gesture: while a capture is dragged out of the bank panel, a track's TCP FX button becomes a +drop zone; dropping there instantiates a ReaSampler 9000 on that track with the dragged capture +already loaded and selected — placement-of-the-player, the third integration gesture alongside +capture and placement-into-arrange. It needed a genuinely new drag mode because the existing +`CF_HDROP` OS-drag path (M11) cannot carry it — REAPER's FX button is not a native +drop-target-that-instantiates-a-plugin-with-a-file — so the extension itself has to hover-track +the pointer over REAPER's own UI and drive the insert. The `drag_out` pure module was to gain a +third `DragGesture`, `InstrumentDrop`, disambiguated from `Internal` and `OsDrag` purely by +pointer position plus a shell-supplied "is this REAPER's own UI" predicate — mirroring how the +existing gesture decision stayed pure over a shell-supplied rect. **The load-capture seam was +the hard, load-bearing part:** the Phase S spec at the time gave the instrument only a read seam +over the bank, with no entry point for an external actor to say "load *this* capture." Two +mechanisms were weighed: **(A, rejected)** a fresh-instance ext-state handshake (a "pending load" +hint keyed to the target track/FX, claimed and cleared by the new instance on init) — rejected +because the claim/clear race needs a cross-process handshake to get right; **(B, settled)** +direct VST3 component-state injection immediately after `TrackFX_AddByName` returns the new FX +index, writing the instance's own serialized chunk (with the capture pre-selected) via +`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", )` — deterministic, no shared +state, no handshake, at the cost of making the component-state blob format a hard cross-artifact +contract between the extension and the instrument. **Deviation from the landed code:** per root +`CLAUDE.md`'s `instrument_drop_win` entry, the shipped mechanism is not the spec's settled (B) — +the `TrackFX_SetNamedConfigParm` "vst_chunk" write was found **silently unappliable for VST3** at +build time, so the shipped shell instead applies the dragged capture's state via a transient +`.vstpreset` image + `TrackFX_SetPreset`, all-or-nothing with a `TrackFX_Delete` rollback on +failure. The gesture-disambiguation contract (position, not a mode toggle; M11's OS-drag and the +internal bank-to-bank drag byte-for-byte unchanged) and the invariant framing (a deliberate +placement act, no auto-capture, no timeline insert, no private sample copy — the instrument +reads the one authoritative bank by reference) carried through unchanged. + +### L2/L4/L5 dock-panel layout contracts — the reasoning +**L2** was scoped as a *layout design*, not a skin pass, because M11 added a real button +inventory (action-trigger buttons + keybinding-help labels) that had to be placed without +crowding the grid — the mandate was to group by *task* (capture / organize / reclaim / view), +not by build phase, and to sequence L2 after M11 merged so it inventoried the buttons actually +landed. **L4** re-homed that inventory around *frequency and intent*, shipping no new action and +changing no capture/placement behavior: capture + placement + maintenance moved to a **top** +toolbar (the eye's first landing, matching the tool's purpose); Design View tagging/switching +took the vacated **bottom** toolbar; the footer got a narrowed `[Arrange|Design]` toggle, a +proper Tail button, and Prune — ordered left (benign/frequent: view toggle, tail length) to right +(destructive/rare: Prune, isolated and `warn`-colored) so a mis-click near the left is cheap and +the one destructive control stays spatially and chromatically distinct. **L5** was a third +refinement pass, again shipping no new action: the rarer capture variants (Batch Items/Razor, +Capture RT, Cancel RT) moved into a right-anchored "⋯ More" overflow menu; button *faces* stayed +short labels with a hover tooltip carrying the full action name (`ReaSampler:` prefix stripped +for display, not for the registered gaccel name) and its keybinding; the Tag Design/Untag pair +became **four opposite-mode-only** buttons (Item/Track × Arrange/Design) over the *already +existing* `doTag`/`doUntag`/`doMoveItems` action families — pure layout + enablement wiring, not +new feature work — with a pure enablement rule (a button is live iff its target mode differs +from the active mode, read from the same source the footer toggle uses); `VIEW_TOGGLE_MODE`, +`VIEW_ACTIVATE_ARRANGE`, and `VIEW_ACTIVATE_DESIGN` were dropped from the bottom toolbar (actions +stay registered, FOREVER-STABLE ids unchanged) since the footer toggle already covers switching, +while `VIEW_SHOW_BOTH` was kept as the cross-mode "pin visible everywhere" escape hatch the +toggle doesn't cover. + +### The L3 gate + Phase S coordination contract +The originally-anticipated coordination contract had two branches for how the VST3 editor/embed +shells would pick up the L1 kit: "born in the kit" (built against it from the start) or "L3 +restyles them" (built plain, then brought onto the kit). In practice Phase S's editor and embed +shells arrived on `dev` drawing flat `LICE_FillRect` blocks and raw GDI `DrawTextA` off a local +pre-L1 forest-green palette — the "born in the kit" branch did not occur — so L3 performed a full +restyle of both shells through the L1 kit instead, landing the settled-and-revised +grey-neutral-plus-three-accent-pastel treatment (with a pastel spectral keyboard strip as the +signature surface) and routing text through the kit's cached-font `text()`. This resolved the +Phase Q gate condition ("Phase S + L3 merged to dev") and closed out Phase L (L1–L7 complete). +The VST3 class UID was left unchanged — a visual restyle is explicitly not a compat event. + +### Open questions the source left unresolved +Two items were still open at the point their sections were written and are not known to have +been revisited since: **multi-capture drag over an FX button** (S17) — reject, or load the +first / a keymap of all — and whether the drop zone has to be the **FX button specifically** vs. +anywhere on the target track's TCP. Neither is known to block anything currently planned; noted +here rather than silently dropped. diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md new file mode 100644 index 0000000..62b1244 --- /dev/null +++ b/docs/COMPLETED.md @@ -0,0 +1,8 @@ +# COMPLETED.md — ReaSampler landed milestones + +Completed milestone entries removed from `PLAN.md`. Each entry preserves its +original Goal, Verify, and checklist points with boxes marked done. + +This file holds the current (1.x) cycle's landed milestones only. Nothing has +landed in the 1.x cycle yet. For all pre-1.0 (version-0) history, see +`docs/ARCHIVE.md`. diff --git a/TODO-1.0.md b/docs/TODO-1.0.md similarity index 100% rename from TODO-1.0.md rename to docs/TODO-1.0.md diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..dde6c00 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,133 @@ +# TODO + +Forward-looking follow-ups. Deferred by decision, not oversight — each entry records why it was deferred and what "done" looks like. + +## Persist ReaSampler 9000 instance identity to let prune reclaim de-referenced captures after reopen + +**Context (what shipped — Phase S usage-detection).** Each ReaSampler 9000 instance publishes the captures it holds to project ext-state (`rsusage_` keys, ComponentState v11). The extension's prune reads those records and unions every live instance's held captures into the referenced-set, so a capture any live instance holds can never be pruned. Fail-safe: unreadable/ambiguous usage state aborts prune (deletes nothing). Airtight on safety. + +**The wart.** The per-instance identity token is minted fresh each incarnation and is NOT persisted. After save→reopen, an instance cannot recognize its OWN prior-session usage record — it looks foreign, so the instance defensively unions and marks the record append-only (poisoned). Net effect: after any reopen, prune stops reclaiming captures an instance once loaded but no longer uses. Safe (never deletes a used capture), but the bank folder grows without bound. + +**Intended fix.** Persist the instance identity in ComponentState so an instance recognizes its own last-session record and does a clean-replace instead of union/poison → prune reclaims de-referenced instance-touched captures normally. + +**The constraint the fix MUST handle (why deferred).** VST3 provides no stable per-instance identity, and Ctrl+D / in-place FX duplication clones plugin state. A persisted identity is inherited by an in-place duplicate → two live instances in one project share one `rsusage_` key. Harmless while both hold the same capture; the risk is a divergent clone — the copies load DIFFERENT captures, and last-writer-wins drops the other's held capture from the record, exposing it to prune. The fix must detect a genuine live same-identity collision and protect the union in that case, WITHOUT reintroducing the sibling-drop bug the fresh-per-session token was originally added to prevent. (Whole-project copies are a non-issue — bank files are cloned with the project and ext-state is per-project.) + +**Priority / risk.** Low / deferred. Current behavior is safe; the only cost is unbounded bank-folder growth after reopens. Decided 2026-07-28 to ship the safe version and defer this. + +**Done looks like.** Save → reopen → de-reference a capture from an instance → prune reclaims it. And: in-place-duplicate + diverge + delete-from-bank never deletes a capture a live instance holds. + +## Isolate capture from out-of-scope aux/parallel sends, not just FX/gain/pan + +**Context (carried from PLAN.md's "Open questions to resolve during build").** The FX-scope capture neutralizes out-of-scope FX, gain, and pan for both item and track scope — root `CLAUDE.md`'s "Capture FX scope" precision invariant states this precisely: the out-of-scope chain (ancestors + master track, plus the item's own track for item scope) has its FX, gain, and pan/width/pan-law/mode neutralized to unity. **Aux/parallel sends are conspicuously absent from that enumerated list** — the invariant as currently written does not cover them, which is the gap this item exists to close. + +**The wart.** A downstream coloring send (e.g. a folder → reverb-track send) still routes and blends into an item/track capture, past the intended isolation point. Repro from PLAN.md: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, but should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded. + +**Intended fix.** Likely approach (PLAN.md): snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path. + +**The constraint the fix MUST handle.** Distinguish **source routing that must be preserved** (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from **coloring sends that must be excluded** (folder→reverb). PLAN.md notes this is "the hard part" and that a rule is needed for which sends are load-bearing. + +**Settled rule (Daniel, 2026-07-29).** The classification rule for which sends are load-bearing: (1) **ancestor sends are excluded** from the capture — the folder parent is *up* the folder tree but *downstream* in signal flow, so this item deliberately says "ancestor," not "upstream," which would read backwards to anyone applying signal-flow convention; (2) **sibling sends are preserved and captured** — a sibling is a track sharing the capture scope's parent; (3) **if the send's destination has a different parent, ignore it in the capture** — it isn't a sibling, so it isn't captured. Applied to the repro above: the T1→T2 MIDI send is a sibling send (T1 and T2 share parent F1) and is preserved — T2's synth is where the item's audio is actually produced; the F1→T3 reverb send is an ancestor send and is excluded. This resolves the repro. + +**Still open.** The rule above settles *which sends to mute*, but the repro is only fully resolved together with a second, unspecified lever — *where the capture taps*. PLAN.md's own framing of this open question says a true item-level capture should be taken "at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan and before out-of-scope aux/parallel sends." How the mute-rule above and the tap-point interact is not yet specified. + +**Priority / risk.** Not stated in PLAN.md (recorded there as an open `(TODO)` question, not yet triaged to a priority). Flagged here as mattering more than the other four carried-over items because it exposes a live gap in a stated precision invariant, not just a deferred feature. + +**Done looks like.** Capturing the MIDI item on T1 in the repro above is isolated to T2's synth output pre-F1 — the T1→T2 MIDI send is preserved and the F1→reverb send is excluded — and the CLAUDE.md "Capture FX scope" invariant's neutralization list is extended to name sends explicitly. + +## Confirm REAPER's VST3 UID-vs-filename instance rebind behavior (Phase S compat verification) + +**Context.** PLAN.md, under "Phase S — product name (ReaSampler 9000)": the working assumption is that REAPER rebinds a saved instance by its VST3 class UID, not by the module filename, so a filename rename with an unchanged UID keeps saved projects working (existing instances still resolve). + +**The wart.** This is not yet confirmed from source — PLAN.md records that a web check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's binding is more nuanced than "UID only" (it can involve an FXID match), so UID-rebind is to-verify, not asserted fact. + +**Intended fix / required check.** DAW-verify: save a project with a ReaSampler 9000 instance under the old filename, rename the module, reopen — confirm the instance rebinds and restores its state. + +**The constraint the fix MUST handle.** If REAPER does key partly on filename, the fallback is to keep the current filename (display-strings-only) and record that as the shipped choice. + +**Priority / risk.** Marked "must-DAW-verify before shipping the rename" in PLAN.md; no explicit priority level stated beyond that. + +**Done looks like.** The save→rename→reopen DAW check is performed and its result (rebind confirmed, or filename-revert fallback taken) is recorded. + +## S13 — cross-artifact ingest relay (deferred, spike verdict DEGRADED) + +**Context.** PLAN.md records the ps-w12 (2026-07-27) spike verdict: DEGRADED — relay deferred. The instrument's REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; a relay would need a new instrument WRITE seam into ext-state and an extension-side timer poller servicing a drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race the S17 spec rejected for alternative (A). The shipped ingest gesture stays drop-onto-docked-panel (S8); the editor shows a "drop files onto the ReaSampler bank panel to add them" affordance as the degrade path. + +**The wart.** Dropping a file directly onto the editor/instrument does not ingest it into the bank — only drop-onto-docked-panel does. + +**Intended fix.** The editor hands the dropped path + this instance's identity to the extension as a bank-ingest request over an agreed seam. + +**The constraint the fix MUST handle.** Requires (a) a new instrument WRITE seam into ext-state (breaking the current read-only-bridge invariant) and (b) an extension-side timer poller + claim/clear nonce — both are load-bearing design calls that need to be made deliberately, not as a call-site patch. Both the read-only-instrument boundary and the new poller were judged load-bearing enough that the relay is deferred to a future wave rather than pushed through now. + +**Priority / risk.** PLAN.md marks this DEFERRED, awaiting a future wave, with no priority assigned. + +**Done looks like.** Not stated in PLAN.md beyond "a future wave when the design is ready." + +## Phase D2 — per-track lane/mode-state panel indicator (deferred) + +**Context.** PLAN.md: Phase D2 is functionally complete (D2-W1, D2-W2, D2-W3-A, D2-W3-B all landed). One item was deferred out of that completion: a panel UI indicator for per-track lane/mode state (a per-track lane-split marker). + +**The wart.** The mode switch already shows the active mode, but there is no per-track indicator; PLAN.md records that no natural cheap home for one was found in the bank panel. + +**Intended fix.** Not specified in PLAN.md beyond the goal (a per-track lane-split marker in the bank panel) — the design is unresolved, which is part of why it was deferred rather than built. + +**The constraint the fix MUST handle.** Finding a home for the indicator in the bank panel's existing layout, which PLAN.md notes doesn't currently have a natural cheap spot for it. + +**Priority / risk.** Not stated in PLAN.md. PLAN.md's own framing: "Explicitly deferred — not silently dropped. Can be picked up later if wanted." + +**Done looks like.** Not stated in PLAN.md. + +## FX-GUID keying for `restoreFxOffline` (Design View park/restore) + +**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): the bulk of reconcile residuals shipped (`ViewModeModel::reconcile(liveGuids)` prunes orphaned snapshots on every toggle/load; folder restructure is self-healing because the tree is rebuilt each toggle; membership is intentionally kept so undo-delete preserves the tag). Two sub-items were left deferred out of that; this is the first. + +**The wart.** `restoreFxOffline` currently restores per-FX offline state by slot index. If the FX chain is reshuffled while a track is parked, restore lands on whatever plugin now occupies that slot rather than the plugin it was originally captured from. + +**Intended fix.** FX-GUID keying — key the per-FX offline snapshot entries by FX identity rather than slot index. + +**The constraint the fix MUST handle.** The keying change requires a snapshot-schema migration; CONTEXT.md names this alongside the keying change as the reason the fix was deferred rather than folded into the reconcile-residuals work. + +**Priority / risk.** Not stated in the source. + +**Done looks like.** Not stated in the source beyond the fix description above. + +## Dormant membership entries in persisted `view_state` + +**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section), the second of the two sub-items left deferred after the reconcile-residuals ship described above. + +**The wart.** Truly-deleted tracks accumulate stale entries in persisted `view_state`. + +**Intended fix.** A future user-initiated "compact" action to remove the stale entries. + +**The constraint the fix MUST handle.** Must NOT be automatic pruning — automatic pruning would reintroduce the undo-delete tag-loss that the deliberate membership-retention was designed to prevent. + +**Priority / risk.** Not stated as a priority level; the source characterizes the wart itself as "harmless and bounded." + +**Done looks like.** Not stated in the source beyond "a future user-initiated 'compact' action." + +## Confirm no fight between Design View flags and screenset recall + +**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): Design View drives the same track flags a screenset recall would drive, and last writer wins between the two. + +**The wart.** Not a defect — this is a verification task, not a code change. The open concern is confirming there is no surprising interaction between Design View's flag-driving and an active screenset recall. + +**Intended fix.** N/A — no fix is proposed; the task is to confirm no surprising fight between the two mechanisms. + +**The constraint the fix MUST handle.** N/A — verification only. + +**Priority / risk.** Not stated in the source. + +**Done looks like.** Not stated in the source beyond "confirm no surprising fight." + +## Active-bank indicator placement (B4 polish) + +**Context.** CONTEXT-ARCHIVE.md's "Open questions to resolve during build" (B4 panel section): forks 1–5 are all settled; one panel-polish detail remains open. Fork 4 already settled that the active-bank indicator must be "visually unmistakable" — only its placement is undecided. + +**The wart.** No placement chosen yet among three candidates: per-region headers, a single header readout, or a lit-tab treatment. + +**Intended fix.** Not stated in the source beyond the three candidate placements above — the choice among them is the open item. This is explicitly a panel-polish detail. + +**The constraint the fix MUST handle.** Not stated in the source. + +**Priority / risk.** Not stated as a priority level; the source characterizes this as a "panel-polish detail." + +**Done looks like.** Not stated in the source beyond choosing one of the three placement options. diff --git a/docs/product/audit-notes/q-w0-t2-architecture.md b/docs/product/audit-notes/q-w0-t2-architecture.md index efea37c..70190ba 100644 --- a/docs/product/audit-notes/q-w0-t2-architecture.md +++ b/docs/product/audit-notes/q-w0-t2-architecture.md @@ -10,7 +10,8 @@ flatness, naming families) are **not restated**; where a finding below touches t is because the functional mechanism is new. Every claim below was verified by grep/read of the actual tree. Line numbers are as of this -audit's snapshot. Wave assignments reference PLAN.md §Q-W1..Q-W6. +audit's snapshot. Wave assignments reference the Q-W1..Q-W6 waves (landed history in +`docs/ARCHIVE.md`; sequencing in `docs/product/code-organization.md` §5). --- diff --git a/docs/product/audit-notes/q-w0-t3-env-constants.md b/docs/product/audit-notes/q-w0-t3-env-constants.md index 993aa15..baa1ca5 100644 --- a/docs/product/audit-notes/q-w0-t3-env-constants.md +++ b/docs/product/audit-notes/q-w0-t3-env-constants.md @@ -2,9 +2,9 @@ Static analysis, 2026-07-28, branch `pq-w0-audit`. Scope: any value stored in an environment-coupled domain — frames, sample rate, DPI, pixels, tick cadence — that should be -stored **rate-free / device-free and resolved at the point of use** (PLAN.md §Q-W0 env-coupled -bullet; `docs/product/code-organization.md` §2c.3; the load-bearing `sample_map` seconds -invariant). Findings are domain-modeling calls, not "rescale by rate" patches. The judgment bar +stored **rate-free / device-free and resolved at the point of use** (`docs/product/code-organization.md` +§2c.3 env-coupled bullet; the load-bearing `sample_map` seconds invariant, documented in +`src/core/instrument/CLAUDE.md`). Findings are domain-modeling calls, not "rescale by rate" patches. The judgment bar applied: a finding requires (a) an env-coupled *stored* domain AND (b) an environment that can actually change under it. Frame counts computed transiently from seconds at the use site are correct and are not reported. @@ -63,7 +63,7 @@ fix-now findings in those files must be remediated in Q-W0 itself. normalize the Trigger fade-in/out knobs. - **Stored vs. correct domain:** the fade **storage** domain (int64 SOURCE frames, persisted in the zones payload) is settled and correct — a source-timeline fact, invariant under project- - rate change (PLAN.md §S15). The *UI ceiling*, however, encodes a wall-clock intent ("2-second + rate change (`docs/ARCHIVE.md` §S15). The *UI ceiling*, however, encodes a wall-clock intent ("2-second max fade throw") as a frame count at an assumed 44.1 kHz source. `88200` is a rate-derived literal in `src/`, brushing the no-hardcoded-rate ruling even though it never touches disk. - **What breaks when the environment shifts:** the environment here is the **source file's @@ -165,7 +165,7 @@ fix-now findings in those files must be remediated in Q-W0 itself. `SampleRefs` (paths + root/loop/channels intrinsics), `instanceGuid` — all rate-free or file-fact domains. Clean. - **Trigger `fadeInFrames`/`fadeOutFrames`/`startPoint`/`SampleLoop.start/end` persisted as - int64 SOURCE frames:** deliberate, settled source-timeline facts (PLAN.md §S15; + int64 SOURCE frames:** deliberate, settled source-timeline facts (`docs/ARCHIVE.md` §S15; `bank_model.h:66-72` documents the loop rationale) — frames *of the file* are invariant under project-rate change; the file's own rate is stored alongside and resolved at decode. Correct domain, not a finding. diff --git a/docs/product/capture-tail.md b/docs/product/capture-tail.md index 4a46843..2d5391a 100644 --- a/docs/product/capture-tail.md +++ b/docs/product/capture-tail.md @@ -1,19 +1,20 @@ # Capture tail — spec Authoritative spec for the **capture-tail** feature: preserving reverb/release -tails that decay past the end of a capture range. The tickable milestone lives in -`PLAN.md` (Milestone T); this doc holds the full technical detail **and** the -product framing. +tails that decay past the end of a capture range. The tickable milestone's landed +history is in `docs/ARCHIVE.md` (Milestone T); this doc holds the full technical +detail **and** the product framing. -> **Why this doc carries the technical spec (not `CONTEXT.md`).** Every other -> pillar (capture M0–M11, Design View, Multi-bank) keeps its authoritative -> technical spec as a `CONTEXT.md §` section and its *why* in a `docs/product/` -> note. Capture-tail is a rider on the already-shipped offline-render path -> (M3/M7), not a standalone pillar, and it is being specced without reopening -> `CONTEXT.md`. So the authoritative detail lands **here**, house-styled to match -> the CONTEXT specs; when the tail work lands, doc-keeper may fold the invariant -> deltas into `CONTEXT.md §Precision invariants` as landed history. Same standing -> discipline applies: **verify every REAPER API name/flag against +> **Why this doc carries the technical spec (not the architecture docs).** Every +> other pillar (capture M0–M11, Design View, Multi-bank) keeps its authoritative +> technical spec as a per-directory `src/**/CLAUDE.md` section and its *why* in a +> `docs/product/` note. Capture-tail is a rider on the already-shipped +> offline-render path (M3/M7), not a standalone pillar, and it was specced without +> reopening the architecture spec. So the authoritative detail lands **here**, +> house-styled to match those specs; the landed invariant deltas are folded into +> root `CLAUDE.md` §Precision invariants and `src/core/capture/CLAUDE.md` / +> `src/shell/capture/CLAUDE.md` as landed history. Same standing discipline +> applies: **verify every REAPER API name/flag against > `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use** — the flag values > below are transcribed from that header (line numbers cited) and are not guesses. @@ -252,7 +253,7 @@ held to bit-identical repeats. - **Opt-in beyond the region.** The tail only ever adds audio past the range end when a tail is explicitly requested (Auto or Manual). This is exactly the existing invariant: *"no added silence unless a tail is explicitly requested"* - (`CONTEXT.md §Precision invariants`, `CLAUDE.md §Exact bounds`). Auto-trim + (`CLAUDE.md §Precision invariants §Exact bounds`). Auto-trim strengthens it — the tail added is decay, not silence, and the silence past the decay is trimmed off. - **The null test uses NO tail.** The null-test / verify capture (M10) and any @@ -274,8 +275,8 @@ held to bit-identical repeats. the tail is **the track's own reverb/delay decay**, not the parent bus's. A track with a reverb plugin captures that reverb's tail; a track feeding a folder reverb does **not** capture the folder reverb's tail (that send is out of scope — and - note the pre-existing send-isolation caveat in `PLAN.md §Open questions`, which - the tail inherits unchanged, does not worsen). Correct and consistent. + note the pre-existing send-isolation caveat, which the tail inherits + unchanged, does not worsen). Correct and consistent. - **Item scope:** item/take FX only, self-track + ancestors + master bypassed → the tail is the **item/take FX decay only**. An item with a take reverb captures its tail; the track's reverb does not ring into it. Correct. diff --git a/docs/product/code-organization.md b/docs/product/code-organization.md index bde8a7b..c0b9357 100644 --- a/docs/product/code-organization.md +++ b/docs/product/code-organization.md @@ -9,8 +9,10 @@ subdirectories) **without sacrificing runtime performance**, against a stated qu > > Bring the codebase "**into the realm of something I can stand to look at.**" -Its build roadmap lives in **PLAN.md §Phase Q** and its authoritative spec in -**CONTEXT.md §Phase Q**. This doc holds the *why* — the quality bar, the evidence base +Its build roadmap's landed history lives in **`docs/ARCHIVE.md`** ("Phase Q — Quality: +the decision record") and its authoritative spec now lives in root **`CLAUDE.md`** +(§Directory and namespace layout, §Performance guardrails, §Structural heuristics) +plus the per-directory `src/**/CLAUDE.md` files. This doc holds the *why* — the quality bar, the evidence base (a grep-verified SOLID audit), the target directory/namespace shape grounded in the Vital reference, and the numbered fork decisions. @@ -321,10 +323,10 @@ re-namespaced, and split, not carried forward untouched into a tidier tree. "Som look at" is partly shape (§2/§2b) and partly *functional soundness* (§2c). Q-W0 is **not executed by product-designer** — this doc *defines the wave and its scope*; the audit -itself is staff-engineer/DSP work. The authoritative wave definition lives in **PLAN.md §Q-W0** and -its spec framing in **CONTEXT.md §"The pre-restructure audit wave (Q-W0)"**; this section is the -evidence-doc framing (the *why* and the smell taxonomy), matching how §2/§2b frame the structural -audits. +itself is staff-engineer/DSP work. The wave's landed history lives in **`docs/ARCHIVE.md`** +("Q-W0 fix-now remediations" and "Phase Q — Quality: the decision record"), and its findings live in +**`docs/product/code-quality-audit.md`**; this section is the evidence-doc framing (the *why* and +the smell taxonomy), matching how §2/§2b frame the structural audits. ### 2c.1 Audit surfaces (the named targets) @@ -451,7 +453,7 @@ namespace of every header, splitting the four largest TUs). Meanwhile: - **Phase L** has **L3** (VST editor + embed-strip restyle, gated on Phase S landing on dev) still to land — it touches the Phase S draw shells (`reasampler_editor` / `reasampler_embed`), which arrive on dev with Phase S. (L1/L2/L4/L5/L6/L7 have **already landed** — see - `COMPLETED.md`; the once-listed "L2 pending" is stale and has been corrected here.) + `docs/ARCHIVE.md`; the once-listed "L2 pending" is stale and has been corrected here.) - **D2** is **functionally complete** (D2-W1..W3-B landed; the only open item — a per-track lane-split panel indicator — is *explicitly deferred*, not a blocking residual). **M9** (slots) is **abandoned** (Daniel, 2026-07-27) — will not be built. D2 is named in the gate only so a diff --git a/docs/product/code-quality-audit.md b/docs/product/code-quality-audit.md index 196df62..e35f0c8 100644 --- a/docs/product/code-quality-audit.md +++ b/docs/product/code-quality-audit.md @@ -3,7 +3,8 @@ Date: 2026-07-28 · Branch: `pq-w0-audit` · Static analysis only; no code changed by the audit. This is the committed Q-W0 findings report (Q-10 SETTLED: a committed doc beside the SOLID/naming -audit — `docs/product/code-organization.md` §2c.3; deliverable contract in PLAN.md §Q-W0). It +audit — `docs/product/code-organization.md` §2c.3; deliverable contract landed per `docs/ARCHIVE.md` +§Phase Q). It synthesizes four parallel audit tracks; the full track notes remain in the tree as appendices and are the evidence base for every claim here — this report cites finding IDs and does not restate mechanisms in full: diff --git a/docs/product/design-view.md b/docs/product/design-view.md index aae7bb5..72ec0b0 100644 --- a/docs/product/design-view.md +++ b/docs/product/design-view.md @@ -1,8 +1,9 @@ # Design View — product notes Framing, rationale, and design-direction calls behind the **Design View** phase. -The tickable spec lives in `PLAN.md` (Phase D) and the authoritative technical -detail in `CONTEXT.md` (§Design View). This doc holds the *why* — the workflow +The tickable spec's landed history lives in `docs/ARCHIVE.md` (Phase D) and the +architecture detail in `src/core/view/CLAUDE.md` + `src/shell/view/CLAUDE.md` +(§Design View). This doc holds the *why* — the workflow narrative, the N-mode reasoning, the screenset differentiation, and the design-direction recommendations — so those don't clutter the build docs. @@ -339,8 +340,7 @@ tooltip on the switch. `docs/design/`), say so and it moves. Flagging because establishing the directory is a project convention, not mine to assume silently. 2. **Phase namespace.** Proposed **Phase D** (a lettered namespace) rather than - M12 — see PLAN.md rationale. Confirm you're happy with letters for parallel, - non-capture phases. + M12. Confirm you're happy with letters for parallel, non-capture phases. 3. **SDK name correction.** The brief cited `B_SHOWINMCP`; the verified SDK flag for mixer-panel visibility is **`B_SHOWINMIXER`** (SDK header line 2235). The spec uses the verified name. Just flagging the discrepancy so it isn't a diff --git a/docs/product/midi-playback.md b/docs/product/midi-playback.md index e5199e6..bf41338 100644 --- a/docs/product/midi-playback.md +++ b/docs/product/midi-playback.md @@ -2,10 +2,11 @@ Framing for a **MIDI-triggered audio sampler** that plays back ReaSampler's captured banks. This began as a discussion-shaping doc; with all forks now settled it has become -the **product framing behind a scoped phase**. Its build roadmap lives in **PLAN.md -§Phase S** and its authoritative spec in **CONTEXT.md §Phase S** — this doc holds the -*why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the settled -decision record). +the **product framing behind a scoped phase**. Its build roadmap lives in +**`docs/ARCHIVE.md` §Phase S** and its authoritative spec in +**`src/core/instrument/CLAUDE.md`** and **`src/shell/instrument/CLAUDE.md`** — this +doc holds the *why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the +settled decision record). Status: framed by product-designer (2026-07-26), **revised 2026-07-27 (r11)**. r11 records the **Sample-face recomposition** (Daniel's post-landing DAW pass, 2026-07-27): all linear sliders → @@ -42,10 +43,11 @@ demoted to an opt-in Zones panel — see the r6 Addendum in §4. r6 also settles S1–S6 instrument: the product name **ReaSampler 9000** and the **"better than RS5K" UX overhaul** (Phase S points S10–S13) — see the r5 Addendum in §4. r4 (below) settled the four residual forks D-A..D-D. The -"no PLAN.md footprint" era is **over** — with D-A through D-D settled (below), the +"no landed-roadmap footprint" era is **over** — with D-A through D-D settled (below), the instrument was scoped into **Phase S** (codename Daniel's: "S" for Sampler, because "D" -collides with the existing Design View phase). **PLAN.md §Phase S is now the -authoritative roadmap; CONTEXT.md §Phase S is the authoritative spec.** This doc is the +collides with the existing Design View phase). **`docs/ARCHIVE.md` §Phase S now records +the landed roadmap; `src/core/instrument/CLAUDE.md` and `src/shell/instrument/CLAUDE.md` +are the authoritative spec.** This doc is the framing/decision record they point back to. Prior revisions (a) established that a REAPER *extension* cannot be a MIDI instrument, (b) corrected a material omission — REAPER's **VST-host bridge**, which lets a VST3 plugin *hosted inside REAPER* call back into @@ -410,8 +412,8 @@ it doesn't carry" — is unchanged. What the bridge settles is *where that mappi between extension and instrument as **live shared `"reasampler"` state**, not a file one writes and the other re-parses. -**What the current index carries** (from `bank_model`'s `Sample`, per CONTEXT.md §Data -model): id, display name, relative path, source range, channel count, sample rate, +**What the current index carries** (from `bank_model`'s `Sample`): id, display name, +relative path, source range, channel count, sample rate, length, capture tempo, an **optional key**, peak/RMS/LUFS, content hash, tier, provenance, timestamp. Notably it *already* has an optional key field and capture tempo — the seeds of pitch-mapping are there. @@ -426,8 +428,8 @@ tempo — the seeds of pitch-mapping are there. to velocity zones). - **Round-robin groups** (cycle through N samples on repeated same-note hits). - **Loop points** (sustain loop start/end for held notes; sample-accurate, - zero-crossing-aware — CONTEXT already flags loop/zero-crossing handling as - day-one-relevant for wavetable material). + zero-crossing-aware — loop/zero-crossing handling is day-one-relevant for + wavetable material). - **Amplitude envelope** (ADSR) and optionally filter/pitch envelopes. - **Tuning/gain trim** per sample. @@ -559,7 +561,7 @@ ReaSampler-native way to build it and it's assumed, not debated, going forward. All four residual decisions are now called. Each is marked **SETTLED** with Daniel's choice and the reasoning kept as the record of *why* — do not re-litigate. They are -scoped into **PLAN.md §Phase S** / **CONTEXT.md §Phase S**. +scoped into **`docs/ARCHIVE.md` §Phase S** / **`src/core/instrument/CLAUDE.md`**. **D-A — SETTLED: bare Steinberg VST3 SDK + LICE editor (no JUCE).** *(The central fork. §1a is the assessment that fed it. The sub-question — who draws the editor? — was the @@ -649,7 +651,7 @@ After Phase S was scoped (D-A..D-D), Daniel set two further directions. These ar **settled directions**, not open forks — specced as new Phase S points (S7–S9), not re-litigated. Recorded here per the doc's settled-decisions convention. -**D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ PLAN.md S7).** +**D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ `docs/ARCHIVE.md` §S7).** Captures are often stereo; the current mono downmix is a Tier-0 simplification. The engine gets a **per-instance channel-mode toggle (1 mono / 2 stereo)** that "works with the REAPER audio bus automatically" — the VST3 declares/negotiates its output bus @@ -662,8 +664,8 @@ Cross-mode policy: mono-source-in-stereo → dual-mono; stereo-source-in-mono never a bank fact). Sequenced **first after the editor/embed work** because it touches the engine Daniel smoke-tests. -**Ingest routes through the bank — "option 1"; the extension owns ingest (→ PLAN.md -S8 + S9).** Loading a sample into the sampler is **one gesture**: capture/import-into-bank +**Ingest routes through the bank — "option 1"; the extension owns ingest (→ +`docs/ARCHIVE.md` §S8 + §S9).** Loading a sample into the sampler is **one gesture**: capture/import-into-bank + auto-assign to the active instance. The **extension owns ingest** (it has arrange access, Media-Explorer access, and the drop-target surface on its own panels); the **instrument stays a read-only bank consumer** — it never captures or imports. Sub-parts, @@ -690,7 +692,7 @@ with the honest SDK reality verified against the vendored headers: *The genuine spikes flagged (not decisions Daniel owes, just build-time unknowns):* the ME merely-selected-file read (b), and the drop-onto-editor cross-artifact relay (c). Both -are honestly-flagged as spikes in PLAN.md S8, not promised. +are honestly-flagged as spikes in `docs/ARCHIVE.md` §S8, not promised. ### Addendum — product name + UX overhaul (Daniel, 2026-07-26, post-S1–S6 DAW test) @@ -700,7 +702,7 @@ open forks (the two flagged forks below are the only calls left to Daniel). **The instrument's product name is `ReaSampler 9000`.** The extension stays **ReaSampler** (capture + organization); the instrument is **ReaSampler 9000** (playback). Propagation is -a checklist item (PLAN.md §Phase S — product name; CONTEXT.md §Product name): the VST3 +recorded in `docs/ARCHIVE.md` §Phase S — product name: the VST3 class **display name** string, the `IPlugView` editor title band (today "ReaSampler Instrument"), the S6 embed-strip label, and the docs. **Compat guard (load-bearing):** the **VST3 class UID must NOT change** — instances in saved projects key off it; a UID change @@ -747,8 +749,9 @@ names the editor as the wound. After the r5 UX-overhaul directive was specced (keymap-first S10), Daniel reframed the workflow before S10 was implemented. This **revises S10** and settles S-NAME-1. Settled -directions, not open forks — recorded here per the doc's settled-decisions convention; PLAN.md -§S10 and CONTEXT.md §Phase S (workflow hierarchy) carry the spec. +directions, not open forks — recorded here per the doc's settled-decisions convention; +`docs/ARCHIVE.md` §S10 records what landed and `src/core/instrument/CLAUDE.md` (the +editor `ui/` modules) documents the current architecture. **The reframe, verbatim (Daniel, 2026-07-26):** *"We need to think hard about the workflow with this plugin. Have a giant list of 'item' blocks is visually useless. When the plugin is @@ -806,8 +809,8 @@ partly on filename, fall back to keeping the filename and record that as shipped Daniel directed a set of engine features for the sampler, specced as **new Phase S points S15 (Trigger vs Gate) and S16 (pitch envelope)**. **The feature set is settled** — recorded -here per the doc's settled-decisions convention; PLAN.md §S15/S16 and CONTEXT.md §Sampling -modes carry the spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param +here per the doc's settled-decisions convention; `docs/ARCHIVE.md` §S15 / §S16 records what +landed and `src/core/instrument/CLAUDE.md` §Sampling modes documents the current spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param granularity); the WDL question was resolved by inspection. **Directive, verbatim (Daniel, 2026-07-26):** *"let's have product spec out some features @@ -927,9 +930,10 @@ This reshapes S16 and **flips the r7 WDL verdict** on `WDL_SimplePitchShifter`. `process` allocation; measure per-voice CPU + onset latency against the polyphony cap. Treat S16's Preserve-engine point as the phase's next real DSP spike, not a thin envelope add-on. -**Where the spec lives:** PLAN.md §S16 (reshaped to "pitch engine modes + pitch envelope", -with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 interaction note; -CONTEXT.md §Pitch engine modes — Varispeed vs Preserve + the corrected WDL surface finding. +**Where the spec lives:** `docs/ARCHIVE.md` §S16 (reshaped to "pitch engine modes + pitch +envelope", with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 +interaction note; `src/core/instrument/CLAUDE.md` §Sampling modes — Varispeed vs Preserve ++ the WDL surface finding. ### Addendum — VST channel isolation (Daniel, 2026-07-26) @@ -972,9 +976,9 @@ with or right after the in-flight waves (S9 ext_keys, S15/S16 processor/editor) channel's banks; stable-project + beta-VST = clean empty (not error); the S-NAME-1 rename/rebind test extends to the beta UID. -**Where the spec lives:** PLAN.md §S18; CONTEXT.md §VST3 channel identity — the UID pair + the -pairing surface. The pairing surface's data half is already load-bearing V4 machinery; S18 -adds only the identity fork on top. +**Where the spec lives:** `docs/ARCHIVE.md` §S18; `src/shell/instrument/CLAUDE.md` §VST3 +channel identity — the UID pair + the pairing surface. The pairing surface's data half is +already load-bearing V4 machinery; S18 adds only the identity fork on top. --- @@ -986,8 +990,9 @@ good — but the two-view editor (today's "Browser" + "Zones" toggle) misallocat the default window is undersized for a 1080p world, and the drop-a-capture-onto-FX gesture is broken in practice. The directive: **make the one job — pick a capture, tune it, play it — fast, easy, and fun. Style is a critical ingredient. No spreadsheet aesthetics.** These are the -`r9` calls. Authoritative spec: **CONTEXT.md §Phase S — editor view-model redesign (S-VIEW)**; -build roadmap: **PLAN.md §Phase S — editor view-model redesign**. +`r9` calls. Current architecture: **`src/core/instrument/CLAUDE.md`** and +**`src/shell/instrument/CLAUDE.md`**; landed record: **`docs/ARCHIVE.md` §Phase S — editor +view-model redesign**. **The reference devices (the north star for control density).** Daniel named Ableton **Simpler** and a Kilohearts/Phase-Plant **sampler group** as the composition targets. Both share one @@ -1021,7 +1026,7 @@ grammar, and it is the grammar the redesign adopts: loading a new one is a distinct act), not a three-way radio. *Why the reframe matters:* it makes "I just want to play this capture" the zero-click default, and "I want a different one" a single deliberate gesture, instead of making the user re-choose their whole stance every - time. See CONTEXT.md §S-VIEW for the precise navigation model. + time. See `docs/ARCHIVE.md` §S-VIEW-1 for the precise navigation model as landed. 2. **The Sample view earns the hero treatment; Browse gets ruthlessly cut.** Browse today carries a waveform preview, root-note piano-roll, loop-point labels, a track-root message, and @@ -1037,10 +1042,10 @@ grammar, and it is the grammar the redesign adopts: 3. **Two engineering prerequisites, framed but routed to implementation.** The **drop-to-FX bug** (dropping a capture onto a track's FX chain does not instantiate + init ReaSampler 9000) and the **undersized default window** are not design decisions — they are a bug and a one-line - default. Both are framed in CONTEXT.md §S-VIEW with the SDK reality swept (drop-to-FX: the S17 - machinery is SDK-correct, so this is a *diagnosis* task, not a redesign; window size: the - `getSize`/`checkSizeConstraint` mechanism is verified), and both are flagged for - staff-engineer, not for a product fork. + default. Both are recorded landed in `docs/ARCHIVE.md` §S-VIEW-BUG-1 (drop-to-FX: the S17 + machinery is SDK-correct, so this is a *diagnosis* task, not a redesign) and §S-VIEW-SIZE-1 + (window size: the `getSize`/`checkSizeConstraint` mechanism is verified), and both were + flagged for staff-engineer, not for a product fork. **New parameters this introduces (both instrument performance state, D-B — never bank facts):** @@ -1064,7 +1069,7 @@ grammar, and it is the grammar the redesign adopts: new top-level `previewVelocity` field), **not** the extension's `persist` project ext-state — that module is REAPER-project-scoped and extension-owned, so it would make the level project-global instead of per-instance and route an instrument concern through a bank-read-only - seam. See CONTEXT.md §S-VIEW for the round-trip and back-compat lift. This is what makes the + seam. See `docs/ARCHIVE.md` §S-VIEW-4 for the round-trip and back-compat lift as landed. This is what makes the preview button *fun*: tap it hard or soft without reaching for a controller — and it remembers. **Two visual components the redesign commits to:** @@ -1101,8 +1106,10 @@ persisted fields is not a compat event; saved instances rebind and restore. And (extended additively — `keyTrack` per-zone, `previewVelocity` per-instance via an envelope bump to v6, both with back-compat defaults on read) are the same load-bearing core. -**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW); PLAN.md -§Phase S — editor view-model redesign. This Addendum is the *why*; those are the *what/how*. +**Where the spec lives:** `src/core/instrument/CLAUDE.md` (envelope overlay, key-tracking, +preview-velocity ownership) documents the current architecture; `docs/ARCHIVE.md` §Phase S +— editor view-model redesign records what landed (S-VIEW-1 through S-VIEW-10). This +Addendum is the *why*; those are the *what/how*. --- @@ -1217,8 +1224,9 @@ for preview velocity — a different struct on a different version axis). Concre the L1 kit, routing mouse through `velocity_curve`), gated on the foundation track and composing with the S-VIEW-2 Sample face + S-VIEW-3 envelope-overlay work. -**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW), velocity-curve -sub-section; PLAN.md §Phase S — editor view-model redesign (S-VIEW-9/S-VIEW-10 + fork R10-F1). This +**Where the spec lives:** `src/core/instrument/CLAUDE.md` (the `velocity_curve` module, its +engine application point, and its ownership rules) documents the current architecture; +`docs/ARCHIVE.md` §S-VIEW-9 / §S-VIEW-10 records what landed (fork R10-F1 resolved). This Addendum is the *why*; those are the *what/how*. --- @@ -1278,11 +1286,13 @@ recomposition of *existing* controls; no new params, no component-state bump, VS unchanged. All drawing through the L1 kit by palette role; all layout/hit-test in new pure modules (`knob_deck`, `curve_popup` — mirrors of `action_bar`/`overflow_menu`); the knobs and the hero's envelope nodes remain two surfaces on one param model (S-VIEW-F2's structural sync, untouched). -The full inventory contract (every landed element → its r11 home) is in the CONTEXT.md spec. +The full inventory of what landed (every element → its r11 home) is recorded in +`docs/ARCHIVE.md` §FB1 and §FB2. -**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW) → "The -Sample-face recomposition (r11)"; PLAN.md §Phase S — editor Wave B (S-VIEW-11/12/13 + forks -R11-F1/R11-F2). This Addendum is the *why*; those are the *what/how*. +**Where the spec lives:** `src/core/instrument/CLAUDE.md` (the `knob_deck`/`curve_popup`/ +`master_gain` modules) documents the current architecture; `docs/ARCHIVE.md` §FB1 and §FB2 +record what landed (S-VIEW-11/12/13 + forks R11-F1/R11-F2 resolved). This Addendum is the +*why*; those are the *what/how*. --- @@ -1329,7 +1339,8 @@ Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Adden frameworks), DS-2 (Direction B "Neon Console" + Direction C's spectral keyboard strip), and DS-3 (thorough panel layout) are all **SETTLED (2026-07-26)**. Framing + palette + the three visual directions + forks: `docs/product/visual-design-language.md` (on `dev`); roadmap + - spec: **PLAN.md §Phase L + CONTEXT.md §Phase L** (on `dev`). **S10–S13 build with the + spec: **`docs/ARCHIVE.md` §Phase L** (landed record) and **`src/core/ui/CLAUDE.md`** + (current architecture) (on `dev`). **S10–S13 build with the current drawing and adopt the L1 kit when it lands — not gated on Phase L.** Answers Daniel's "the VST is dogshit / temple os / does Cockos have a toolkit" (2026-07-26, post-S1–S6 DAW test). @@ -1350,15 +1361,16 @@ Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Adden S15-F1 (choke, held) / S15-F2 (param granularity, lean per-zone). Feature set settled; the engine default is Daniel's fork. -**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1–S6 the original -dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded -UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10–S13** the ReaSampler -9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes engine features); -**CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the channel-mode -/ ingest / bank-generation / sampling-mode / pitch-engine contracts, the UX-overhaul spec, -the product-name convention, the pure/shell split, the WDL finding, the must-verify -SDK/bridge surfaces). This doc is the framing/decision record they point back to. The "no -PLAN.md footprint" era is over. +**Authoritative from here:** **`docs/ARCHIVE.md` §Phase S** is the landed roadmap (S1–S6 +the original dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier +1 → embedded UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10–S13** the +ReaSampler 9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes engine +features); **`src/core/instrument/CLAUDE.md`**, **`src/shell/instrument/CLAUDE.md`**, and +**`src/core/wire/CLAUDE.md`** are the current spec (seam-field semantics, scope contracts, +the channel-mode / ingest / bank-generation / sampling-mode / pitch-engine contracts, the +UX-overhaul spec, the product-name convention, the pure/shell split, the WDL finding, the +must-verify SDK/bridge surfaces). This doc is the framing/decision record they point back +to. The "no landed-roadmap footprint" era is over. --- diff --git a/docs/product/multi-bank.md b/docs/product/multi-bank.md index e8702da..3359d8b 100644 --- a/docs/product/multi-bank.md +++ b/docs/product/multi-bank.md @@ -1,8 +1,9 @@ # Multi-bank — product notes Framing, rationale, and design-direction calls behind the **Multi-bank** phase. -The tickable spec lives in `PLAN.md` (Phase B) and the authoritative technical -detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow +The tickable spec's landed history lives in `docs/ARCHIVE.md` (Phase B) and the +architecture detail in `src/core/model/CLAUDE.md` + `src/shell/bank_ops/CLAUDE.md` +(§Multi-bank). This doc holds the *why* — the workflow narrative, the pool-privilege reasoning, the movement semantics, and the design-direction recommendations — so those don't clutter the build docs. diff --git a/docs/product/provenance.md b/docs/product/provenance.md index 1390e8d..917c3b0 100644 --- a/docs/product/provenance.md +++ b/docs/product/provenance.md @@ -1,10 +1,10 @@ # Provenance — product notes Framing, rationale, and the dual-canvas reconciliation behind the reshaped -**Milestone 10 (provenance)**. The tickable spec lives in `PLAN.md` (M10); the -authoritative technical detail is `CONTEXT.md` (§Data model, §capture) plus this -note for the reconciliation calls. This doc holds the *why* and the open forks so -they don't clutter the build docs. +**Milestone 10 (provenance)**. The tickable spec's landed history is in +`docs/ARCHIVE.md` (M10); the architecture detail is in `src/core/model/CLAUDE.md` +and `src/shell/capture/CLAUDE.md` plus this note for the reconciliation calls. +This doc holds the *why* and the open forks so they don't clutter the build docs. Status: **IMPLEMENTED (2026-07-26).** Settled 2026-07-23; landed 2026-07-26. Reshaped from the old "provenance + null-test verify" M10. Two decisions were fixed diff --git a/docs/product/removal-and-prune.md b/docs/product/removal-and-prune.md index 7758d9f..a4896ca 100644 --- a/docs/product/removal-and-prune.md +++ b/docs/product/removal-and-prune.md @@ -2,24 +2,26 @@ Framing, rationale, and open forks behind the two missing removal capabilities: **sample-remove** (a sample-level index verb) and **prune** (the file-lifecycle -path CONTEXT.md keeps forward-referencing but never scoped). The tickable spec -lives in `PLAN.md` (Phase B point B5 for remove; **Phase R** for prune) and the -authoritative technical detail in `CONTEXT.md` (§Sample removal, §Prune — file -lifecycle). This doc holds the *why* — the workflow, the guardrails, the -index-vs-file boundary, and the forks that need a Daniel decision. +path the spec kept forward-referencing but never scoped). The tickable spec's +landed history is in `docs/ARCHIVE.md` (Phase B point B5 for remove; **Phase R** +for prune) and the architecture detail lives in `src/core/model/CLAUDE.md` + +`src/shell/bank_ops/CLAUDE.md` (§Sample removal) and `src/core/reclaim/CLAUDE.md` ++ `src/shell/persist/CLAUDE.md` (§Prune — file lifecycle). This doc holds the +*why* — the workflow, the guardrails, the index-vs-file boundary, and the forks +that need a Daniel decision. Status: framed by product-designer (2026-07-23); **all five forks settled by Daniel (2026-07-24)** — R-A this-bank-primary, R-B batched REAPER undo points (Phase-B-wide), R-C trash-preferred-with-unlink-fallback, R-D owned-file manifest (seam lands early in Phase B / capture), R-E manual action + panel button. The -decisions are folded into the fork sections below and into the B5 / Phase R spec -prose in CONTEXT.md and the tickable points in PLAN.md. +decisions are folded into the fork sections below and into the B5 / Phase R +history in `docs/ARCHIVE.md` and the architecture docs above. --- ## The one boundary that governs everything: index vs. file -ReaSampler already draws a hard line, stated repeatedly in CONTEXT.md: **a bank +ReaSampler already draws a hard line: **a bank operation touches the *index*, never the *file*.** Move, copy, evacuate, and delete-bank are all index-only; files persist on disk "until prune." Every removal capability below sits on exactly one side of that line, and keeping the @@ -120,7 +122,7 @@ line. It is the verb Phase B forgot, not a new pillar. --- -## Prune — the file-lifecycle path CONTEXT.md kept promising +## Prune — the file-lifecycle path the spec kept promising ### What the user is doing @@ -131,7 +133,7 @@ purpose. Over a long project the bank folder accumulates dead `.wav` files that cost disk and clutter. **Prune is the reclaim pass**: "sweep the bank folder, delete the files nothing references, tell me what you reclaimed." -This is the path CONTEXT.md forward-references in at least four places ("files +This is the path the spec forward-references in at least four places ("files persist on disk until prune," "the capture/prune path reclaims it") but never scopes. It is a real, promised capability with **no phase, no module, no point** — a dangling reference the plan has to make good on. @@ -141,8 +143,8 @@ scopes. It is a real, promised capability with **no phase, no module, no point** ReaSampler already shipped this exact shape once. Design View's `view_mode_model` has **`ViewModeModel::reconcile(liveGuids)`** — a pure function fed the live set (the tracks that still exist), returning the residual membership entries to drop -(CONTEXT.md §Design View: "prunes orphaned snapshots on every toggle/load; -tolerates unknown/stale GUIDs (prune on reconcile)"). Prune is the **file-pool +(`src/core/view/CLAUDE.md`: "tolerates unknown/stale GUIDs (pruned on reconcile +via `ViewModeModel::reconcile(liveGuids)`)"). Prune is the **file-pool mirror of that pure pattern**: > `reconcile(liveGuids)` reconciles *membership entries* against *live tracks*. @@ -224,7 +226,7 @@ Prune is **not** a Phase B point. Three reasons it earns its own lettered phase 1. **It is a different pillar.** Phase B is the *bank container* pillar (index-only, non-destructive, above the file). Prune is the *file lifecycle* - pillar (the one path that deletes files). CONTEXT.md already names it as a + pillar (the one path that deletes files). The spec already named it as a separate concern every time it says "the capture/**prune** path" — file lifecycle is spoken of as its own thing, owned by neither the capture nor the bank layer. Giving it its own phase matches how the spec already talks about it. @@ -337,7 +339,8 @@ capture).** The book tracks the set of files it has created; prune reclaims ext-state**, and Phase R's R1/R2 *consume* that manifest. The exact persistence shape — a sibling ext-state key vs. folded into the `banks` blob — is a small residual to settle at build; the **manifest-now decision is firm**. (Settled - 2026-07-24; the up-front point is added to Phase B / the capture path in PLAN.md.) + 2026-07-24; the up-front point is recorded in `docs/ARCHIVE.md` under Phase B / + the capture path.) **Fork R-E — prune trigger: manual-only vs. offer-on-orphaning vs. periodic. SETTLED: MANUAL ACTION + PANEL BUTTON.** Prune runs via a bindable manual action diff --git a/docs/product/versioning-and-release.md b/docs/product/versioning-and-release.md index da5d543..a48b7c4 100644 --- a/docs/product/versioning-and-release.md +++ b/docs/product/versioning-and-release.md @@ -8,8 +8,8 @@ concrete and decidable: 2. **A beta side-channel** — so development can continue and a beta build run *alongside* the stable one without the beta clobbering the release. -This doc holds the *why*, the forks, and a recommendation. When Daniel picks, the -tickable points land in `PLAN.md` and the deploy/build wiring hands off to dev-ops. +This doc holds the *why*, the forks, and a recommendation. The tickable points' +landed history is in `docs/ARCHIVE.md`; the deploy/build wiring hands off to dev-ops. This is a framing note; it changes no source or CMake. Status: framed by product-designer (2026-07-26); **all four forks settled by Daniel @@ -54,7 +54,7 @@ Two sharp edges follow directly and recur throughout this note: Both binaries reading/writing the same namespace on the same open project means a **beta can read — and rewrite — a stable project's saved bank/view state.** Given the forward-only migrations already in the design (legacy `bank_index` retired - after promotion; `banks` authoritative thereafter — CONTEXT.md §Multi-bank), a + after promotion; `banks` authoritative thereafter — `docs/ARCHIVE.md` §Phase B), a beta that writes a newer schema into a project a user then reopens in stable is a real corruption path, not a theoretical one. diff --git a/docs/product/visual-design-language.md b/docs/product/visual-design-language.md index b6fc8a1..821574a 100644 --- a/docs/product/visual-design-language.md +++ b/docs/product/visual-design-language.md @@ -16,11 +16,12 @@ written after Daniel DAW-tested the S1–S6 instrument: revised 2026-07-26 (Daniel)** from the single electric-cyan accent to a **three-accent pastel system** (primary/secondary/tertiary = pastel lime green / pastel teal / pastel purple) — see §6. (A stylish-font upgrade was considered and **declined** by Daniel — no -font bundling/redistribution; the kit keeps its current cached-font face.) The build points it feeds live in -**PLAN.md §Phase L** (**L1** the shared LICE drawing kit, **L2** the dock-panel layout -redesign, **L3** the VST editor + embed-strip restyle) and **CONTEXT.md §Phase L** (the -design-system spec). Toolkit facts below are **verified against the vendored `vendor/WDL` -tree**, not lore. +font bundling/redistribution; the kit keeps its current cached-font face.) The build points it +fed have landed; their history is in **`docs/ARCHIVE.md`** ("Phase L — Look-and-feel", **L1** the +shared LICE drawing kit, **L2** the dock-panel layout redesign, **L3** the VST editor + +embed-strip restyle) and the architecture now lives in **`src/core/ui/CLAUDE.md`** / +**`src/shell/panel/CLAUDE.md`** (the design-system implementation). Toolkit facts below are +**verified against the vendored `vendor/WDL` tree**, not lore. > **Phase-boundary note (2026-07-26).** This look-and-feel effort was originally drafted > as Phase S points **S0-DS** (the shared kit) and **S14** (the panel refresh). It was @@ -497,7 +498,7 @@ within the pastel intent — is one file. --- -## 5. The build shape (feeds PLAN.md / CONTEXT.md §Phase L) +## 5. The build shape (landed history in `docs/ARCHIVE.md` §Phase L) Three points and their sequencing. @@ -547,7 +548,7 @@ draw it. affordances, per-selection **move / copy / remove** sample menu. - **Prune** button (R-E) — the byte-deleting action, `warn`-colored. -*M11 adds (dev PLAN.md M11 — merging now):* +*M11 adds (landed — `docs/ARCHIVE.md` Milestone 11):* - **Action trigger buttons** — clickable buttons that fire the capture + provenance action family directly (capture item / capture track scopes, re-capture from source, resample-and-mute-source, batch capture, conform-on-insert, insert-at-cursor, drag-out, diff --git a/src/app/CLAUDE.md b/src/app/CLAUDE.md new file mode 100644 index 0000000..edfc667 --- /dev/null +++ b/src/app/CLAUDE.md @@ -0,0 +1,35 @@ +# src/app — REAPER extension entry point + +## Scope + +Contains only `main.cpp`. Since the Phase Q hoists (Q-W3 onward), this TU is ONLY +pointers + entry + dispatch — the actual capture/panel/persist/action orchestration +lives in `shell/`. `main.cpp` owns: receiving REAPER's dispatch struct +(`ReaperPluginEntry`), resolving the REAPER API function pointers +(`REAPERAPI_LoadAPI`), the globals other files reference via `extern` (`g_hInst`, +`g_rec`), the `ReaSamplerSession` instance, its own bindable-action family via the +Q-W6 data-driven registration table (`shell/actions/action_registry`), and invoking +the other action families' (`design_view` / `bank` / `ingest`) own +register/handle/unregister triples at load and unload. + +Exactly **one** translation unit defines `REAPERAPI_IMPLEMENT` — that is `main.cpp`. +Every other `.cpp` includes `reaper_plugin_functions.h` without the define and gets +`extern` declarations for the global API function pointers. + +See root `CLAUDE.md`'s "REAPER extension contract" section for the full four-step +action-registration contract (`command_id` / `gaccel` / `hookcommand` / unload +mirror-unregister) that both this file's own action-table rows and the other +families' register/handle/unregister triples follow. + +## Modules + +- `main.cpp` — the REAPER extension's entry point and the sole `REAPERAPI_IMPLEMENT` TU; see root `CLAUDE.md`'s "REAPER extension contract" section for the registration contract this file implements. + +## Gotchas + +- This is intentionally a thin TU post-Phase-Q. Adding a new bindable action to + `main.cpp`'s own family means adding one row to its `ActionTableRow` table and a + flat handler function — do not hand-roll a parallel register/hookcommand/unregister + mechanism alongside the table. +- Never let a second `.cpp` define `REAPERAPI_IMPLEMENT` — that would double-allocate + the global REAPER API function pointers. diff --git a/src/core/audio/CLAUDE.md b/src/core/audio/CLAUDE.md new file mode 100644 index 0000000..396785b --- /dev/null +++ b/src/core/audio/CLAUDE.md @@ -0,0 +1,13 @@ +# src/core/audio — pure audio-data math + +## Scope + +Pure, REAPER-free audio-data math with no dependence on REAPER's own peak-cache +API. Currently one module: waveform min/max bin computation from raw PCM. Does +**not** include: LICE waveform drawing (`draw_kit`, `shell/panel`), the editor's +waveform/marker geometry (`waveform_view`, `core/instrument/ui`), or PCM +decoding itself. + +## Modules + +- `peaks` — waveform min/max bin computation from raw PCM; does not depend on REAPER's peak API. diff --git a/src/core/capture/CLAUDE.md b/src/core/capture/CLAUDE.md new file mode 100644 index 0000000..93ae32c --- /dev/null +++ b/src/core/capture/CLAUDE.md @@ -0,0 +1,69 @@ +# src/core/capture — pure logic behind the capture pillar + +## Scope + +Pure, REAPER-free logic behind the capture pillar: path arithmetic, the RIFF/WAV +codec, render-settings/FX-scope/tail-mode mapping, `InsertMedia` mode-bit +computation, the realtime-record state machine, and batch-capture planning. +Does **not** include: the REAPER-bound capture backends themselves +(`shell/capture`), the docked panel's tail-toggle window/click-handling +(`shell/panel`), or the `InsertMedia` call/undo-block mechanics +(`shell/capture`'s `insert.cpp`). + +## Invariants + +The repo-wide precision invariants (null test, bit-identical repeats, +non-destructive, exact bounds, relative-paths-only, capture FX scope) are +authoritative in root `CLAUDE.md` — reference them, don't re-copy them. +Detail specific to these pure modules: + +- **No silent time-stretch, made checkable.** `insert_plan` never sets the &4 + ("stretch/loop to fit time sel") bit; `kStretchToTimeSelBit` is exposed + precisely so a test can assert it is never present in any computed + `InsertMedia` mode. +- **Tail is a three-state mode (`docs/product/capture-tail.md`), not a + per-action variant:** None (exact bounds, byte-identical, the only mode for + null-test/verify captures), Auto (generous 8 s tail then trim trailing + silence to -72 dB surgical normalize), Manual (fixed length, clamped to the 8 + s cap, no trim). `render_settings` owns the offline RENDER_* mapping; + `tail_control` owns the panel-facing toggle/cycle/clamp/label logic sharing + the same `TailMode` enum and the same 8 s / -72 dB constants (single source + of truth — do not hardcode a second copy in either module). +- **Capture FX scope is enforced via FX-bypass + gain-neutralize, not a render + bit.** `render_settings::fxBypassPlanFor` selects which tracks (self / + ancestors / master) get their FX bypassed for a given `CaptureScope`; there + is no master capture scope (to capture the master, render a track instead). +- **Relative paths only, by construction.** `capture_paths::BankPaths` + separates the absolute render directory REAPER needs from the + project-relative path the `BankIndex` stores; `bankRelativeForName` spells an + enumerated folder entry the identical way `deriveBankPaths` spelled it at + capture time, so the prune core's exact-string match cannot drift. +- **Project-identity transition is GUID-primary.** `capture_paths`'s + `classifyProjectTransition` checks the minted GUID before the live + `ReaProject*` object, specifically because REAPER can recycle a closed + project's pointer address onto an unrelated project. + +## Modules + +- `wav_codec` — chunk walker + layout parse + float32 build + size-field patch + content hashes; the single pure RIFF/WAV owner (`wav_trim` is retired; `wav_codec` is the sole owner). +- `capture_realtime` (`core/capture`, **renamed from `realtime_record` in Q-W3** — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matching `drag_out`/`drag_out_win`) — the M8 realtime-record pure logic: capture scope + FX-tap point → `I_RECMODE`/`I_RECMODE_FLAGS` values, wet/dry → tap point, the recorded-file → `Sample` mapping, and the async record-phase state machine. Depends on `bank_model` for the plain `Sample`/`SourceMode` types. The transport/temp-track/send recipe lives in the shell (`shell/capture/capture_realtime_shell.cpp` + `capture_realtime_finalize.cpp`). +- `batch_capture` — pure batch-capture planner: maps source ranges to capture units and aggregates results. +- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition` → `NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives. +- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`. +- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), and the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family. +- `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project. + +## Gotchas + +- `render_settings`'s `RENDER_SETTINGS`/`RENDER_NORMALIZE`/`RENDER_TAILFLAG`/ + `RENDER_TRIMEND` bit values are transcribed verbatim from the SDK header + (`reaper_plugin_functions.h` lines ~3041/~3047/~3051/~3062) — re-verify + against the header before changing any bit value, per the root `CLAUDE.md` + API-verification rule. +- `kRenderPreFaderStems` (&8192) is deliberately **not** used — REAPER offline + render has no true pre-FX "dry" bit; FX scoping is done entirely by the + FX-bypass-around-render mechanism, never by a render bit. +- `tail_control`'s `kDefaultManualTailMs`/`kManualStepMs` and + `render_settings`'s `kMaxTailMs`/`kAutoTrimThresholdDb` are separate constants + in separate files by design (panel-facing default/step vs. runaway-guard cap) + — don't conflate them when touching either. diff --git a/src/core/instrument/CLAUDE.md b/src/core/instrument/CLAUDE.md new file mode 100644 index 0000000..e2d6411 --- /dev/null +++ b/src/core/instrument/CLAUDE.md @@ -0,0 +1,223 @@ +# src/core/instrument — pure VST3-instrument core (engine / map / ui) + +## Scope + +The ReaSampler 9000 instrument's pure, REAPER-free, VST3-free, unit-tested core, in three +subdirectories: + +- **`engine/`** — the polyphonic voice engine, per-zone play params, pitch shifting, + velocity curve, and master-gain taper math. +- **`map/`** — the zone/keymap payload, the cross-artifact `ComponentState` codec, and the + small pure helpers the engine/shell share (bank-generation sync, bridge-read + marshalling, note-name parsing, Trigger frame↔fraction conversion). +- **`ui/`** — pure editor geometry/hit-test modules (layout, waveform, keyboard strip, + capture browser, param controls, envelope overlay/edit). These are geometry-and-math + only; the LICE draw + REAPER/VST3 plumbing is the `shell/instrument` editor shell, + **out of scope for this file** (owned by a parallel dispatch), along with the VST3 + processor, `reaper_bridge`, `reasampler_embed`, and `vst_entry`. + +## Invariants + +### The three locked decisions this spec assumes (settled 2026-07-26) + +- **D1 — native VST3.** Not JSFX. Full sampler sophistication, clean integration, and + access to the REAPER VST-host bridge. +- **D5 — Windows-only, VST3-only, REAPER-only.** No cross-platform DSP/build/signing + matrix, no multi-format wrapper, no standalone-in-other-hosts concern. +- **D6 — two products, tightly integrated.** A separate artifact, but not a divorced + file-only companion: via the VST-host bridge it reads the live `"reasampler"` project + ext-state and is project-aware. (The bridge mechanism itself is documented in + `src/core/wire/CLAUDE.md`.) + +### The two seams (audio via files, mapping via live state) + +- **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float + WAVs — project-relative, travelling with the `.rpp`. The instrument resolves those + paths the same way `persist` does (a shared convention, not a re-implementation). + There is no live PCM stream across the bridge, by design. +- **Live-state seam (the mapping, via the bridge).** For everything that is not raw + audio — the bank index, the mapping, which project is active — the instrument reads + the live `"reasampler"` ext-state via the bridge. + +### The seam fields — what becomes a bank intrinsic (D-B, settled 2026-07-26) + +The split model is the settled answer, mirroring the capture/placement separation: + +- **Bank intrinsics (facts about the captured file) live on `Sample`.** Root note (the + MIDI note the sample was recorded at) and loop points (sustain-loop start/end for held + notes) are facts about the file, added as an additive field extension (same shape as + `provenance`). +- **The performance map (a creative arrangement) lives in the instrument.** Key zones, + velocity layers, round-robin groups, amplitude envelopes, and per-sample tuning/gain + trim are a performance choice, not a fact about a file — they belong to the instrument, + not the bank. This "who owns which field" rule (D-B) governs every performance-map + field added since, including play mode/AHDSR/Trigger params (S15), pitch engine mode + and pitch envelope (S16), key-tracking, preview velocity, and the velocity curve + (S-VIEW) — all are per-instance/per-zone `ComponentState`, never written to `Sample` or + the bank. + +### The pure core (D3 — the load-bearing split) + +The sampler's voice engine, envelope math, key/velocity mapping, repitch/interpolation, +and keymap resolution are a pure, REAPER-free, DAW-free, unit-tested module — the mirror +of `bank_model`/`peaks`/`view_mode_model`/`bank_book`. The VST3 wrapper (the +`SingleComponentEffect` subclass, bus setup, `process` marshalling, the `IPlugView` LICE +editor, and the bridge calls) is the thin shell — the only part that touches VST3 or +REAPER at all. Any VST3 or REAPER type leaking into this core is a bug. + +- **The bank is one source; the instrument is another view of it (never a fork).** The + instrument is a pure consumer of the bank — it does not copy samples, does not own a + private sample store, and does not mutate the bank. +- **`Sample` field additions are additive and lossless.** No existing `Sample` field + changes; no `BankIndex` behavior changes. +- **Relative-paths-only survives.** The instrument resolves audio via the project-relative + machinery; it introduces no absolute paths. + +### Channel mode — current reality + +**Current reality (root `CLAUDE.md`, GA post-launch pass): the output bus is +permanently stereo.** `ChannelMode` is decode-only; the dynamic mono↔stereo bus +renegotiation (`setBusArrangements` per-instance toggle) has been deleted. Channel mode +auto-defaults from the loaded capture's channel count via a pure `channelModeFor` helper, +gated by a persisted `channelModeExplicit` flag (`ComponentState` v9). Mono source + +stereo mode → dual-mono (same signal both channels, centered); stereo source + mono mode +→ downmix (existing decode-side policy). + +> **Superseded design, do not reintroduce:** an earlier "Channel mode — mono | +> stereo (D-E)" design specified a per-instance toggle that **dynamically +> renegotiates the REAPER audio bus** via `setBusArrangements`/`getBusArrangement` +> (the instrument reporting mono or stereo per instance and REAPER's routing +> following). That dynamic-bus-negotiation design was superseded by the GA fix +> above; root `CLAUDE.md` is current and wins. + +### Sampling modes — Gate vs Trigger, pitch engine, pitch envelope (S15/S16 — settled, landed) + +Daniel's directive (2026-07-26, verbatim): *"Sampling mode: Trigger vs Gate. Gate has an +AHDSR envelope. Trigger has fade in, % length, and fade out. Both modes have modifiable +start point, Gate has modifiable loop points too. In addition to amp env, there will be a +pitch envelope/curve (AD?) which is off by default."* + +- **Gate — classic held note.** Note-on enters the amp envelope; note-off enters + release; a sustain loop applies for held notes. Envelope is **AHDSR**: `0→1` over + attack, hold at 1 over `holdFrames`, `1→sustain` over decay, hold sustain until + note-off, `level→0` over release. `holdFrames == 0` is exactly the pre-Gate ADSR — a + back-compat degenerate. +- **Trigger — one-shot drum-pad.** Note-on fires playback of a defined `%` of sample + length with a fade-in and fade-out ramp; note-off is ignored (the voice plays through, + no sustain loop). Frame span `[startFrame, playEnd)` where `playEnd = startFrame + + round(lengthFraction·(frames − startFrame))`; amplitude ramps `0→1` over + `fadeInFrames` at the head and `1→0` over `fadeOutFrames` anchored to `playEnd`; fades + clamp so `fadeInFrames + fadeOutFrames ≤ play length`. Fade curve is equal-power + (constant-power sin/cos). **Note-off in Trigger is a no-op** — choke-on-note-off is + held/out of scope (fork S15-F1). +- **Both modes: modifiable start point.** Playback begins at `startFrame` (clamped `0 ≤ + startFrame < frames`). Gate additionally has modifiable loop points; Trigger has none. +- **Pitch engine — Varispeed vs Preserve (per-zone toggle, S16).** Varispeed (current/ + classic path): `ratio_ = pitchRatio(note,root)`, `readPos_ += ratio_` with linear + interp — resampling that couples pitch and duration; cheap, zero-latency, musically + right for drums/one-shots. Preserve (duration-preserving): the read advances at the + source rate while a pitch shifter transposes the output — musically right for + tempo-locked loops/phrases; **the engine default leans Preserve** (fork S16-F1). + Contract for Gate's sustain loop under Preserve: *loop the source, shift the output* + (loop points stay source-frame facts). `WDL_Resampler` is **not** a Preserve engine (it + is a resampler that couples duration) — never wire it as the duration-preserving path. +- **Pitch envelope — AD, off by default.** A short attack-decay pitch-offset curve + (`peakSemitones` over `attackFrames`, decaying to 0 over `decayFrames`) riding on top of + whichever pitch engine; a zero attack gives a pure percussive pitch drop. **Off by + default** — a regression that applies pitch modulation when the envelope is disabled is + a bug. Under Varispeed the offset is a per-frame multiply of `ratio_`; under Preserve it + is added to the shifter's shift amount. +- **Preserve RT discipline.** The shifter pre-warms at voice-allocation; no allocation in + `process()` in steady state. **Note (supersedes an earlier framing):** the + shifter's onset latency (~25 ms, half-window) was once described as "an + accepted property, not a defect." Root `CLAUDE.md`'s GA2 pass **eliminated** that onset + latency (ring buffer primed with the actual upcoming source at note-on instead of + zero-filled, so Preserve now speaks on frame 0, matching Varispeed) — a + cold-started/un-pre-warmed shifter producing a click or smear remains a bug. +- **S15/S16 stay channel-count-agnostic.** The mode/envelope logic is per-frame amplitude + and read-rate, independent of the stereo channel dimension — any S15/S16 code that + assumes a fixed (mono) channel count rather than operating per-frame pre-mix is a bug. +- **S15/S16 are Tier 0–1 engine features, not Tier 2/3** — do not let the held Tier-2 + feature list (velocity layers / round-robin / filter work) drive their build shape. + +### Non-goals / guardrails (instrument-specific; repo-wide invariants live in root CLAUDE.md) + +- **No cross-platform / multi-format.** Windows-only, VST3-only, REAPER-only (D5). Do not + add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target. +- **The pure core stays REAPER-free *and* VST3-free.** Any VST3 or REAPER type leaking + into the voice engine / envelope / keymap / repitch module is a bug (the D3 split). +- **Channel mode is a performance choice, not a bank fact.** Never written to `Sample` or + the bank. +- **Do not spec Tier 2/3** from this directory. Tier 2 is held, Tier 3 is + optional-forever; don't let their feature lists drive Tier 0–1's build shape. + +### Envelope overlay + draggable nodes (S-VIEW, settled 2026-07-27, landed) + +The amp envelope is drawn as a curve over the Sample view's hero waveform at the shared +time base — Gate → the AHDSR shape, Trigger → the fade-in/unity/%-length/fade-out shape +anchored to `playEnd`. **The overlay is directly editable — draggable nodes +(SETTLED, S-VIEW-F2).** Dragging a node and the existing sliders are two surfaces onto +one model: both read/write the same zone envelope fields, so a drag updates the params, +the sliders reflect them live, and a slider edit re-lays the nodes — one source of truth, +structural (re-read-every-paint), not a listener chain. Nodes are monotonic in time (a +node cannot be dragged past its neighbours) and range-clamped to the same per-param +min/max the sliders enforce, so node-drag can never produce a param the slider couldn't. +Two pure modules split the forward (draw) and inverse (edit) maps — see `envelope_overlay` +and `envelope_edit` in Modules below. + +### New performance-map parameters — ownership and persistence (D-B) + +- **Key-tracking** — per-zone, additive/version-bumped component state, default 100% + (absent field on an older blob lifts to 100%, bit-identical playback). +- **Preview velocity** — a per-instance utility setting for the Sample view's + preview-trigger button (not a musical parameter of the capture); **persists across + reloads** via the instrument's own `ComponentState` (envelope-bumped), never via the + extension's `persist` ext-state module (that would make it project-global rather than + per-instance and leak an instrument concern into the extension's key space). +- **Velocity curve** — per-zone; the one non-back-compat surface in S-VIEW: an + already-saved zone with no stored curve now plays every velocity at unity under the + flat-default (Option A), not bit-identical to the old linear `velocity/127` mapping — + a deliberate, Daniel-approved behavior change (see `velocity_curve` in Modules). + +## Modules + +### `engine/` + +- `sampler_core` — polyphonic voice engine with bounded stealing, user-parameterized voice count (1–32, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` Retrigger/Legato toggle), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots); per-zone `ZonePlayParams` (Gate/Trigger, AHDSR, pitch engine Varispeed/Preserve, AD pitch mod envelope), repitch/interpolation with loop-point-aware sustain. Preview injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` — no dedicated `PreviewCard`; preview obeys polyphony/mono/voice-stealing/envelopes. +- `zone_params.h` (`core/instrument/engine`) is the sibling header split out of `sampler_core.h` (T4-14/T4-17): the per-zone play-parameter value structs (`ZonePlayParams`/`AdsrParams`/`TriggerParams`/`PitchEnvParams`) and the per-instance mode enums (`ChannelMode`/`VoiceMode`/`MonoTrigger`) the engine, the codec, and the editor all share. +- `pitch_shift` — hand-rolled **correlation-aligned SOLA** (splice-overlap-add) pitch shifter for the Preserve playback mode: one active read tap chases the write head at the shift ratio; each splice jump is refined by a cross-correlation search so the new read point is waveform-aligned, then old and new taps are crossfaded (raised-cosine, amplitude-complementary). Replaces the prior dual-tap OLA whose fixed half-window tap offset caused anti-phase cancellation on many source frequencies. **GA2:** ring buffer **primed with the actual upcoming source** at note-on (was zero-filled) → gap-free frame-0 onset, ~25 ms Preserve onset latency eliminated (Preserve now speaks on frame 0, matching Varispeed), and real-content-bounded tail (last-window tail-truncation gone). No third-party dependencies; RT-discipline: no allocation in `process()`. +- `velocity_curve` — pure velocity→amp transfer curve: `VelocityCurve` evaluated by a Fritsch–Carlson monotone cubic Hermite spline (no overshoot outside [0,1]). `eval(velocity)` called once per note-on. `flat()` default (y=1, every velocity→unity) replaces the prior fixed `velocity/127` path — a deliberate non-back-compat behavior change (Daniel-approved). +- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift. + +### `map/` + +- `sample_map` — zone payload: zones keyed by note range. **Wall-clock times stored as rate-free SECONDS, resolved against the live project rate — NO hardcoded sample rates in `src/`** (Daniel's standing ruling, load-bearing). JSON round-trip. +- `component_state_io` (`core/instrument/map`) — the `ComponentState` envelope + zones-payload binary codec (envelope v1…v11, zones-payload v1…v7), split out of `sample_map` (Q-W2v, T4-13 ≡ T2-07) so BOTH artifacts can link the codec without the extension pulling in the whole voice engine (`sampler_core`/`pitch_shift`) to serialize one preset blob — the extension's `instrument_drop` and the instrument's processor read/write the identical bytes, so the cross-artifact contract cannot drift. +- `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects. +- `bridge_marshal` — pure marshalling helper for the REAPER VST-host bridge read: interprets the `GetProjExtState` int return against its filled buffer. +- `note_entry` — parses a raw string into a clamped MIDI note [0,127]; accepts plain decimal integers or note names (C4==60, DAW convention). +- `trigger_seam` — pure Trigger frames↔fraction converter: owns the shared formula for converting between engine source-frame fade counts and the overlay's fractional representation, threading `startFrame` correctly through pack and unpack directions. + +### `ui/` + +- `editor_geometry` (`core/instrument/ui`) — VST3 editor layout: aliases the shared `core::ui::Rect` (+ `contains()`) rather than defining its own; owns `EditorLayout`/`layoutEditor(w,h)`, the Tier-0/Tier-1 sample-list and keymap-editor row layout/hit-test, and — hoisted here off the former `reasampler_editor.cpp` god-TU (Q-W2v, T2-06) — the r11 Sample-face band layout (`SampleBands`/`ClusterRects`/`channelToggleRects`) and the Zone-face content/legend/deck layout, so the editor shell only draws + routes. +- `keyboard_strip` — piano-keyboard strip: MIDI-note→key rect mapping, black/white key layout, hit-test, zone highlight overlay geometry. +- `waveform_view` — waveform/marker geometry: maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap. +- `capture_browser` — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing. +- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search. +- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel. +- `embed_strip` — compact single-row control layout for embed mode in the track FX chain. +- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types. +- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types. +- `envelope_overlay` — pure amp-envelope→polyline geometry for the Sample-view envelope overlay (read from `envelope_overlay.h`): maps Gate's AHDSR shape or Trigger's fade-in/unity/%-length/fade-out shape to a polyline inside a rect at the shared time base (Gate: a bounded param-domain schematic, sample-length-free; Trigger: PCM-aligned wall-clock), every vertex clamped in-canvas (`x`/`y` inside the rect). Shares the `EnvNode`/`AmpEnvelope`/`timeToX`/`levelToY` vocabulary with `envelope_edit` so the drawn handle and its grab region agree pixel-for-pixel. No VST3/REAPER/LICE types at the boundary. +- `envelope_edit` — pure node hit-test + pixel-delta→clamped-param inverse map for the draggable envelope nodes (read from `envelope_edit.h`): `nodeAtPoint` resolves a grab to the nearest node within a pick radius (Chebyshev distance, draw-order tie-break); `resolveNodeDrag` maps a pixel delta since grab to a new `AmpEnvelope`, enforcing monotonic-in-time ordering between neighbouring nodes and the same caller-supplied per-param clamp bounds the sliders use — a drag can never produce a param a slider couldn't. Mirror of `card_drag`/`waveform_view`; the inverse of `envelope_overlay`'s params→polyline forward map, so node-drag and slider-edit read/write one shared model and can never diverge. + +## Gotchas + +- **Gate's envelope-overlay x-axis is schematic, not PCM-aligned** (per `envelope_overlay.h`'s FA2 contract note) — it does NOT line up with the waveform under it; only Trigger's x-axis is wall-clock/PCM-aligned. Don't assume the Gate curve is time-accurate against the sample. +- **Trigger's fade fields require a non-trivial converter, not a field copy.** `TriggerParams` (engine) stores fades as source *frames*; `AmpEnvelope` (the overlay's view struct) stores them as *fractions* of the played span. A converter is owed on both the pack (draw) and unpack (commit) directions — `trigger_seam` owns this formula; do not copy the fields directly. +- **`param_slider`'s linear slider rows are retired on the Zone panel** — per root `CLAUDE.md`'s FB2 note, the `Knob` primitive (`editor_geometry`/knob deck grammar) is now the only live consumer of that half of `param_slider` on the Zone face. Don't assume `param_slider`'s SLIDER row type is still drawn there. +- **Two superseded designs are called out in Invariants above**: the earlier + Channel-mode (D-E) bus-renegotiation design and the earlier Preserve-onset-latency + framing in the S16 guardrails. Root `CLAUDE.md` is the current source of truth + for both — do not reintroduce either superseded design. diff --git a/src/core/json/CLAUDE.md b/src/core/json/CLAUDE.md new file mode 100644 index 0000000..be2242f --- /dev/null +++ b/src/core/json/CLAUDE.md @@ -0,0 +1,20 @@ +# src/core/json — the hand-rolled JSON lexical layer + +## Scope + +The ONE hand-rolled JSON lexical layer used across the pure core: string/number/ +bool/null tokens, the scoped object `Writer`, and the bounds-checked `Reader` +cursor. Domain grammars — what fields a bank, view-mode, or manifest blob actually +has — stay in the consumers; this module owns lexing/emitting only. + +## Modules + +- `json` (`core/json`) — the ONE hand-rolled JSON lexical layer (Q-W1): string/number/bool/null tokens, the scoped object `Writer`, and the bounds-checked `Reader` cursor, byte-compatible with the five pre-extraction per-module writers it replaced (`bank_model` / `bank_book` / `view_mode_model` / `owned_manifest` / `tail_control`). Domain grammars stay in the consumers; this owns lexing/emitting only. + +## Gotchas + +- Byte-compatible with the five pre-extraction per-module writers it replaced + (`bank_model` / `bank_book` / `view_mode_model` / `owned_manifest` / + `tail_control`) — a change here risks silently breaking round-trip compatibility + with ext-state blobs already persisted by projects written before the Q-W1 + extraction. diff --git a/src/core/model/CLAUDE.md b/src/core/model/CLAUDE.md new file mode 100644 index 0000000..2e6cbaa --- /dev/null +++ b/src/core/model/CLAUDE.md @@ -0,0 +1,81 @@ +# src/core/model — the pure bank/sample index and its multi-bank container + +## Scope + +Pure (REAPER-free, unit-tested outside the DAW) sample-index models: the single-bank +index, the multi-bank registry that wraps it, its JSON codec, the gap-preserving +per-bank slot carrier, the owned-file manifest, and the capture-recipe fingerprint. +No REAPER types, no filesystem I/O — see root `CLAUDE.md` for the pure-core/shell +split this directory sits on. + +## Invariants + +**Pool privileges (multi-bank).** +- The pool is privileged, not special-cased: structurally one `BankIndex` among many + in `bank_book`; semantically it always exists, is un-deletable, and un-renamable + (fixed id + fixed display name "Pool"). New projects and migrated single-bank + projects start with the pool and zero named banks. Enforced in the pure rules + layer, not just the UI. +- No action path may delete or rename the pool, leave a project with zero banks, or + evacuate the pool (the pool is evacuation's destination, not a source). + +**Bank identity, movement, dedup.** +- Bank id is the stable key (GUID-style, minted on create); display name and ordinal + are mutable. Display names are unique — trimmed + case-insensitive (ASCII) — + enforced by `createBank`/`renameBank`; the pool's reserved name "Pool" is protected + by the same check. +- Movement moves the index entry, not the file: move/copy between banks is + index-only (remove from A's `BankIndex`, add to B's); the underlying file stays in + the shared project bank folder. Per-bank subfolders on disk are an explicit + non-goal. +- Dedup-by-hash is per-bank. Moving a sample whose hash already exists in the + destination bank collapses onto the existing entry there. Cross-bank dedup is not + enforced — the same hash may exist in the pool and a named bank simultaneously. +- Move is the default (removes from source, adds to destination); copy is the + deliberate secondary act (adds to destination, leaves source intact). +- Delete drops members (files are not deleted); evacuate returns all of a bank's + members to the pool (index-only, same destination-collapse rule). Evacuate cannot + be applied to the pool. A plain delete of a non-empty bank orphans those members + out of every index until prune reclaims their files — the UI confirms on + non-empty delete and offers evacuate as the alternative. + +**Sample removal.** +- Remove is index-only: drops one `Sample` entry from one `BankIndex`; mutates only + index + ext-state, no file written/moved/deleted, no timeline item touched. +- Remove can orphan a file — the same designed orphaned-until-prune state a + non-empty delete-bank produces — when it drops the last index reference to a + file. Reclaimed later by prune, never by remove. +- The pool's contents are removable; the pool container is not. Remove-from-pool is + allowed. +- Remove scope is this-bank only (settled 2026-07-24): drops the entry from this + bank, leaving copies in other banks untouched. `scope: this-bank | all-banks` is a + latent seam; only this-bank is a surfaced verb. +- Removes are silent — no confirm dialog. Recoverability comes from batched REAPER + undo (`Undo_BeginBlock`/`Undo_EndBlock`): one Ctrl-Z restores the index entry. + This undo-batching is Phase-B-wide (create/rename/reorder/delete-bank, move, copy, + evacuate, and remove all batch this way). `hashReferencedElsewhere` is a tested + model API retained for Phase R prune; it has no shell caller in the remove path. + +**Precision implications.** +- Relative-paths-only survives unchanged: every `BankIndex` in the book keeps the + relative-path invariant at its `add` boundary; movement is index-only so files + never relocate. +- Non-destructive: bank create/rename/delete/activate/evacuate and sample + move/copy/remove mutate only index + ext-state; no file is written, moved, or + deleted, and no timeline item is touched. + +## Modules + +- `bank_model` — `Sample` metadata struct + `BankIndex` (add/remove/query/tier/dedup-by-hash + JSON round-trip). Test it hard — it is the heart. +- `bank_book` — multi-bank registry: an ordered set of banks each wrapping a `BankIndex`. **Pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model.** Owns create/rename/reorder/delete of named banks, active-bank id, and index-only move/copy/remove of a sample between banks. The JSON round-trip lives in the sibling `bank_book_json` TU (Q-W5 split; serialize/deserialize via a private static `nameKey` seam) — one model, one codec, same public surface. +- `slot_map` (`core/model`) — the gap-preserving display-position carrier for ONE bank (sample id → slot, ≥0), extracted from `bank_book` (Q-W1): append/remove/reorder (insert-before-and-shift)/`reconcile` against live membership, `resetDense` migration seed, JSON round-trip. Wrapped (not merged) by `bank_book`. +- `owned_manifest` — the set of project-relative files the capture path itself created, persisted under the `"owned_files"` ext-state key, so the prune path can distinguish the bank system's own orphans from hand-dropped files. +- `provenance` — capture-recipe fingerprint: build/encode/compare a `rsprov1` fingerprint of scope, range, tail, rate/channels, track GUIDs, and FX-chain identity. **A thin reproducibility fingerprint — NOT a serialized chain to restore.** + +## Gotchas + +- `bank_book` wraps `BankIndex`; it does not modify it (additive — no `bank-id` + field on `Sample`). Do not add per-bank subfolders on disk or a global + cross-bank dedup — both are rejected-in-review non-goals. +- `bank_book_json` is a sibling TU, not a separate module — its round-trip is part + of `bank_book`'s public surface, not a distinct thing to describe separately. diff --git a/src/core/reclaim/CLAUDE.md b/src/core/reclaim/CLAUDE.md new file mode 100644 index 0000000..b82e8c4 --- /dev/null +++ b/src/core/reclaim/CLAUDE.md @@ -0,0 +1,41 @@ +# src/core/reclaim — pure prune orphan computation + +## Scope + +Houses the safety-critical "which files are orphans" decision for the file-lifecycle +(prune) pillar — filesystem-free, unit-tested before any I/O exists. The filesystem +enumeration and the actual deletion live in `shell/persist` (`prune_fs`), not here. + +## Invariants + +- **The load-bearing rule: remove creates orphans; prune reclaims them.** + Sample-remove and delete-bank drop index entries and may leave a file referenced + by nothing. Prune is the single path that turns such an orphan back into free + disk space. No other operation deletes a file; prune deletes only files that no + index references. +- Prune reuses the shape Design View already shipped (`view_mode_model`'s + `reconcile(liveGuids)`): prune reconciles files on disk against referenced files + (the union of every bank's index) and returns the orphan set to delete — same + pure pattern, one level down (files instead of GUIDs). +- Referenced-set is the union across ALL banks, pool included: a file is an orphan + iff no bank in the book references it. This is the safety-critical computation — + the prune null test is *prune never deletes a file that any index references.* +- Orphan attribution is an owned-file manifest (fork R-D): the book tracks the set + of files it has created; prune reclaims `(owned ∩ on-disk) − referenced`. This + rejects folder-sweep (which would delete hand-dropped files). +- **Prune null test:** a prune of a folder whose every file is referenced by some + bank deletes nothing; a prune deletes exactly the `present − referenced` orphan + set and nothing else. +- Never a referenced file; never a non-bank file — the union-across-all-banks rule + protects referenced files; the ownership-attribution rule (fork R-D) protects + hand-dropped files. + +## Modules + +- `prune_reconcile` — pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) − referenced`; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists. Gains `mergeReferenced(bankRefs, liveInstanceHeldPaths)` (pS-usage) — unions live instance holds into the prune referenced-set so the pure orphan computation includes them. + +## Gotchas + +- Dry-run/confirm UX, trash-preferred deletion mechanics (fork R-C), and the + manual-trigger guardrail (fork R-E) are deletion-*mechanics* concerns, not + orphan-*computation* ones — they live in `shell/persist`, not here. diff --git a/src/core/ui/CLAUDE.md b/src/core/ui/CLAUDE.md new file mode 100644 index 0000000..d6869a8 --- /dev/null +++ b/src/core/ui/CLAUDE.md @@ -0,0 +1,92 @@ +# src/core/ui — pure UI geometry, palette, and interaction-decision modules + +## Scope + +Pure, REAPER-free UI geometry, palette, and interaction-decision modules shared by +the extension's docked bank panel and the VST3 instrument's editor/embed surfaces: +layout math, hit-testing, hover/drag-gesture-precedence decisions, and the +role-based color palette. Does **not** include: the actual LICE drawing (`draw_kit` +lives in `shell/panel`; the editor's own paint code lives in `shell/instrument`), +REAPER/SWELL window or dialog mechanics, or the DAW-side Design View flag +application (`shell/view`). + +## Invariants + +Look-and-feel — visual design language (Phase L) (settled decisions, 2026-07-26; +L7 sub-pass, 2026-07-27): + +- **DS-1 — toolkit discipline.** Draw with LICE + reused WDL/vwnd pieces directly; + external frameworks (iPlug2/JUCE/VSTGUI) are rejected. "**Caution, not a ban:** + keep hit-test **geometry** in pure CTest-covered modules — do not import vwnd's + retained-mode object model wholesale (its controls own their hit-test internally, + which would move geometry into untestable shell code and undercut the pure/shell + split)." This directory is that pure-geometry seam. +- **DS-2 — palette is role-based, not hardcoded hue**, in one constants block + (`theme`): `bg/base`, `bg/panel`, `bg/cell`, `line/hairline`, `text/primary`, + `text/dim`, `accent/primary`, `accent/secondary`, `accent/tertiary`, + `accent/hot`, `warn`. Neutral ladder sits in REAPER's mid-grey theme family + (`bg/base` ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈ `#3a3a3a`, + `line/hairline` ≈ `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈ + `~#a0a0a0`+), elevation-ladder discipline (base < panel < cell by a few %, + micro-gradient + inner highlight/shadow carry elevation, not hard borders). + Three pastel accents carry categorical meaning: **primary (pastel lime) = + live/active/selected**, secondary (pastel teal) + tertiary (pastel purple) = + supporting categorical distinctions (kinds, not intensity). `warn` (red/amber) + is reserved **only** for byte-deleting or clip states (prune, delete). +- **WCAG-floor discipline (tight pairs to re-verify on any palette change):** + `text/dim` on `bg/panel`/`bg/cell` is the classic mid-grey-on-mid-grey floor + failure — must clear AA 4.5:1 body text. The three pastels as state + indicators/active fills on `bg/cell` have a shrunken contrast cushion + (~6:1–7:1, still clears 3:1 but re-check on any hue nudge). Body text on a + pastel fill is a tight AA 4.5:1 pair. Take the most pastel value that still + clears the floor, approached from the soft side, never re-saturated toward + neon "to be safe." +- **"Speed is the selling point" — a design constraint on this geometry, not + just the draw layer.** Sub-frame hover/press/drag feedback must repaint + immediately on the input message. **No decorative animation** — no + tweens/fades/pulses; the only permitted motion is a level/meter readout + following audio directly. Any glow/bloom state is a static drawn state, never + a pulse. +- **Precision/invariant implications (Phase L does not change these):** "the + pure/shell split holds" — all layout/hit-test stays in pure CTest-covered + geometry modules; the kit's *draw* half is shell, its *geometry* half is pure, + even where a WDL piece is reused. Look-and-feel work never touches capture, + placement, or bank data ownership. +- **L7 drag-gesture precedence is a pure decision helper.** The rule — leave + client rect → OS drag-out; else drop on a tab/other bank → move/copy; else + same-bank grid → reorder-to-slot (empty slot = place, occupied + no modifier = + insert-before-and-shift, occupied + Alt = replace) — is "encoded in a pure + decision helper (mirror `drag_out::decideGesture`)"; the shell only reads live + pointer/focus/client-rect/modifier state and calls it, then maps the resolved + gesture to a cursor via `SetCursor`. No cue or precedence logic belongs in the + shell. + +## Modules + +- `rect` (`core/ui`, header-only) — the ONE concrete pixel rectangle (Q-W1): XYWH storage + `right()`/`bottom()`/`ltrb()`/`contains()`, replacing 12+ byte-identical role structs (`ButtonRect`/`FooterRect`/`CellRect`/`KitBox`/…) and the VST side's separate LTRB `Rect`; every prior role name survives as a `using` alias at its old site (e.g. `editor_geometry::Rect`). +- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel. +- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip. +- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button. +- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag. +- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + three-accent pastel system, WCAG contrast-floor helpers. +- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers. +- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching). +- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement. +- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button. +- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons. +- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect. +- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace. +- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped. + +## Gotchas + +- The WCAG contrast-floor pairs above are real math in `theme`'s tests, not a + visual eyeball check — any new hue or role needs its own contrast-floor + assertion. +- `card_drag`'s precedence order must stay a pure decision helper mirroring + `drag_out::decideGesture` — don't let a shell reimplement gesture precedence + ad hoc; the cursor-cue mapping in the shell must stay a thin lookup over the + pure result. +- `rect`'s prior role names survive only as `using` aliases at their old call + sites — changing `rect.h` itself ripples across every directory that aliases + it (e.g. `editor_geometry::Rect`); check all alias sites, not just this one. diff --git a/src/core/util/CLAUDE.md b/src/core/util/CLAUDE.md new file mode 100644 index 0000000..1a8ee9f --- /dev/null +++ b/src/core/util/CLAUDE.md @@ -0,0 +1,19 @@ +# src/core/util — small shared pure utilities + +## Scope + +Tiny, dependency-free pure helpers linked by both artifacts: whole-file byte +loading and unit-interval clamping. + +## Modules + +- `file_bytes` (`core/util`) — the ONE whole-file byte loader (Q-W1), linked by both artifacts; blocking I/O, off-audio-thread only. +- `clamp01` (`core/util`, header-only) — the ONE unit-interval clamp (Q-W1), replacing four per-module static copies; NaN passes through unchanged rather than collapsing to a bound. + +## Gotchas + +- `clamp01` lets NaN pass through unchanged rather than collapsing it to a bound — + this is deliberate (it replaced four per-module static copies that already + behaved this way); don't "fix" it to clamp NaN to 0 or 1. +- `file_bytes` does blocking I/O — off-audio-thread only, never call it from + `process()`. diff --git a/src/core/version/CLAUDE.md b/src/core/version/CLAUDE.md new file mode 100644 index 0000000..da15b2c --- /dev/null +++ b/src/core/version/CLAUDE.md @@ -0,0 +1,20 @@ +# src/core/version — version/channel identity + +## Scope + +REAPER-free version/channel identity consumed by both artifacts (the REAPER +extension and the VST3 instrument) to derive binary names, ext-state namespaces, +command-id prefixes, action-name prefixes, dock idents, and version display +strings. + +## Modules + +- `app_version` — REAPER-free version/channel identity: CMake-sourced semver constant, ext-state stamp value, and the full set of channel-derived identity accessors. All channel strings derive from one `REASAMPLER_CHANNEL_IS_BETA` bit; no scattered `#ifdef`s in the shells. + +## Gotchas + +- All channel strings derive from one `REASAMPLER_CHANNEL_IS_BETA` bit — route new + channel-specific behavior through this module's accessors rather than adding a + scattered `#ifdef` in a shell. See root `CLAUDE.md` "Beta channel build" for the + full fan-out (binary name, ext-state namespace, command-id prefix, action-name + prefix, dock ident, version display). diff --git a/src/core/view/CLAUDE.md b/src/core/view/CLAUDE.md new file mode 100644 index 0000000..1fcc3dc --- /dev/null +++ b/src/core/view/CLAUDE.md @@ -0,0 +1,104 @@ +# src/core/view — pure Design View mode model + +## Scope + +Pure, REAPER-free Design View model: mode/track membership, folder-derived +visibility, snapshot-based park/restore planning, new-content (GUID) detection, +and the managed/manual lane-identity convention that underlies per-item mode +separation (fixed lanes). Does **not** include: the actual DAW-side flag +application (hide, CPU-park, per-FX offline, restore via `B_SHOWINTCP` / +`B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`) or the never-touch-master/mute/solo +enforcement — those live in `shell/view`. + +## Invariants + +Design View — additive phase spec (settled decisions; Two-canvas sub-phase, +settled 2026-07-23): + +- **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves + opt in to Design (or any mode). No track appears in two modes at once except + via an explicit show-both toggle or parent/folder derivation. +- **Parents are derived, never tagged.** A parent/folder track is visible in + mode M if either any descendant leaf is visible in M, or the parent belongs to + M by its own membership; an untagged parent is an Arrange member by default. A + parent is never parked in any mode it is visible in. Master track is always + visible and never touched. +- **N-mode model, two-mode UI.** The data model carries arbitrarily many modes; + the UI ships Arrange + Design. A mode is (stable id, display name, ordinal). +- **Persistence.** Membership index + last-active mode + per-track flag + snapshots ride in the existing `"reasampler"` project ext-state namespace and + travel with the `.rpp`. +- **Non-destructive restore (enforced in the pure layer).** For every flag the + tool drives, snapshot the prior value before parking; on toggle-back, restore + from the snapshot, never to a hardcoded default. Round-trip (snapshot → park → + restore) returns every driven flag to its captured value — this is the + phase's trust anchor, the analog of the capture null test. +- **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`), + never track index; tolerates unknown/stale GUIDs (pruned on reconcile via + `ViewModeModel::reconcile(liveGuids)`). +- **Relative/portable state only** in the persisted view section (GUID strings, + mode ids — no absolute paths, no index positions). +- **Show-both semantics.** A per-track "pin visible across modes" flag that + re-enables processing whenever shown. A show-both leaf appears in every + mode's visible set and is never parked — its driven flags stay at + snapshot/restored values. Stored on the membership record; persists; + togglable per selection. +- **Non-goals / guardrails:** No literal second canvas — a second window or + duplicated project stays rejected; subproject/second-project-file approaches + and overloading item `D_POSITION` with mode semantics (timebase-offset + regions) are rejected paths (the latter collides with the capture null test). + Every leaf is managed: an untagged leaf is an Arrange member and, when the + active mode is not Arrange, is fully parked and snapshot-restored exactly + like a tagged leaf; show-both is the only way to opt a leaf out of parking. + Restore from snapshot, never to a default: no hardcoded "on" restores. + +**Two-canvas sub-phase (Phase D2/E) — settled and landed parts only:** + +- **Mechanism: fixed item lanes.** Map mode → lane; toggle drives per-lane + play/show so only the active mode's lane is present. Items keep their real + position and real track — nothing is moved in time or deleted. +- **Membership: adoption rule for new items; active mode for new tracks.** New + tracks are tagged to the active mode at creation. New items follow an + adoption rule: if the item's track has pre-existing managed-eligible content + spanning exactly one mode, the item adopts that mode; the active-mode + fallback applies only when the track is empty or already spans multiple + modes. Items in manual lanes are excluded from the prior-mode computation and + are not auto-tagged at all. Membership is exclusive per item except via + show-both. +- **Managed vs. manual lanes — indexed and distinct.** The tool maintains a + lane-ownership index — per (track GUID, lane): managed (which mode owns it) + vs. manual (user-minted, outside the mode system). Mode operations touch only + managed lanes; manual lanes are never shown, hidden, silenced, or re-laned by + a toggle, and their `C_LANEPLAYS` stays exactly as the user set it. The + ownership index rides in `"reasampler"` `view_state` alongside the membership + index, GUID-keyed and portable. **New invariant — mode operations touch only + managed lanes:** "the 'which lanes may this toggle touch' decision is a pure + query over the ownership index; only reading REAPER's live lane state is + shell." +- **REAPER floor: v7** for this sub-phase (fixed lanes shipped in v7); no + version-gate branch — below v7 the sub-phase is simply unavailable. +- Precision invariants (null test, bit-identical repeats, non-destructive, + exact bounds, relative-paths-only) are entirely unaffected by this + sub-phase — no capture path changes; lane assignment and `C_LANEPLAYS` are + reversible flags, never a destructive operation. + +## Modules + +- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, JSON round-trip. +- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell. +- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch. +- `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects. +- `lane_keys` — the pure, REAPER-free convention mapping a fixed lane's durable REAPER name (`P_LANENAME:n`) to the opaque lane-key `view_mode_model` keys by, plus the managed/manual heuristic both the toggle-apply path and the new-content/auto-tag exemption path share: `kManagedLanePrefix` ("reasampler:") stamps every lane the tool mints; `isManagedLaneName`/`managedLaneKey`/`laneNameForMode`/`modeIdFromLaneName` round-trip a lane name ↔ its owning mode id; `isOnManualLane` is the single predicate governing which lanes a toggle may drive and which items are exempt from auto-tag. + +## Gotchas + +- REAPER exposes no durable per-lane GUID — the only lane identity is the + ordinal `I_FIXEDLANE`, which REAPER renumbers on reorder/delete. Lane + identity must ride on the durable `P_LANENAME` (`lane_keys`), never the raw + ordinal, or a reorder will silently corrupt managed/manual ownership. +- `kManagedLanePrefix` ("reasampler:") is stable-forever like an action-id + string — changing it strands the ownership of every already-minted lane in + every already-saved project. +- `guid_diff::GuidBaseline` must have `reset()` called on every detected + project switch, or the next `observe()` will diff across two unrelated + projects and mass-tag (or miss) content. diff --git a/src/core/wire/CLAUDE.md b/src/core/wire/CLAUDE.md new file mode 100644 index 0000000..37ea188 --- /dev/null +++ b/src/core/wire/CLAUDE.md @@ -0,0 +1,94 @@ +# src/core/wire — pure ext-state and wire-format codecs, cross-artifact contracts + +## Scope + +The lexical/wire layer underneath the domain grammars: length-prefixed ext-state field +codecs, the little-endian byte codec, the `GetProjExtState` grow-loop retry policy, the +FOREVER-FROZEN VST3 class-UID macros, and the two mirror-image cross-artifact wires +(`assignment_request` extension→instrument, `sample_usage` instrument→extension) plus +the FX-drop payload builder (`instrument_drop`). Domain grammars themselves (what the +fields *mean*) stay in their own modules (`bank_model`, `sample_map`, `provenance`, +`bank_sync`, …) — this directory owns lexing/emitting/marshalling only, not domain +semantics. + +This directory owns two cross-artifact contracts specifically: +- `reasampler_uid.h` is the FOREVER-FROZEN VST3 class-UID header shared by the runtime + `FUID` (`reasampler_vst.h`) and the `.vstpreset` hex string (`instrument_drop`) — so + binary identity and preset identity cannot diverge. +- `assignment_request` and `sample_usage` are mirror-image wires: the former carries the + drop payload extension→instrument, the latter carries usage records instrument→extension. + +## Invariants + +- **The VST-host bridge (the integration mechanism, stated once here).** A VST3 + hosted inside REAPER can call back into REAPER's own API by + resolving function pointers by name over the host callback (`hostcb(&effect, + 0xdeadbeef, 0xdeadf00d, 0, "FunctionName", 0.0)` — the same string-keyed API table the + extension uses; verified in `video_processor.h` and the `reaper_plugin_functions.h` + `GetProjExtState`/`SetProjExtState`/`EnumProjExtState` entries). The plugin can also + fetch its host context — the track/take/project it was instantiated in (opcode + `0xdeadf00e`). Consequence: the instrument reads the *same* live `"reasampler"` + ext-state that `persist` writes, follows the active project, and needs no + "point me at the bank folder" wiring — it asks REAPER which project it is in. Confirm + the bridge opcodes and by-name resolution against the vendored `vendor/reaper-sdk/sdk/` + headers (`reaper_plugin.h`, `video_processor.h`, `reaper_plugin_functions.h`) before + relying on new opcodes. + +- **Instance-usage wire — `rsusage_` (pS-usage; pure portions only — + the extension-side scan shell `usage_scan` and the prune-abort behavior are owned + by the `shell/persist` layer, a parallel dispatch).** + Each VST3 instance holds a per-instance GUID persisted in `ComponentState` v11 + (`instanceGuid` field; pre-v11 blobs mint the guid on first publish). At the tail of + every `reloadInstrument` call (off audio thread) the processor publishes its held + `SampleRefs` paths to the ext-state key `rsusage_` in the `"reasampler"` + namespace via `reaper_bridge::writeUsageExtState` — an entry point that is + **prefix-guarded** (accepts only `rsusage_`-prefixed keys, refuses all others), so the + read-only-bank invariant is structurally enforced. Direction: the instrument writes + usage keys; the extension reads them — the one sanctioned instrument→ext-state write, + a deliberate exception analogous to `assignment_request` on the other wire. + Usage records are **never cleared by the instrument at teardown** (REAPER destroys the + plugin instance when an FX chain is set offline, including Design View's CPU-park, so a + terminate-time clear would strip a still-live instance's record); liveness is decided + extension-side at prune-scan time. + - **The pure fold (`sample_usage::foldUsageRecords` / `usageHeldPaths`).** A record + counts iff its publishing track still hosts at least one instance (offline FX + included); a record with no track context counts while any instance exists; and when + records exist but zero instances were identified, **every** record's paths are + protected (identity-failure net — a matcher failure must never degrade toward + delete). The guarantee: a capture held by any live instance can never be deleted; if + the prune cannot determine with certainty which captures are held, it aborts + entirely (deletes nothing). Over-protection is the accepted residual; under-protection + is a data-loss bug. + - **Collision safety (`planUsagePublish`).** A persisted GUID is copyable (FX copy / + track duplication). `ownerNonce` — a per-lifetime nonce minted fresh in memory at + instance creation, never persisted — proves "exactly this incarnation wrote the key + last." `unioned` — a sticky multi-writer poison: once a same-track sibling is + detected, the key enters union-forever mode (holds only accumulate, never drop). + Resolution always leans over-protect: same-nonce + not-unioned → clean replace; + same-track foreign nonce or unioned → union; cross-track foreign nonce → remint under + a fresh key. None of the three directions can under-protect. + - **Deferred follow-up (TODO.md, not this dispatch's scope):** `ownerNonce` is not + persisted, so after save→reopen an instance cannot recognize its own prior-session + usage record — it unions and marks the record `unioned` forever, so prune stops + reclaiming captures the instance once held but no longer uses (safe, but the bank + folder grows unbounded). Persisting the nonce is deferred because a persisted nonce + would be inherited by a Ctrl+D in-place FX duplicate, and a divergent clone must + still be detected and protected fail-safe without reintroducing the sibling-drop bug. + +## Modules + +- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory. +- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge. +- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge. +- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns the `infoNamesFxHotspot` prefix classifier for `GetThingFromPoint` tokens. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure. +- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW. + +## Gotchas + +- `bytes.h`, `ext_state_read.h`, and `reasampler_uid.h` are header-only (no sibling + `.cpp` / no dedicated `_tests` target of their own) — they are consumed directly by + the modules named in their bullets above; don't go looking for a standalone build + target for them. +- `sample_usage` is deliberately silent on liveness *enumeration* (which FX instances + are currently live) — that scan lives in `shell/persist`'s `usage_scan`, not here. This + directory owns only the wire format and the two pure fold/collision decisions. diff --git a/src/shell/actions/CLAUDE.md b/src/shell/actions/CLAUDE.md new file mode 100644 index 0000000..338801c --- /dev/null +++ b/src/shell/actions/CLAUDE.md @@ -0,0 +1,53 @@ +# src/shell/actions — bindable REAPER actions, drag/drop shells, ingest + +## Scope + +The bindable action families routed through REAPER's `command_id`/`gaccel`/ +`hookcommand` contract (Design View toggle actions, bank actions, the prune +action, and the shared registration plumbing/table), plus the OS drag-out and +FX-drop shells, plus the extension-side ingest-through-the-bank shell. This is +where user-facing REAPER actions and OS-level drag/drop live; the underlying +mutation logic (bank verbs, prune's orphan computation, view-mode reconciliation) +is owned by other directories and only skinned here. + +## Invariants + +- **Ingest is an extension act; the instrument is a read-only bank consumer.** Any + instrument code path that captures, imports, inserts a timeline item, or writes + back into the bank is a bug — the instrument reads and plays only. +- **Ingest NEVER inserts a timeline item.** Arrange capture→bank→assign reuses the + existing capture add-path and assigns the resulting `Sample` id to the target + instance; it never places anything on the timeline — capture/placement + separation is load-bearing here same as everywhere else. Only the arrange-capture + surface writes the `assignment_request` wire; Media-Explorer import and file-drop + onto the bank panel do not, and neither affects a live instance's selection. +- **Every bank index verb wraps its mutation in a batched REAPER undo point** + (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is + one Ctrl-Z. +- **The prune action is the ONLY file-deletion action in the system**; it opens no + undo point (file deletion is not REAPER-undoable). It halts on + `abortedUnreadableUsage` and prints the offending `rsusage_*` key names. + +## Modules + +- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). **pS-usage:** `BANK_PRUNE_FOLDER` halts on `abortedUnreadableUsage` and prints the offending `rsusage_*` key names with clear instructions. +- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. +- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.** +- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** + +## Gotchas + +- **Structural wart, not yet fixed:** `ingest.cpp` / `ingest.h`, plus `ext_keys.h` + and `resource.h`, physically live at `src/` root rather than under + `shell/actions/` — Phase Q's reorg did not re-home these files into + `core/`/`shell/`/`app/`. `ingest` is documented here as its nearest sibling by + role, but the files themselves are not in this directory. This is a code + organization issue, not a documentation one — see Open questions in the + originating dispatch report. +- Media-Explorer import is single-file, pull-on-action (`OpenMediaExplorer` + + `MediaExplorerGetLastPlayedFileInfo`) — there is no enumerate-selected-files or + register-a-drop-handler API on the Media Explorer surface. +- REAPER exposes no drag-drop registration API; drop handling is only on + ReaSampler's own HWNDs (`WM_DROPFILES`/`IDropTarget` on the docked `bank_panel`). + A drop onto the VST3 editor window relaying to the extension is an unproven + spike, not a shipped path. diff --git a/src/shell/bank_ops/CLAUDE.md b/src/shell/bank_ops/CLAUDE.md new file mode 100644 index 0000000..0e3abc5 --- /dev/null +++ b/src/shell/bank_ops/CLAUDE.md @@ -0,0 +1,42 @@ +# src/shell/bank_ops — promptless bank-mutation verbs + +## Scope + +The single home for bank-mutation logic: create/rename/delete/evacuate/activate/ +transfer/remove a sample or bank, plus the undo-batched persist that follows a +mutation. No prompts, no message boxes, no panel-state reads — this is the verb +seam that `shell/panel/panel_bank_ops` (menu/prompt UX) and `shell/actions/ +bank_actions` (bindable-action UX) both consume as thin skins, so the mutation +logic has exactly one home. The in-model enforcement of bank rules (pool +privileges, uniqueness, etc.) lives in `core/model` (`bank_book`) — this directory +calls into that model, it does not reimplement its rules. + +## Invariants + +- **Pool privileges are inviolable.** The pool always exists, is un-deletable, is + un-renamable (fixed id + fixed display name "Pool"), and a project can never be + left with zero banks. No verb in this directory may delete or rename the pool, + or evacuate it (the pool is evacuation's destination, not a source). +- **Delete drops members; evacuate returns them.** Deleting a named bank drops its + member index entries only — files are never deleted by a bank op (file lifecycle + stays owned by the capture/prune path). Evacuate moves all of a bank's members + back to the pool (index-only, same destination-collapse-by-hash as move), leaving + the bank empty. A plain delete of a non-empty bank orphans those members out of + every index until prune reclaims them — an accepted, designed window, not a bug. +- **Movement is index-only.** Moving/copying a sample between banks removes/adds + the index entry only; the underlying file never moves on disk. No bank operation + writes, moves, or deletes a file. +- **One bank operation is one Ctrl-Z.** Every bank index verb wraps its mutation in + a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) + via `persistBankOp` — this is enforced in this directory, not left to callers. + +## Modules + +- `bank_ops` (`shell/bank_ops`) — the promptless bank-mutation verb seam (Q-W6 lift out of `panel_bank_ops`): `bankOpCreate`/`Rename`/`Delete`/`Evacuate`/`Activate`/`Transfer`/`Remove` + `persistBankOp` (the undo-batched ext-state persist), each taking a `ReaSamplerSession&` and returning whether the model accepted the mutation — no prompts, no message boxes, no panel-state reads. `shell/panel/panel_bank_ops` (menu/prompt UX) and `shell/actions/bank_actions` (bindable-action UX) both consume these as thin skins, so the mutation logic has exactly one home. + +## Gotchas + +- The pool-privilege rules and uniqueness rules are enforced in the pure + `core/model` `bank_book` layer, not re-checked here defensively — if a mutation + looks like it should be rejected but isn't, the bug is more likely in `bank_book` + than in this seam. Reference `core/model`, do not duplicate its rules here. diff --git a/src/shell/capture/CLAUDE.md b/src/shell/capture/CLAUDE.md new file mode 100644 index 0000000..3b592c4 --- /dev/null +++ b/src/shell/capture/CLAUDE.md @@ -0,0 +1,55 @@ +# src/shell/capture — REAPER-facing capture backends and action bodies + +## Scope + +Everything that turns a capture request into a rendered file + populated `Sample`, +plus the action bodies that drive capture from REAPER's UI/action list: the two +concrete capture backends (offline render, realtime record), scope/source +resolution, the realtime in-flight state machine, single- and batch-capture +orchestration, insert-to-timeline, provenance stamping, and the shared +`MediaItem*`/`MediaTrack*` GUID-read helpers. Pure decision logic (what counts as +an orphan, how a range maps to capture units, etc.) lives in the corresponding +`core/` modules this shell calls into — this directory is the REAPER API surface +only. + +## Invariants + +This directory implements, but does not restate, the repo-wide capture precision +invariants (null test, bit-identical repeats, non-destructive, exact bounds, +relative-paths-only) and the load-bearing capture/placement separation — see root +`CLAUDE.md` §Precision invariants and §The load-bearing principle. Shell-specific +detail not covered there: + +- **FX-bypass guard ordering.** `scope_resolve` reads the M10 provenance-assembly + inputs (track/item selection, FX-chain identity) BEFORE the FX-bypass guard + neutralizes the in-scope chain — provenance must see the chain as it really is, + not as capture temporarily leaves it. +- **Realtime capture drives off REAPER's transport across timer ticks** — + `capture_realtime_shell` cannot block REAPER's UI for the duration of a realtime + record, so `begin`/`tick`/`abort` are async by construction and the temp-track + + send recipe lives in the shell, not the pure core. +- **`RunInsertSelected` is the one deliberate exception to capture-never-places** + (see `capture_orchestrator` below) — every other capture entry point writes only + a file + index entry. + +## Modules + +- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`. +- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain). +- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + owned-manifest record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places). +- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action. +- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload. +- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26). +- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`. +- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.** +- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively. +- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys. +- `item_read` — the ONE place a `MediaItem*` is read for its canonical GUID string (`itemGuid`) and for the durable `P_LANENAME` of the fixed lane it sits on (`itemLaneName`); extracted from previously-duplicated `itemGuid`/`itemLaneName` pairs in `view.cpp` and `bank_panel.cpp` — the item-read analog of `track_guid`'s single `MediaTrack*`→GUID-key formatter. Callers must already know the track is fixed-lane (`I_FREEMODE==2`) before calling `itemLaneName`; the pure `isOnManualLane` predicate handles the non-fixed-lane case separately. + +## Gotchas + +- This directory's governing precision invariants are the repo-wide capture + invariants in root `CLAUDE.md`, not a standalone spec block here. +- `capture` and `capture_realtime_shell` deliberately share NO common interface with + each other (the former `ICaptureBackend` was removed) — do not reintroduce one + without a real second polymorphic call site. diff --git a/src/shell/instrument/CLAUDE.md b/src/shell/instrument/CLAUDE.md new file mode 100644 index 0000000..3cebce0 --- /dev/null +++ b/src/shell/instrument/CLAUDE.md @@ -0,0 +1,105 @@ +# src/shell/instrument — ReaSampler 9000 VST3 shells + +## Scope + +The REAPER/VST3-facing shells for the ReaSampler 9000 instrument: the read-only bank +bridge, the processor, the editor, the embed strip, and the VST3 entry point — plus +two small identity/helper headers this directory owns outright +(`reasampler_vst.h`, `editor_internal.h`). + +The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`, +`sample_map`, `component_state_io`, `zone_params.h`, `editor_geometry`, +`keyboard_strip`, `waveform_view`, `capture_browser`, `browser_scroll`, `note_entry`, +`param_slider`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`, +`curve_popup`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and +`core/wire` and is documented there — this directory consumes it but does not own it. + +## Invariants + +**The build shape (D-A, settled 2026-07-26 — bare Steinberg VST3 SDK + LICE editor).** +Bare Steinberg VST3 SDK, no JUCE, with the editor drawn in the same LICE/SWELL stack +`bank_panel` already uses. `SingleComponentEffect` (the SDK's combined +processor+controller base) plus the SDK's factory macros is the audio-processing +scaffolding. Drawing the editor in a VST3 `IPlugView` that hosts a LICE surface reuses +the `bank_panel` docking muscle, keeps the look house-consistent, and avoids JUCE's +AGPL-or-pay license posture. The `IPlugView`↔LICE bridge (window lifecycle, sizing, +event routing from the host into the draw/hit-test loop) is the same class of work as +docking `bank_panel`, not a new competence. + +**Embedded TCP/MCP UI (D-D) — `reasampler_embed`.** A REAPER-hosted VST3 can draw its +own UI inline in the track/mixer control panel via `reaper_plugin_fx_embed.h` (the +plugin implements `IReaperUIEmbedInterface` — the same Cockos surface REAPER's own +embedded FX use). Because this uses the same LICE-class drawing as the main editor +path, it composes naturally with the bare-SDK-plus-LICE build. **Must-verify:** the +`IReaperUIEmbedInterface` contract and embed message/lifecycle against +`vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`. + +**Channel mode (D-E) — current reality.** An earlier design (D-E, settled +2026-07-26) specified a per-instance mono/stereo toggle negotiating the REAPER +audio bus via `setBusArrangements`/`getBusArrangement`, with mono-source+stereo-mode +→ dual-mono and stereo-source+mono-mode → downmix as the cross-mode policy. **This +was superseded by the GA post-launch DAW-fix pass**: the output bus is now +permanently stereo, `ChannelMode` is decode-only (the dynamic mono↔stereo bus +renegotiation from the earlier design was deleted), and channel mode auto-defaults +from the loaded capture via `ComponentState` v9's `channelModeExplicit` flag + the +pure `channelModeFor` helper. Root `CLAUDE.md` is authoritative for this behavior — +do not reintroduce per-instance bus renegotiation. + +**VST3 channel identity — the UID pair + the pairing surface (S18).** A beta-built VST +pairs with the beta extension only, a stable VST with stable only, both installable +side-by-side in one REAPER — one channel per binary; all channel identity derives from +the ONE `REASAMPLER_CHANNEL_IS_BETA` bit via the pure `app_version` module (no +scattered `#ifdef`s in the VST shell, except the one described below). +- **The UID-pair invariant is a permanent commitment.** The VST3 class UID is the + plugin's identity — a saved REAPER project records it and rebinds a saved instance + by it. BOTH channel UIDs (`reasampler_uid.h`'s stable + beta pairs) are frozen + forever once shipped; the channel bit selects which one is compiled into this + binary (one `DEF_CLASS2`, one class per binary — never both classes in one binary). + The UID selection is the ONLY channel `#ifdef` in the VST shell, because an + `INLINE_UID` needs literal brace-init tokens and cannot route through + `app_version`'s runtime string accessors. +- **Binary + display identity are channel-derived**, sourced from `app_version`'s + VST-name accessors — never a literal in `reasampler_vst.h`/`vst_entry.cpp`. +- **The complete pairing surface is structural, not per-key.** Plugin identity + (UID + filename + display) is channel-forked, and all wire keys live under the + channel-derived ext-state namespace — the two together make pairing complete: no + per-key or per-seam isolation work is ever needed for a new wire key. +- **Verify** all identity/factory wiring against the vendored Steinberg SDK + (`DEF_CLASS2` / `INLINE_UID` / `FUID` from `pluginfactory.h` + `funknown.h`). + +**Non-goals / guardrails.** +- The instrument never captures and never inserts into the arrange. Playback is a + read-only act over the bank. Any instrument path that captures, places a timeline + item, or writes back into the bank is a bug. +- The instrument never ingests. Capture, import, and drop-ingest are *extension* + acts; the instrument only reads and plays. A drop onto the editor window (if ever + shipped) is relayed to the extension as an ingest request — the instrument never + writes the bank itself. +- No cross-platform / multi-format. Windows-only, VST3-only, REAPER-only (D5). Do not + add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target. +- The pure core stays REAPER-free *and* VST3-free — the voice engine / envelope / + keymap / repitch module takes no VST3 or REAPER type at its boundary; the shell + marshals. Any VST3 or REAPER type leaking into `core/instrument` is a bug. +- Verify Steinberg SDK, bridge, embed, and LICE-view surfaces against the vendored + headers before use. + +## Modules + +- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. **pS-usage:** gains `writeUsageExtState` (prefix-guarded — accepts only `rsusage_`-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant. +- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded keymap via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish. +- `reasampler_editor` (`shell/instrument/`: eight face-axis TUs — `editor_session` session/bridge state, `editor_controls` parameter plumbing, `editor_paint_sample`/`editor_paint_browse_zone` paint, `editor_input_sample`/`editor_input_browse_zone` input, `editor_platform` IPlugView/Win32 window plumbing, plus the pure `editor_geometry` layout hoist as the eighth axis; shared internals in `editor_internal.h`, no TU of its own — Q-W2v, T4-11 split of the former god-TU) — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; default face is the capture browser, then single-capture setup, with opt-in zones panel. Drop-onto-editor ingest is NOT shipped (deferred). +- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout/hit-test to `embed_strip`. +- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs. +- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family (Q-W2v split), included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / spectral strip / root marker / title band), label helpers, deck group ids, and the velocity-curve box derivation — the former god-TU's anonymous-namespace helpers that more than one split TU needs. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/editor_internal.h`'s own header comment and body.)* +- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)* + +## Gotchas + +- `editor_internal.h` is include-only — it has no TU of its own and must never become + a public seam; only the eight `reasampler_editor` face-axis TUs include it. +- The two VST3 class UIDs (`core/wire/reasampler_uid.h`, consumed via + `reasampler_vst.h`) are FOREVER-FROZEN — never regenerate an already-shipped UID. +- The UID selection `#ifdef` in `reasampler_vst.h` is the one deliberate exception to + "channel identity derives from `app_version` accessors, no scattered `#ifdef`s" — + `INLINE_UID` needs literal brace-init tokens, so it can't route through a runtime + string accessor. diff --git a/src/shell/panel/CLAUDE.md b/src/shell/panel/CLAUDE.md new file mode 100644 index 0000000..59ac294 --- /dev/null +++ b/src/shell/panel/CLAUDE.md @@ -0,0 +1,70 @@ +# src/shell/panel — the docked bank-panel shell + the shared LICE draw kit + +## Scope + +The REAPER-facing shell for the docked bank panel: the eight `bank_panel` split TUs +(`panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / +`panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via +`panel_state.h`), plus `draw_kit`, the shared LICE draw shell also consumed by the +VST3 editor (`shell/instrument/`). + +Pure layout/hit-test/palette modules the panel draws through (`theme`, +`component_geometry`, `bank_grid`, `tab_strip`, `mode_switch`, `action_bar`, +`footer_bar`, `overflow_menu`, `prune_button`, `mode_enable`, `tooltip`, `card_drag`, +`card_meta`) live in `core/ui` / `core/model` and are documented there — this +directory consumes them but does not own them. The promptless bank-mutation verbs +(`bankOpCreate`/`Rename`/`Delete`/… + `persistBankOp`) that `panel_bank_ops` skins +live in `shell/bank_ops`, a sibling directory, not here. + +## Invariants + +- **Draw through the kit, by palette ROLE, not hardcoded hue.** The shared LICE-based + drawing kit is the one source of drawing for the whole system — a button, row, + slider, or waveform looks identical in the bank panel, the embed strip, and the VST + editor because it is the same kit function, drawn against palette roles (`bg/base`, + `bg/panel`, `bg/cell`, `accent/primary`, `accent/secondary`, `accent/tertiary`, + `accent/hot`, `text/primary`, `text/dim`, `line/hairline`, `warn`) rather than a + literal color. The kit palette stays abstract (role→color, one constants block), so + a whole visual direction is a single-file change (Phase L, DS-1/DS-2). +- **Dock-panel layout is a thorough redesign, not a light re-skin (DS-3).** The panel + lays out the full button/affordance inventory intuitively and uncluttered, then + applies the kit — but this does **not** restructure the panel bones: the + vertical-split / grid / tab structure is sound and stays as-is; a redesign designs + the layout of the button inventory *around* it, not through it. +- **What Phase L does not change (Precision / invariant implications):** + - The pure/shell split holds — all layout/hit-test stays in pure CTest-covered + geometry modules; the kit's *draw* half is shell, its *geometry* half is pure, + even where a WDL piece is reused. No hit-test math moves into untestable code. + - RT discipline is untouched — the kit is draw-thread only; nothing here touches + `process` or any off-thread reload handoff (a VST3-instrument concern). + - Read-only-over-bank is untouched — this is look-and-feel; no data-ownership + change. + - The capture/placement load-bearing principle is untouched — Phase L draws; it + does not capture, place, or mutate the bank. + - VST3 class UID / component-state contract is unchanged. + - Windows-only (D5) — font/GDI/HFONT choices assume Windows; no cross-platform + font-fallback concern. + +## Modules + +- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Maintenance → Placement via `action_bar`, short labels, More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU. + - `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in. + - `panel_layout` — toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read). + - `panel_render` — the WM_PAINT draw. + - `panel_input` — click/wheel/keyboard routing + the new-content auto-tag timer. + - `panel_drag` — the hover + card-drag state machine + drop dispatch. + - `panel_thumbnails` — the PCM→envelope thumbnail cache + the bank-change fingerprint pass. + - `panel_audition` — the preview-playback engine. + - `panel_bank_ops` — the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. +- `draw_kit` — shared LICE draw shell: `fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`, full interaction-state model, double-buffer preserved. Consumes `theme` + `component_geometry`. + +## Gotchas + +- **No external UI framework.** iPlug2 / JUCE / VSTGUI are rejected (DS-1). LICE + + reused WDL pieces are the toolkit; reject any path that pulls in a new framework. +- **No hit-test geometry in untestable shell code.** Even when reusing a WDL piece + (e.g. a `vwnd` control for a long scroll list), layout/hit-test math stays in pure + CTest-covered modules — do not import a WDL control's retained-mode object model + wholesale, since its controls own their hit-test internally and that would move + geometry into untestable shell code. +- Verify every LICE/WDL/SWELL API name/signature against `vendor/WDL` before use. diff --git a/src/shell/persist/CLAUDE.md b/src/shell/persist/CLAUDE.md new file mode 100644 index 0000000..d534515 --- /dev/null +++ b/src/shell/persist/CLAUDE.md @@ -0,0 +1,60 @@ +# src/shell/persist — project ext-state persistence, prune filesystem I/O, usage scan + +## Scope + +The persist seam: project ext-state read/write (`session` / `ext_state_io`), the +prune path's filesystem half (`prune_fs`), and the extension-side instance-usage +scan (`usage_scan`) that feeds prune's referenced-set. Internal helpers shared only +within the persist TU family live in `persist_internal.h`. The pure orphan +computation is owned elsewhere (`core/reclaim`); the pure usage wire is owned +elsewhere (`core/wire`) — this directory is the REAPER/filesystem-facing half only. + +## Invariants + +- **Prune is the single, exclusive file-deletion authority.** No bank op, no + capture op, no Design View op deletes a file; if any path other than prune + deletes a bank file, reject it in review. +- **Dry-run first, always; no silent deletion.** Prune reports before it deletes + (orphan count, reclaimed size, and — for a small set — the files); actual + deletion is a confirmed second step. No periodic/background sweep. +- **Referenced-set is the union across ALL banks, pool included**, further unioned + (pS-usage) with every live instance's held paths via `usage_scan` → + `prune_reconcile::mergeReferenced`. A file is an orphan iff no bank AND no live + instance references it. +- **Safest platform deletion available.** Trash-preferred, unlink fallback — Windows + routes through `SHFileOperationW` (`FOF_ALLOWUNDO`, verified against SDK + 10.0.26100); macOS/Linux fall back to unlink (no portable SWELL trash surface). + `prune_fs` is the only module that calls this. +- **Manual, explicit trigger only** — a bindable action + a `bank_panel` button, + never a silent background sweep. +- **Instance-usage fail-safe (pS-usage):** a capture held by any live ReaSampler + 9000 instance can never be deleted by prune. If any `rsusage_*` record is + unreadable or ambiguous, prune **aborts entirely and deletes nothing** — + over-protection is the accepted residual, under-protection is a data-loss bug. + `usage_scan` decodes every `rsusage_*` key, enumerates every ReaSampler 9000 FX + instance (all tracks incl. master, normal + record/input chains, containers + recursively, take FX), and folds via the pure `sample_usage::foldUsageRecords` / + `usageHeldPaths` (a record with no live instance context protects all its paths — + identity-failure net, never degrades toward delete). This is read-only at + prune-scan time: `usage_scan` writes no ext-state. +- **`PruneReport` carries `abortedUnreadableUsage` + `offendingUsageKeys`**; dry-run, + orphan-set, and reclaim each independently abort (delete nothing) when usage + state is unreadable. `BANK_PRUNE_FOLDER` (in `shell/actions`) halts on this flag + and prints the offending keys. + +## Modules + +- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, `OwnedManifest` JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `prune_fs` hosts the prune dry-run / full-set orphan queries (supplying `referencedPaths()` + `owned().paths()` to the `prune_reconcile` pure core) and is **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. **pS-usage:** the prune scan unions instance usage via `usage_scan`; `PruneReport` carries `abortedUnreadableUsage` + `offendingUsageKeys`; dry-run / orphan-set / reclaim each independently abort (delete nothing) when usage state is unreadable. +- `usage_scan` — extension-side prune-scan shell (pS-usage): at prune-scan time, enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and folds with `sample_usage::foldUsageRecords` / `usageHeldPaths` to produce the set of held paths — or `abortPrune` when any record is unreadable (fail-safe: an unreadable record may protect anything, so the prune halts). Feeds `prune_reconcile::mergeReferenced`. Read-only: writes no ext-state. +- `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, project-dir derivation, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses. + +## Gotchas + +- The pure orphan computation (`prune_reconcile`, `(owned ∩ present) − referenced`) + is documented under `core/reclaim`, not here — do not duplicate its spec in this + file. +- The pure usage wire (`sample_usage`: `UsageRecord`, `planUsagePublish`, + `foldUsageRecords`/`usageHeldPaths`, `identityMatches`) is documented under + `core/wire`, not here. +- `persist_internal.h` is an internal seam, not a public header — do not include it + outside `session.cpp` / `ext_state_io.cpp` / `prune_fs.cpp`. diff --git a/src/shell/view/CLAUDE.md b/src/shell/view/CLAUDE.md new file mode 100644 index 0000000..6144b6f --- /dev/null +++ b/src/shell/view/CLAUDE.md @@ -0,0 +1,85 @@ +# src/shell/view — Design View mode application shell + +## Scope + +The REAPER-facing half of Design View: applying a mode's visibility/processing +state to live tracks (park/restore), snapshotting flag values before parking, and +restoring from snapshot on toggle-back. The mode registry, membership derivation, +and the pure park/restore planner are owned by `core/view` (`view_mode_model`) — +this directory is the shell that reads/writes REAPER track flags, it does not +decide membership or mode rules. + +## Invariants + +- **Never touches master or `B_MUTE`/`I_SOLO`.** The tool owns only visibility, + `B_MAINSEND`, `I_FXEN`, and per-FX offline, on every managed leaf, tagged or + untagged. User mute/solo survives every toggle untouched; the master track's + visibility flags are never driven (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER` + on master). +- **Parking a track** (inactive-mode leaf) drives `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` + (hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and + `TrackFX_SetOffline(track, fx, true)` for each FX (reclaim CPU) — full CPU-park, + not mix-removal-only. +- **Non-destructive restore.** For every flag the tool drives, snapshot the prior + value BEFORE parking; on toggle-back restore FROM the snapshot, never to a + hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag + to its captured value — the phase's trust anchor, the analog of the capture null + test. +- **GUID-keyed, reorder-safe.** Membership/snapshot keys on track GUID + (`GetTrackGUID`), never track index; tolerates unknown/stale GUIDs (pruned on + reconcile). +- **Relative/portable state only** in the persisted view section (GUID strings, + mode ids — no absolute paths, no index positions). +- **Documented caveat:** offlined FX re-instantiate when a track returns to the + active mode — stateful plugins (convolution, loaded samplers, tail-holding + effects) re-initialize on return (possible load hitch, un-persisted internal + state lost). Accepted cost of the CPU reclaim; surfaced at the toggle affordance + (tooltip). +- **Show-both semantics:** a per-track "pin visible across modes" flag re-enables + processing whenever shown. A show-both leaf appears in every mode's visible set + and is never parked — its driven flags stay at snapshot/restored values, FX + online, in the mix. ("Show but keep parked" is not offered.) Stored on the + membership record; persists; togglable per selection. +- **No literal second canvas.** A literal second arrange surface, a second window, + or a duplicated project stays rejected — reject any such path in review. + +**Two-canvas sub-phase (Phase D2/E) — settled and landed parts, DAW-application half:** + +- **Fixed-lane item-level separation mechanics.** Map mode → lane; toggle drives + per-lane play/show so only the active mode's lane is present. Items keep their + real position and real track — nothing is moved in time or deleted. SDK surface + (verified present in `vendor/reaper-sdk`): track-side `I_FREEMODE = 2`, + `I_NUMFIXEDLANES`, `C_LANEPLAYS:N`; item-side `I_FIXEDLANE`, `C_LANEPLAYS`, + `B_FIXEDLANE_HIDDEN`. `I_FREEMODE` changes require `UpdateTimeline()` to take + visible effect. +- **Inactive-mode content is hidden AND silenced.** The off-mode lane is set + `C_LANEPLAYS = 0` — neither shown nor played — consistent with exclusive + membership and with D1's "flipping modes is a real change, not cosmetic." + Show-both is the deliberate opt-out for a lane that must stay audible across + modes. +- **Capture placement is mode-aware.** An explicit placement while in Design mode + — including capture-and-place — lands the item in the Design lane; the same + rule governs manual insertion. The capture load-bearing principle is untouched: + capture still writes a file + index entry and never auto-inserts; this governs + only *where* an explicit placement lands. +- **REAPER floor: v7** for this sub-phase (fixed lanes shipped in v7); no + version-gate branch — below v7 the sub-phase is simply unavailable. + +Item→lane membership rules (the adoption rule, exclusive-per-item membership, the +managed/manual lane distinction, and the lane-ownership index) are model concepts +owned by `core/view` — see that directory's Invariants; this directory only +applies the resulting lane state to live tracks. + +## Modules + +- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. **Never touches master or `B_MUTE`/`I_SOLO`.** + +## Gotchas + +- The pure mode model (`ViewModeModel`, membership, `reconcile(liveGuids)`, the + snapshot-based park/restore planner) lives in `core/view` — reference it, do not + duplicate its spec here. +- The Two-canvas sub-phase (Phase D2/E)'s settled DAW-application rules + (fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are + reflected in Invariants above; the membership/lane-ownership model concepts + it also covers live in `core/view`'s Invariants.