# 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` — the `ICaptureBackend` interface plus `OfflineRenderBackend` and `RealtimeRecordBackend`. 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 `commitMapAndReload` → 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.