Files
reasampler/PLAN.md
T
daniel 3d6c0ab66a docs(product): system-wide visual design language (S0-DS kit + S14 panel refresh)
LICE verified sufficient from vendored WDL — current editor draws at its
floor (flat fills, raw GDI text); no framework shift needed, D-A holds.
Adds design-language doc (palette, three directions, motion policy),
S0-DS shared drawing kit before S10, S14 panel refresh.
2026-07-27 04:09:49 -04:00

93 KiB
Raw Blame History

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.

Milestone 9 — slots (MPC-style)

Explicitly deferred (Daniel, 2026-07-26) — not silently dropped. Can be picked up later if wanted. An extra, not scheduled work.

Goal: "Capture to slot N" / "insert slot N", MIDI-bindable. CONTEXT.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.

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)

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. When a point lands, doc-keeper moves it to COMPLETED.md.

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 COMPLETED.md. 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.


Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact)

New pillar, own lettered namespace, and — uniquely — a second build artifact. Every prior phase 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 separate native VST3 plugin the user instantiates on an instrument track, reading ReaSampler's banks and playing them MIDI-triggered. Namespaced S (Sampler) rather than "D" (Daniel's call — "D" collides with Design View). Authoritative spec: CONTEXT.md §MIDI-playback instrument — additive phase spec (Phase S). Product framing + the settled decision record (D1/D5/D6 locked, D-A..D-D settled 2026-07-26): docs/product/midi-playback.md. When a point lands, doc-keeper moves it to COMPLETED.md.

Locked (see docs/product/midi-playback.md §4): D1 native VST3 (not JSFX); D5 Windows-only / VST3-only / REAPER-only; D6 two products, tightly integrated via the VST-host bridge (live "reasampler" ext-state, project-aware). Settled forks (2026-07-26): D-A bare Steinberg VST3 SDK + LICE editor (no JUCE); D-B split seam with root-note + loop-points added to Sample now; D-C Tier 01 committed (Tier 2 held, Tier 3 optional-forever); D-D embedded TCP/MCP UI scheduled as a later in-phase point (after the main editor exists).

Second build artifact (load-bearing, flagged up front): Phase S produces a separate VST3 binary alongside reaper_reasampler. The Steinberg VST3 SDK is a new vendored dependency (vendor at the spike — an implementation-time prerequisite, not done here), and CMake grows a second target with Windows VST3 module-export/bundle wiring. Both are established by S1 so nothing downstream leans on an unbuilt target.

S1 — opening spike: VST3 skeleton + IPlugView↔LICE bridge (proof + second target)

Goal: Stand up the second build artifact and prove the two least-trodden unknowns before the engine build leans on them: (1) a silent-but-loading VST3 SingleComponentEffect skeleton that REAPER hosts, and (2) an IPlugView that hosts a LICE-drawn surface. Converts §1a's experienced-estimates (Windows module-export names, factory-macro spellings, exact bridge marshalling) into verified fact. CONTEXT.md §Phase S (build shape, module architecture, API surface). Prerequisite (implementation-time): vendor the Steinberg VST3 SDK (a new submodule/dependency alongside reaper-sdk / WDL); confirm whether VSTGUI is bundled (moot for D-A but resolves the noted fallback rung). Verify (in DAW): the VST3 skeleton loads in REAPER on an instrument track, enumerates via GetPluginFactory, sets up an event-in + audio-out bus, and runs an empty process without error; an IPlugView opens and draws a LICE surface with a working hit-test; the VST-host bridge resolves GetProjExtState by name and reads a known "reasampler" value. Nothing plays yet — this is the loading/drawing/bridge proof.

  • CMake second target: a separate VST3 module artifact built alongside reaper_reasampler (Windows VST3 export/bundle wiring; GetPluginFactory + InitDll/ExitDllverify exact export names against the vendored SDK).
  • SingleComponentEffect skeleton: factory + class registration, initialize declaring an event-input bus + an audio-output bus (no audio input), setupProcessing, setActive, empty process. Loads silently in REAPER.
  • IPlugView↔LICE bridge spike: open a plugin editor window hosting a LICE-drawn surface (window creation/sizing, host→draw/hit-test event routing), reusing the bank_panel LICE/SWELL competence. The decision's one real unknown — prove it here. (VSTGUI is the noted fallback only if this proves gnarlier than the panel work suggests.)
  • Bridge read spike: resolve GetProjExtState/EnumProjExtState by name via IReaperHostApplication::getReaperApi (host project context via getReaperParent(3)) from reaper_vst3_interfaces.h, and read a known "reasampler" ext-state value. (The 0xdeadf00d/0xdeadf00e opcodes are the VST2 path — verified not applicable here.)

S2 — Sample intrinsic fields (root note + loop points; in the extension)

Goal: Add the two bank-intrinsic seam fields to Sampleroot note (MIDI note the sample was recorded at; distinct from the existing optional musical key) and loop points (sustain-loop start/end, sample-accurate, zero-crossing-aware) — as an additive field extension with JSON round-trip, populated at/after capture. This touches the extension codebase, is independently shippable, and lands early to close the backfill cliff before the instrument consumes the fields. CONTEXT.md §Phase S (seam fields, D-B). Same additive shape as provenance (M1). Verify: CTest green. Round-trip lossless across the new fields; pre-existing samples (no root note / loop points) deserialize with clean defaults (no loss, no migration break); capture populates root note where derivable and loop points where set; relative-paths-only unaffected; BankIndex behavior unchanged (purely additive). Depends on: nothing in Phase S (extension-only; can land before or in parallel with S1).

  • Add rootNote (optional MIDI note) + loopStart/loopEnd (optional sample-accurate loop points) to Sample; JSON serialize/deserialize with clean defaults for samples lacking them (additive, backward-compatible — mirror of how provenance was added).
  • Populate the fields on capture where derivable (root note) / settable (loop points); leave them cleanly empty otherwise. No existing Sample field changes.
  • Tests: full round-trip lossless including the new fields; a legacy Sample JSON (no new fields) parses with defaults and re-serializes without loss; additive invariant (no change to existing fields, dedup, tier, or BankIndex behavior).

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.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).

  • Voice engine: polyphonic voice allocation (note-on/off, bounded voice stealing), per-voice state, mono-and-basic-polyphony sufficient for Tier 0.
  • Amplitude envelope (ADSR) math — asserted against a known signal.
  • Repitch/interpolation from root note (chromatic pitch ratio across the keyboard); loop-point-aware sustain for held notes.
  • 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.
  • 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.

  • 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.)
  • 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.
  • 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.
  • 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.

  • 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).
  • 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.
  • 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.

  • Implement IReaperUIEmbedInterface on the VST3; draw a compact keymap/level strip inline in the TCP/MCP using the same LICE surface as the editor.
  • 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.

  • Core channel dimension (pure, S3 extension): SampleData carries N-channel (1 or 2) decoded PCM; Voice::renderFrame and VoiceEngine::render produce a per-channel frame; stereo linear interpolation + loop read per channel. Mono stays the degenerate case (single channel) — no behavior change for existing mono play. Tests: stereo render asserted against a known 2-channel signal; mono render unchanged.
  • Channel-mode toggle as per-instance state: mono | stereo in the instrument's own component state (setState/getState, alongside the selected sample); default preserves current behavior (mono). Cross-mode policy: mono source + stereo mode → dual-mono (same signal both channels, centered); stereo source + mono mode → downmix (the existing decode-side policy). The toggle lives in the instrument, never written to the bank (a performance choice, not a file fact — D-B).
  • Shell: decode fills 1- or 2-channel SampleData per the source's channel count (the S2/bank channel-count intrinsic already exists); the process path renders the active mode's channel count into the output bus.
  • VST3 bus negotiation: implement setBusArrangements so the output bus reports mono or stereo per the instance's channel mode, and REAPER's routing follows automatically (no 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.

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.

  • 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.
  • 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.
  • 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).

S0-DS — shared LICE drawing kit (design-system foundation; BEFORE S10)

Goal: A shared LICE-based design kit — palette + type + component-draw layer — that both the ReaSampler 9000 VST editor and the extension's bank panel draw through, replacing the current flat LICE_FillRect + raw-GDI-DrawTextA "temple os" drawing with a modern dark look (gradient surfaces via LICE_GradRect, anti-aliased rounded controls via LICE_RoundRect/ LICE_Line, cached anti-aliased text via LICE_CachedFont, hover/pressed/drag interaction states). Sequenced before S10 so the keyboard-strip editor is born in the new language, not restyled after. No new dependency — LICE (already the draw surface) is the toolkit; vwnd's drawing craft is cribbed, not its object model (fork DS-1 settled A). Product framing + palette + the three visual directions + all three forks: docs/product/visual-design-language.md. CONTEXT.md §Phase S (design-system foundation). Verify LICE gradient/AA/font surfaces and the LICE_CachedFontHFONT lifecycle against vendor/WDL/WDL/lice/lice.h + lice_text.h before use (surfaces verified present in the design-language doc; confirm signatures at build). Depends on: S1 (the IPlugView↔LICE editor surface the kit draws into) + the existing bank_panel LICE surface. Blocks S10 and S14.

  • Pure theme/palette layer: role→color mapping (bg/base, bg/panel, bg/cell, text/primary, text/dim, accent, accent/hot, warn) selected via one direction constants block (DS-2), so the visual direction is a single-file change. Unit-test that each text-on-surface pair clears its WCAG floor (AA 4.5:1 body / 3:1 large + state indicators) — the "punch to the floor, not past it" rule made testable. No LICE/host types.
  • Pure component geometry/hit-test helpers where not already owned: button rect, slider track/handle geometry, list-row rect + hover hit-test (mirror of mode_switch/embed_strip/editor_geometry — no LICE, no host types, CTest-covered).
  • Shell draw kit (LICE): fillSurface (micro-gradient + 1px inner highlight/shadow — the vwnd trick that kills the flat look), drawButton/drawSlider/drawListRow/ drawWaveform/segmented-switch 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). Double-buffered draw discipline preserved (draw to LICE_SysBitmap, single BitBlt) — the "zero-jank" half of "speed is the selling point."
  • Retire the raw GDI DrawTextA path in the VST editor's drawText/drawTextCentered and the bank panel's GDI text in favor of the kit's cached-font text() — the single biggest "temple os → modern" lever. (This is the drawing-substrate change; the surfaces it serves land in S10S14.)

S10 — direct-manipulation keymap editor ("ReaSampler 9000" UX overhaul, part 1)

Goal: Replace the ±1 nudge-button zone editor with a piano-keyboard-strip zone editor where the keymap is edited by direct manipulation: a horizontal keyboard span along the top, each zone drawn as a bar over the keys it covers; drag a zone's edge to change its low/high note, drag the bar's body to move the whole zone, click a key (or the zone's root marker) to set the root note. No number is ever set by clicking a button 40 times. This is the friction Daniel feels every test pass — the current row of seven mini-buttons per zone is the catastrophe. All layout/hit-test math is a new pure geometry module (keyboard_strip — mirror of mode_switch/editor_geometry); the LICE draw + drag-state machine is the editor shell. RT discipline untouched (edits still commit off-thread via commitMapAndReload). CONTEXT.md §Phase S (ReaSampler 9000 UX — keymap editor). Verify (in DAW): a zone's range is set by dragging its edges on the keyboard strip (not by nudge clicks); dragging a zone's body moves the whole range preserving its span; clicking a key sets/relocates the zone's root (with the repitched result audible on the next held note); overlapping zones render legibly and resolve first-match as before; zone add/select/delete still work; the pure geometry module is CTest-green (edge-grab regions, body-drag delta, key→note mapping, root hit-test) with no host types at its boundary; the ±1 nudge-button row is gone. Depends on: S0-DS (the shared drawing kit — the strip/zone bars are drawn in the kit's new language, and the drag machine's WM_MOUSEMOVE tracking also lights the kit's hover states at near-zero marginal cost), S5 (the PerformanceMap/zone model it edits), S1 (the LICE IPlugView drag/event routing — extends the click-only wndProc to WM_MOUSEMOVE/WM_LBUTTONUP).

  • Pure keyboard_strip geometry module: map a MIDI key span across a strip width (128 keys → pixels, reusing the S6 embed_strip key-span idiom); per-zone bar rect from [lowNote,highNote]; edge-grab hit regions (a few px inside each end = resize handles, the middle = move handle); pixel→note and a keyAtPoint for click-to-set-root; a drag-delta resolver (grabbedField, startNote, dxPixels) → newNote. No VST3/REAPER/LICE types at the boundary; unit-tested outside the DAW (edge grabs, body-move delta, key mapping, clamps low≤high, boundary rounding). Mirror of mode_switch/editor_geometry.
  • Editor shell: draw the keyboard strip + zone bars in LICE (house palette), and drive a drag-state machineWM_LBUTTONDOWN grabs an edge/body/key, WM_MOUSEMOVE updates the in-flight zone against the pure resolver, WM_LBUTTONUP commits via the existing commitMapAndReload (off-thread reload; RT path untouched). Live visual feedback while dragging; a single undo-coherent edit on release.
  • Click-to-set-root + zone move/resize replace the seven per-row nudge/delete mini-buttons; delete stays as one affordance (a small × on the zone bar or a keystroke). The zoneHitTest/±1 nudge path in editor_geometry is retired for the new strip (kept only if a numeric fallback is wanted for accessibility — build-time residual, not a fork).

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.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.

  • 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 commitMapAndReload; 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.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), 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).

  • Scrollable, searchable sample list: a scroll offset (wheel + scrollbar drag) so long banks are fully reachable; a type-to-filter search that narrows the drawn rows to matching display names. Scroll/filter layout + hit-test is pure geometry (visible-row window, scrollbar thumb rect, search-box rect); filter/scroll state is transient UI state.
  • Direct numeric entry for zone low/high/root: a click-to-edit field over the strip (LICE text-entry idiom or a SWELL edit control on the child HWND) so a precise note can be typed, not only dragged. Commits via commitMapAndReload like every other edit.
  • ADSR editor: four draggable controls (attack/decay/sustain/release) over the S3 AdsrParams; per-instance component state (additive to the map/selection blob, version- bumped, back-compat defaults to the current fixed envelope). Slider layout/hit-test pure; the audible envelope change goes through the same off-thread reload.

S13 — drop-to-load: the S8 ingest story, folded into the editor UX (UX overhaul, part 4)

Goal: Make "load a sample into the sampler" one gesture from the editor: dropping an OS file (or REAPER media item) onto the editor window ingests it into the bank and assigns it to this instance — the RS5K "drag a file straight onto it" affordance, which is the single biggest first-impression win RS5K has and we currently lack. This is the editor-window end of S8's option 1 ingest — the instrument does not ingest itself (it stays a read-only bank consumer); the editor's drop handler relays a bank-ingest request to the extension over the agreed cross-artifact seam, and the extension performs the capture/import-into-bank + assign. S8 already flags drop-onto-editor as an unproven cross-artifact spike — this point is where that spike is either proven and shipped or falls back to the docked-panel drop path. CONTEXT.md §Phase S (ReaSampler 9000 UX — drop to load / S8 relay). Cross-artifact relay is a spike, not a promise — sequence after S8. Verify (in DAW): an OS file dropped onto the ReaSampler 9000 editor window ingests into the active bank and this instance plays it (via the S8 capture/import add-path + S9 refresh, or a direct reload if S9 absent) — never inserting a timeline item (capture/placement separation intact); the instrument itself performs no capture/import/write (the editor only relays the request to the extension); if the cross-artifact relay proves unworkable, the drop-onto-docked-bank_panel path (S8) is the shipped ingest and this degrades cleanly with a clear affordance pointing there. Depends on: S8 (owns the extension-side capture/import + assign, and the relay seam), S1 (the editor HWND that accepts the drop), S9 (hands-free refresh after assign; functional without it via a direct reload). Spike — do not promise the drop-onto-editor path until the relay is proven.

  • 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.
  • Cross-artifact ingest relay (the S8-flagged spike): the editor hands the dropped path + this instance's identity to the extension as a bank-ingest request over the agreed seam (the instrument never writes the bank). Prove the relay mechanism before promising it; if gnarly, fall back to the S8 docked-panel drop path and mark drop-onto-editor deferred.
  • 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.

S14 — dock-panel refresh (extension-side; applies the S0-DS kit to the bank panel)

Goal: Bring the extension's docked bank_panel up to the new design language by applying the S0-DS kit — it is "not terrible" already (thumbnails, selection, focus ring, LICE grid), so this is a re-skin, not a redesign: route its text through the shared LICE_CachedFont (retiring its GDI text), re-draw buttons / tab strip / segmented mode-switch / full-height toggles through the kit's drawButton/segment draw, adopt the palette roles + micro-gradient surfaces, and add hover to tabs and rows. Structure stays; only the drawing upgrades (fork DS-3 settled: light/kit-application only). CONTEXT.md §Phase S (dock-panel refresh). Depends on: S0-DS (the kit). Extension-side and independent of the VST S10S13 chain — can land any time after the kit, in parallel with the VST overhaul.

  • Route all bank_panel text through the S0-DS cached-font text(); retire the GDI DrawText path in the panel.
  • Re-draw the panel's buttons, the named-banks tab strip, the Design View segmented mode-switch, and the pool/banks full-height toggles through the kit's component draws (gradient fills, rounded, inner highlight); adopt the palette roles + micro-gradient surfaces. Hit-test geometry unchanged (bank_grid/tab_strip/mode_switch stay the pure source of truth) — this touches draw only.
  • Add hover state to tab strip and grid rows/cells (the panel's wndProc already runs a timer; add WM_MOUSEMOVE hover tracking). No structural/layout change — the vertical-split, tab, and grid layout are untouched (fork DS-3).

Phase S — design-system forks (Daniel's calls)

Framing + full analysis: docs/product/visual-design-language.md §6. Two of three carry a strong product-designer lean; DS-2 is deliberately low-cost-to-reverse.

  • Fork DS-1 — toolkit. (A) LICE drawing kit (recommended — no new dependency, consistent with settled D-A; crib vwnd's drawing craft, not its object model); (B) vwnd-based retained-mode controls (not recommended — moves hit-test geometry into untestable shell, pulls in image-skin assets; narrow exception: vwnd's listbox for the S12 scroll list only, a build-time residual); (C) iPlug2 / JUCE shift (reject — re-opens settled D-A to solve a look problem that is not a toolkit-ceiling problem; REAPER's own themed UI is LICE). Lean: A. This fork exists mainly to let Daniel rule against the LICE kit if he wants a framework anyway.
  • Fork DS-2 — visual direction. (A) Studio Rack (evolutionary green-on-charcoal, done properly); (B) Neon Console (near-black + one vivid accent, modern soft-synth idiom); (C) Spectral (bold — hue-mapped zone/waveform gradients + glow, turns the multi-zone-in-one- instance advantage into a visual signature). Lean: B default + C's spectral keyboard strip. Because the kit palette is abstract (roles, one constants block), this fork is reversible for ~one file — pick a starting direction, not a permanent one.
  • Fork DS-3 — panel refresh scope. (a) Light — apply the kit (fonts/buttons/palette/ hover), keep the structure (S14 as specced); (b) Structural — also rework the vertical-split / tab-strip / grid layout. Lean: (a) light — the panel's structure is sound; a structural rework is scope Daniel didn't ask for and risks destabilizing landed Phase-B panel work. (b) available later if the panel's layout starts to feel dated once the VST is sharp.

Phase S — product name (ReaSampler 9000)

The MIDI-playback instrument's product name is ReaSampler 9000 (Daniel, 2026-07-26, on DAW-testing the S1S6 instrument). The extension remains ReaSampler; the instrument is ReaSampler 9000. Framing + propagation surfaces: docs/product/midi-playback.md §Product name.

  • 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 (fork below).
  • Fork S-NAME-1 (Daniel's call): the binary filename. Renaming the built VST3 module (e.g. reasampler_9000.vst3) is a compat break — REAPER keys a saved project's plugin reference partly by filename, so a rename can require users to re-select the plugin on existing instances. Lean: keep the current filename, change only display strings (zero compat weight, the name shows everywhere the user reads), unless Daniel wants the on-disk name to match. Flag, do not decide.

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. Today a drag out of the bank_panel becomes an OS file drag once it leaves the panel (M11 — drag_out + drag_out_win, CF_HDROP). This wave adds a second, internal drag mode: while a capture is dragged, a track's TCP FX button lights as a drop zone, and dropping there instantiates a ReaSampler 9000 (the Phase S VST3 sampler) on that track with the dragged capture already loaded and selected for playback. The extension drives the whole gesture itself — REAPER's FX button is not a native plugin-with-file drop target, so this cannot ride the CF_HDROP path. CONTEXT.md §Phase S (drop-and-load — internal-drag hover mode + the FX-button drop → add-VST + load-capture seam). Product framing: docs/product/midi-playback.md (drop-and-load — the third integration gesture). Consistent with the load-bearing principle (make the reasoning explicit): this is an explicit, user-driven placement gesture — the user is deliberately choosing to place a playing instrument on a track, exactly as inserting an item into the arrange is a deliberate placement 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 distinct acts; drop-and-load is a placement-of-the-player gesture, not a capture and not a timeline insert. Two-part mechanism:

  • (a) Internal-drag hover mode. A drag armed with a single capture that stays inside REAPER's own UI (does not cross to Explorer / another app) is tracked by the extension: it detects the pointer hovering a track's TCP FX button, highlights it as a drop target, and on release drives the insert. This is a third DragGesture beyond the existing Internal (bank-to-bank) and OsDrag (M11) — call it InstrumentDrop.

  • (b) FX-button drop → add-VST + load-capture. On drop, the extension adds a ReaSampler 9000 instance to the target track via TrackFX_AddByName (verified present in reaper_plugin_functions.h; signature int TrackFX_AddByName(MediaTrack*, const char* fxname, bool recFX, int instantiate) — use "VST3:ReaSampler 9000" and a negative instantiate to always create a new instance), then pushes the dragged capture's identity into that instance so it plays that sample — via the load-capture seam (below). The ReaSampler 9000 load-capture seam (the hard Phase S coupling — MUST be added): The current Phase S spec gives the instrument a live-state read seam (it reads the bank index + mapping from "reasampler" ext-state via the bridge) but no entry point for an external actor to say "instantiate playing this specific capture." This wave is the reason to add that seam. It is a Phase S dependency, not extension-side, and must land in the instrument before drop-and-load's drop half can work end-to-end. See CONTEXT.md §Phase S for the two candidate seam mechanisms (fresh-instance ext-state handshake vs. VST3 setState preset injection) and the open question on which is chosen. Coexistence with the M11 OS drag-out (disambiguation, load-bearing): the two drag modes are disambiguated by where the pointer goes, not by a mode toggle. Inside the panel client rect → Internal (unchanged). Left the panel but still over REAPER's own window/UI → InstrumentDrop (new — hover-tracks the FX button). Left REAPER entirely (Explorer / another app) → OsDrag (unchanged M11). The M11 decideGesture boundary (pointer left the client rect) is refined, not replaced: leaving the client rect no longer immediately means OS-bound; it means "resolve which of InstrumentDrop / OsDrag by whether the pointer is over REAPER's UI." Single-capture vs. multi-capture also disambiguates — see open questions. 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 (no manual pick step); the OS drag-out to Explorer / another DAW still works unchanged; the internal bank-to-bank drag still works unchanged; no media item is ever inserted into the arrange; the instrument holds no private copy (it reads the one authoritative bank). Depends on: M11 (drag_out gesture machinery — the mode it extends); Phase S S4 (a loadable, playing ReaSampler 9000 instance must exist) AND the new load-capture seam added inside ReaSampler 9000. Composes with — but is distinct from — S8 (ingest through the bank: capture/import/drop into the bank) and S13 (drop-to-load inside the editor). S17 is the third integration gesture: drop onto a track's FX button to instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S artifact, not extension-only.

  • Extend the drag_out pure module with the third gesture: decideGesture (or a successor) returns 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. Pure over (drag state + pointer + panel rect + an "over-own-UI" predicate the shell supplies). Unit-tested — the existing drag_out invariants (M11) must not regress.

  • Shell (extension): hover-track the pointer over REAPER's UI during the drag, resolve the hovered track + its FX button (verify the TCP/FX-button hit surface against the SDK — see must-verify), highlight it as a drop target, and on release drive the drop. Extends the bank_panel drag hook alongside the M11 drag_out_win path.

  • Shell (extension): on drop, TrackFX_AddByName(track, "VST3:ReaSampler 9000", false, /*instantiate*/ negative) to always add a fresh instance; capture the returned FX index; then invoke the load-capture seam to point the new instance at the dragged capture. Batched into one REAPER undo point (Undo_BeginBlock2/EndBlock2) so the whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline).

  • ReaSampler 9000 (Phase S artifact): add the load-capture seam — the entry point that lets the just-added instance be told which capture to play (mechanism chosen per the CONTEXT.md open question). This is the cross-artifact half; it lands in the instrument, not the extension.

  • Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across single- and multi-capture payloads; FX-button hit resolution (pure geometry where it can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged.

Numbering reconciliation (resolved on merge to dev). This wave was authored on dev as a provisional S7 while dev's Phase S ran only S1S6. On merge with the phase-s worktree, the worktree's authoritative numbering (S7 stereo, S8 ingest, S9 change-detection, S10S16) took the lower labels, so drop-and-load was renumbered to S17 — the next free label past the worktree's Phase S set. It stays a distinct integration gesture (drop onto a track's FX button), a sibling of but not the same as S8 (ingest into the bank) and S13 (drop-to-load inside the editor).

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 01 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.

Phase S — must-verify-before-build (carried from CONTEXT.md §Phase S)

  • Steinberg VST3 SDK surface — interface members, base-class overrides, factory-macro spellings, Windows module-export symbol names (InitDll/ExitDll/GetPluginFactory), and whether VSTGUI is bundled. Several are §1a experienced-estimates until S1 confirms them against the vendored SDK.
  • VST-host bridgeopcodes 0xdeadf00d/0xdeadf00e verified (S1): VST3 path is IReaperHostApplication::getReaperApi (resolve-by-name) and getReaperParent(3) (host context) via reaper_vst3_interfaces.h; the 0xdeadf00d/0xdeadf00e opcodes are VST2-only and do not apply.
  • IReaperUIEmbedInterfaceembed contract + message/lifecycle, against reaper_plugin_fx_embed.h (needed only at S6). verified (S6): interface exposed via queryInterface on the controller; inline strip drawn into REAPER's embed bitmap (LICE idiom, no HWND); WM_GETMINMAXINFO size hints confirmed; embed open/close/resize lifecycle clean.
  • VST3 bus arrangement (S7)setBusArrangements / getBusArrangement and REAPER's mono/stereo instrument-bus expectations, against the vendored Steinberg SDK + reaper_vst3_interfaces.h. The channel-mode toggle depends on the output bus re-negotiating cleanly.
  • Media Explorer surface (S8) — confirmed thin against the vendored headers: OpenMediaExplorer (open/select) + MediaExplorerGetLastPlayedFileInfo (read the one last-played/selected file + range) are the whole contract; no enumerate-selected and no ME-drop-handler API. Spike: does MediaExplorerGetLastPlayedFileInfo return a usable path+range for a merely-selected (not-yet-played) file?
  • Drop targets (S8) — REAPER exposes no drag-drop registration API (verified); drop handling is on ReaSampler's own panel HWNDs via SWELL/Win32 (WM_DROPFILES / IDropTarget). Drop-onto-VST3-editor relayed as a bank-ingest request is an unproven cross-artifact spike, not a promise.
  • Bank-generation ext-state read (S9) — confirm 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.
  • Drop-and-load (S17) — three surfaces. (1) TrackFX_AddByNameverified present in reaper_plugin_functions.h (signature confirmed; the "VST3:" name prefix and the negative-instantiate-always-adds semantics are documented in the header comment). (2) TCP / FX-button hit resolution — how the extension resolves the pointer-under-cursor to a track and its FX-button hotspot during a drag: not yet confirmed against the SDK/SWELLGetTrackFromPoint / GetThingFromPoint are candidates to verify against reaper_plugin_functions.h; whether the FX button specifically is addressable (vs. the TCP as a whole) is an open verification. (3) The ReaSampler 9000 load-capture seam — a new interface added inside the Phase S instrument; its mechanism is a Phase S design choice (see CONTEXT.md §Phase S open question), not a pre-existing SDK surface.

Phase L — Look-and-feel (system-wide visual design language)

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. When a point lands, doc-keeper moves it to COMPLETED.md.

L1 (shared LICE drawing kit — the foundation), L2 (dock-panel layout redesign), L4 (dock-panel button layout enhancement), L5 (dock-panel button refinements), L6 (toolbar polish), and L7 (capture ordering, card metadata, and selection styling) have landedtheme/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); 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 COMPLETED.md. L3 remains — L3 gated on Phase S landing on dev.

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 systemaccent/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:17: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.

L3 — VST editor + embed-strip restyle (GATED on Phase S landing on dev)

Goal: Bring the ReaSampler 9000 VST editor (IPlugView LICE surface) and the S6 embed strip up to the settled-and-revised B + three-accent pastel look via the L1 kit: kit cached-font text (§3.1 — the kit's current face, no font change), kit component draws, the Neon-Console palette with REAPER-grey neutrals (#2b2b2b/#333333/#3a3a3a) and the three pastel accents (primary lime / secondary teal / tertiary purple), the pastel spectral keyboard strip as the signature surface (DS-2 revised), and hover/pressed/drag states throughout. CONTEXT.md §Phase L (VST restyle + the Phase S coordination contract). Product framing: docs/product/visual-design-language.md §5.3. 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). Depends on: L1 (the kit) AND — GATE — Phase S landing on dev. The VST editor, embed strip, and keyboard strip live in Phase S, which is not on dev yet (it exists on the phase-s worktree). L3 cannot be built on dev until Phase S's drawing shells (src/vst/reasampler_editor.cpp + reasampler_embed.cpp) merge to dev. The Phase L team must NOT chase these files on dev — they are not there. Until then L3 is a planned, blocked point; L1 and L2 are the live Phase L work.

Coordination contract with Phase S (load-bearing). Phase S's editor/embed UX (S10S13, S15S18) build their interaction UX with the current drawing and adopt the L1 kit when it is available — they are NOT gated on Phase L. Whichever lands first (the L1 kit or the Phase S UX), the kit is the one source of drawing: if the Phase S surfaces reach dev before L1, they draw in the current language and L3 restyles them; if L1 lands first, they are born in the kit. **Reality check (product-designer, 2026-07-27, read-only against the phase-s worktree): the Phase S surfaces did NOT adopt the kit — reasampler_editor.cpp and reasampler_embed.cpp still draw flat LICE_FillRect blocks

  • raw GDI DrawTextA, off a local pre-L1 forest-green palette (kColBackground etc.), not theme/draw_kit.** So L3 lands as a full restyle, not a born-in-kit no-op — all three plan points below are live and confirmed still-needed. Either way there is one kit and one look, and L3 is the point that completes the VST/embed adoption and applies the settled B+spectral treatment.

Landed-scope note (product-designer, 2026-07-27). All Phase S drawing is centralized in exactly two shellsreasampler_editor.cpp (the IPlugView editor) and reasampler_embed.cpp (the S6 embed strip). Every other src/vst/ UI file (editor_geometry, keyboard_strip, waveform_view, capture_browser, param_slider, browser_scroll, embed_strip) is a pure geometry/hit-test module — zero LICE, zero draw (the house discipline held on the branch). So L3 touches two draw shells, but their rendered component set is broader than the original plan named: the editor draws a capture-first browser (search + tabs + thumbnails, S10), channel toggles (S7), waveform + start/loop markers (S11), the keyboard strip + zone bars (S10/S10-Z), and ADSR / pitch sliders + mode toggles (S12/S15/S16). Every one of these is on the restyle surface — enumerated in the points below.

  • Route the VST editor's + embed strip's text through the kit's cached-font text() (the kit's current face — §3.1, no font change); retire their raw GDI DrawTextA path (present in both shells today). (Gated — Phase S surfaces must be on dev.)
  • 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 instead. This is the coordination-contract reconcile: today there are two palettes (the landed theme kit + this local constants block); L3 collapses them to the one kit. (Gated.)
  • Restyle the editor + embed components through the kit (browser search/tabs/thumbnails, channel toggles, ADSR + pitch sliders, mode toggles, zone bars, list rows, waveform, segmented controls) in the B (Neon Console) palette — REAPER-grey neutrals (bg/base #2b2b2b / bg/panel #333333 / bg/cell #3a3a3a) with the three pastel accents (primary lime = live layer; secondary teal / tertiary purple = categorical); add hover/pressed/drag states. (Gated.)
  • 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 lifting to accent/primary + a static glow — never a pulse, per the speed constraint) as the signature surface; also carry the waveform + loop/start markers onto the kit's drawWaveform + warn/accent marker roles; VST3 class UID unchanged. (Gated.)

Phase L — sequencing

L1 (shared kit) ──► L2 (dock-panel layout redesign) [LANDED]
        ├─────────► L3 (VST editor + embed-strip restyle; GATED on Phase S landing on dev)
        └─────────► 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, L4, L5, L6, and L7 have all landed. L3 waits for Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L.

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_CachedFontHFONT 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 COMPLETED.md §L7.
  • L3 restyle surface (L3)PRE-CONFIRMED read-only against the phase-s worktree (product-designer, 2026-07-27); re-confirm against dev once Phase S merges. (1) Both draw shells (reasampler_editor.cpp, reasampler_embed.cpp) still draw flat — LICE_FillRect + raw GDI DrawTextA, no theme/draw_kit/LICE_CachedFont include — so all three L3 points are live (no born-in-kit no-op). (2) There is a second palette to reconcile: a local kCol* forest-green-on-charcoal constants block in the editor shell, divergent from the landed theme roles. (3) Draw is centralized in those two shells only; every other src/vst/ UI file is pure geometry (kit adoption is a shell-side change, no geometry rework). (4) The phase-s branch carries an older L1 kit (its action_bar predates dev's L4L7 theme palette revisions) — after Phase S merges, L3 draws against dev's current theme/draw_kit, so re-confirm the palette roles L3 consumes are dev's, not the branch's stale copy. The single open question is now RESOLVED (Daniel, 2026-07-27): the S18 beta channel title band gets no distinct visual accent — L3 restyles the title band in the standard B pastel palette, and the beta-vs-stable distinction stays purely textual (the channel-derived plugin name, as today), no channel-specific accent color. With that closed, the settled DS-1/2/3 spec forces the rest.

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 the outstanding work is precisely: (1) Phase S merged to dev (the large second-artifact branch, currently on the phase-s worktree — the dominant gate item); (2) Phase L L3 merged to dev (the VST restyle, itself gated on Phase S landing on dev). L1/L2/L4/L5/L6/L7 have already landed (see COMPLETED.md) — the earlier "L2 + L3" wording was stale and is corrected here to L3 only. 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 explicitly deferred (Daniel, 2026-07-26), not scheduled work. D2 and M9 are named in the gate only so that reactivating either re-arms the quiescence condition; neither 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. Do not begin any Q point until the gate is satisfied.

M9 disposition (Daniel-decision note). M9 is recorded "deferred indefinitely," which is not the same as "abandoned." Immaterial to the gate (both clear it). It matters only if M9 is ever reactivated: before Phase Q it lands cheaply on the flat layout; after, it is authored against the reorganized tree. Surfaced, not silently resolved — no action unless Daniel schedules M9. (Full note: docs/product/code-organization.md §4.)

Settled (Q-1, this-doc): the phase is Q (Quality); point-id family Q1..Qn, wave prefixes Q-W1..Q-W6. 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 (BankIndexBankModel; the JSON Parserjson::Reader/Writer), leave the merely-quirky (rec); Q-9 align the capture_realtime/realtime_record shell↔core word order during W3 (rec: yes).

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<float>&; 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.

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 → W2W5 god-module splits → W6 OCP finish).

Q-W1 — safe opener: extract core/json + impose the directory/namespace layout on clean modules

Goal: The zero-god-module-risk opener. Two moves: (1) extract a pure core/json module (parser + serializer) and delete the four hand-rolled Parsers in bank_model / bank_book / view_mode_model / owned_manifest (the single largest DRY+SRP violation, and entirely off the hot paths); (2) impose the settled core//shell//app/ directory layout + sub-namespaces (reasampler::model/view/capture/audio/ui/reclaim/version/json) on the 30 clean pure libs + the clean shells that need no splitting — pure relocation, no logic change. Proves the wave discipline (relocate + encapsulate, CTest-green) before any god-module surgery. CONTEXT.md §Phase Q (json extraction; directory + namespace map). Verify: CTest green at every commit. The four duplicate Parsers are gone, replaced by one core/json consumed by all four models; round-trip serialization is byte-identical to before (no format change — a structural dedupe, not a behavior change). Every relocated clean module compiles and its test executable passes unmoved. Sample (model) vs AudioSample (audio) vs unified Parser (json) do not collide once sub-namespaced. No REAPER type crosses into any core/ file; the CMake pure/shell enforcement still holds. Depends on: the GATE (tree quiescent). Nothing else in Phase Q.

  • Extract core/json (pure parser + serializer: parseString/parseInt/parseKey/skipValue + escape, plus emit helpers); unify under reasampler::json; guard the Parser name against cross-lib collision. Off all hot paths — safe to abstract freely.
  • Rewire bank_model, bank_book, view_mode_model, owned_manifest onto core/json; delete the four duplicate Parsers. Round-trip output byte-identical (dedupe, not reformat).
  • Relocate the 30 clean pure libs into core/{model,view,capture,audio,ui,reclaim,version, json}/ and the clean shells into shell/{capture,panel,view,persist,actions}/; move main.cpp to app/. Update CMakeLists.txt src/ paths only (no target-graph change).
  • Apply sub-namespaces matching the directories on every relocated clean module (the god-modules re-namespace their own new TUs as they split, W2W5). Resolve Sample/ AudioSample/Parser homes. This alone resolves the naming collisions (§2b.2): the shared pure-UI rect types (FooterRect/ButtonRect/Selection/CellRect) get one ui:: owner — retire the hand-collision "NAME NOTE" in footer_bar.h.
  • Naming riders (Q-8, if settled): rename the survivor JSON parser to json::Parser (or json::Reader/json::Writer); if Daniel takes the BankIndexBankModel rename, land it here (mechanical class rename, verified by bank_model_tests). No rename on a file this wave isn't already relocating (Q-7).
  • Confirm CTest green + no hot-path change: peaks/audition/realtime-tick untouched by this wave (pure relocation of clean modules; peaks stays a free function).

Q-W2 — split bank_panel.cpp (the biggest god-module, 2424 LOC)

Goal: Split the largest god-module (8+ responsibilities) along the audit's named seams: panel_render / panel_thumbnails / panel_audition / panel_input / panel_bank_ops / panel_window. 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/; 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. Depends on: Q-W1 (directory/namespace layout established). Independently landable.

  • Split rendering (draw*/paint*) → panel_render; thumbnail compute+cache → panel_thumbnails.
  • Split the audio audition/preview engine → panel_auditiondirect call-through, not virtual; preview idle path unchanged.
  • Split input handling (mouse/key/wheel) + new-content detection → panel_input; window lifecycle + OS drag-out/drop-target → panel_window.
  • 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.

Q-W3 — split main.cpp (hoist orchestration; leave main = pointers + entry + dispatch)

Goal: Reduce main.cpp (1762 LOC) to its actual job — API pointers + ReaperPluginEntry + dispatch (~the owns-pointers ~120 lines) — by hoisting: capture_orchestrator (RunCapture / captureAndIndexOne / renderOffline / batch/recapture/realtime Run*), scope_resolve (resolveRange/resolveRazorRange/collectSelectedTracks + provenance assembly inputs), and realtime_lifecycle (the realtime-capture state machine + globals + selection guards). FxBypassGuard moves out but stays a stack RAII object (precision-critical); the realtime idle tick stays a single pointer test. 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. Depends on: Q-W1. Independent of Q-W2.

  • Hoist capture orchestration → capture_orchestrator (shell/capture/); keep FxBypassGuard a stack RAII object as it moves (precision-invariant-critical).
  • Hoist scope/source resolution + provenance assembly inputs → scope_resolve.
  • Hoist the realtime-capture lifecycle state machine + globals + the two RAII selection guards → realtime_lifecycle; idle tick stays a single pointer test.
  • Leave main.cpp = API-pointer ownership + ReaperPluginEntry + dispatch; move to app/.
  • Naming rider (Q-9, if settled): align the capture_realtime (shell) / realtime_record (pure) word-order inversion to the house shell↔core convention (rec: stem capture_realtime, shell suffixed) — a free rider since W3 already hoists the realtime lifecycle. No rename on a file this wave isn't already touching (Q-7).
  • Verify in DAW: null test nulls, bit-identical repeats match, capture≠placement holds; CTest green; no realtime-tick branch-shape change.

Q-W4 — split actions.cpp + dedupe bank verbs against panel_bank_ops

Goal: Split the two unrelated command-id families in one TU (981 LOC) 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.

  • Split → design_view_actions (toggle/activate/tag/untag/showBoth/moveItems), bank_actions (bank CRUD family), prune_action (doBankPruneFolder — the single file-deletion action).
  • Dedupe actions.cpp's promptText/mintBankId + bank verbs against panel_bank_ops (one owner); do not change any command-id string.
  • Verify in DAW: all action families fire unchanged; one bank op = one Ctrl-Z; prune still no-undo/no-ext-state; CTest green.

Q-W5 — split persist.cpp (isolate the single file-deletion authority into prune_fs)

Goal: Split persist.cpp (766 LOC, 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. 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.

  • Split → session (lifecycle/poll + projectconfig reload hook), ext_state_io (serialization bridge + GUID minting + folder relocation).
  • Isolate prune scanning + deleteOrphanFile (SHFileOperationW) → prune_fs — the one file-deletion module; nothing else may delete bytes.
  • Verify in DAW: save/load/undo-reload/relocation/prune unchanged; deletion authority is one module; relative-paths-only holds; CTest green.

Q-W6 — OCP registration-table + residual fat-header (I) splits

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). 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).

  • Convert the hand-written Register("command_id"/"gaccel"/"hookcommand") blocks to a data-driven registration table; unload mirror-unregisters from the same table.
  • Split residual fat headers (capture.h/persist.h and any other) alongside their TUs (I).
  • Verify: all actions register/fire/unregister unchanged; command-id strings untouched; CTest green.

Phase Q — sequencing

GATE: Phase S + Phase L L3 merged to dev (D2 complete, M9 deferred) — tree quiescent
      ("when Phase S and L3 are finished" — L1/L2/L4L7 already landed)
  │
  ▼
Q-W1 (safe opener: core/json extract + directory/namespace layout on clean modules)
  ├─► Q-W2 (split bank_panel) ──► Q-W4 (split actions + dedupe bank verbs vs panel_bank_ops)
  ├─► Q-W3 (split main.cpp; hoist orchestration) ──► Q-W6 (OCP registration-table + I splits)
  └─► Q-W5 (split persist; isolate prune_fs)   [best after Q-W4]

W1 is the safe, high-leverage opener (all later waves assume the layout it establishes). The four god-module splits (W2W5) are risk-ordered and mostly parallel-safe; W4 depends on W2's panel_bank_ops, W6 depends on W3's isolated registration code. Big-bang is rejected — every wave is independently landable and CTest-green.

Phase Q — must-verify-before-build

  • 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<float>&; 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-namespacingSample (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 deferred; a reactivation of either re-arms the gate). Re-confirm quiescence against dev before W1.