Files
reasampler/PLAN.md
T
daniel fca311b4bd docs(product): spec Phase L L4 — dock-panel button layout enhancement
Capture+placement move to a top toolbar; Design View tag/switch verbs to the bottom toolbar; footer gains a narrow Arrange|Design toggle + a Tail button (was a click-zone), with warn Prune set apart at the right. Ungated/extension-side; sequences after the palette revision. Layout only.
2026-07-26 21:59:28 -04:00

49 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 over the host callback (hostcb opcode 0xdeadf00d), fetch host project context (0xdeadf00e), and read a known "reasampler" ext-state value. Verify opcodes + marshalling against reaper_plugin.h / video_processor.h / reaper_plugin_functions.h.

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.
  • 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).
  • Sample selection UI (minimal, in the IPlugView LICE editor or a parameters-only default view): choose which bank sample this instance plays.
  • Tier-0 playback: chromatic-from-root, basic polyphony, amp envelope, velocity→volume — plays in REAPER's routing/record/render path like any VSTi.

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 (read/written over the live "reasampler" seam per D-B's data-ownership split), 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.

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 bridge — opcodes 0xdeadf00d (resolve-by-name) / 0xdeadf00e (host context) and the exact call marshalling, against reaper_plugin.h / video_processor.h / reaper_plugin_functions.h.
  • IReaperUIEmbedInterface — embed contract + message/lifecycle, against reaper_plugin_fx_embed.h (needed only at S6).

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) and L2 (dock-panel layout redesign) 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). See COMPLETED.md. L3 and L4 remain — L3 gated on Phase S landing on dev; L4 (dock-panel button layout enhancement) ungated and extension-side, sequencing after the in-flight palette revision merges to 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 editor/embed surfaces (≈ S1 / S6 / S10) 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 S10S13 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 S10S13 UX), the kit is the one source of drawing: if S10S13 reach dev before L1, they draw in the current language and L3 restyles them; if L1 lands first, they are born in the kit. 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.

  • 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. (Gated — Phase S surfaces must be on dev.)
  • Restyle the editor + embed components through the kit (buttons, sliders/ADSR, 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 (hue-mapped zones as a pastel sweep anchored on the three accents, glow-on-active as a static drawn state — never a pulse, per the speed constraint) as the signature surface; VST3 class UID unchanged. (Gated.)

L4 — dock-panel button layout enhancement (ungated, extension-side)

Goal: Re-home the bank_panel's L2 button inventory around frequency and intent, so the two acts the tool exists for (capture, placement) sit at the top where the eye lands, the Design View organize/switch verbs get a real button home at the bottom, and the footer carries the small persistent controls (the Arrange|Design toggle, Tail, Prune) in a coherent left-to-right order. This is a layout re-home of buttons that fire existing actions — no new actions, no capture/placement behavior change, no touching the "capture ≠ placement" principle. It is independent of L3 (which stays gated on Phase S); L4 is ungated, extension-side only (the dock panel lives on dev). Product framing: docs/product/visual-design-language.md §L4 (new) + CONTEXT.md §Phase L "L4 dock-panel button layout".

The four layout moves (Daniel, 2026-07-26):

  1. Capture + placement clusters move to a TOP toolbar. The capture cluster (capture item, capture track, batch items, batch razor, capture RT) and the placement cluster (insert, insert-conform) leave the current bottom L2 action bar and become a top toolbar — the first thing the eye meets, matching their frequency and the tool's purpose.
  2. The bottom toolbar is repurposed for Design View tagging + switching. In the space the capture/placement buttons vacate, place 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 registered actions today (the Design View action family); L4 gives them a button home in the bottom toolbar.
  3. The Arrange|Design segmented toggle shrinks and moves to the footer. The large top [ Arrange | Design ] segmented control shrinks to just wide enough for its text comfortably and moves into the footer, to the LEFT of the Prune button (see footer order below). The per-mode membership count travels with it (as a compact adjacent label).
  4. Tail becomes a real button. The Tail footer affordance is a click-zone today (clicking cycles the tail setting) — inconsistent with the other footer controls. Convert it to a proper button drawn through the L1 kit like the others; click still cycles the tail setting, but it now reads and behaves as a button (rest/hover/pressed states).

Footer affordance order (left → right): [Arrange|Design] toggle · Tail button · … · Prune (set apart at the right, warn). Rationale: the two view/session controls (mode toggle, tail) group at the left as the "how this panel/capture behaves" cluster; Prune stays isolated at the far right, visually separated and warn-colored, because it is the only byte-deleting affordance and must never sit adjacent to a benign toggle where a mis-click is cheap. Order reads left (benign, frequent) → right (destructive, rare).

Verify (in DAW): the top toolbar fires every capture + placement action; the bottom toolbar tags/untags selected tracks and switches/toggles Arrange/Design/show-both; the footer shows a narrow [Arrange|Design] toggle at the left, a Tail button that cycles tail on click with button states, and the Prune button set apart at the right in warn; every surface draws through the L1 kit in the DS-2 grey+pastel palette; capture and placement still never auto-insert (the buttons only fire the existing, unchanged actions).

Depends on: L1 (the kit) + L2 (the landed layout + action_bar module). Ungated by Phase S — extension-side (bank_panel) only. Sequence AFTER the palette revision merges to dev — the in-flight DS-2 grey+pastel palette branch also reworks bank_panel heavily; landing L4 concurrently would collide the same file. L4 begins once the palette revision is on dev. Pure/shell discipline (unchanged): all new toolbar/footer layout math goes in pure CTest-covered geometry modules — extend/mirror action_bar (toolbar row layout) and mode_switch (the now-narrow footer toggle); bank_grid/tab_strip stay owners of their own surfaces' hit-testing. The kit draws; geometry stays pure.

  • Top toolbar: re-home the capture cluster (item / track / batch items / batch razor / capture RT) + placement cluster (insert / insert-conform) from the bottom L2 bar to a top toolbar (pure row layout in action_bar; kit draw). Buttons fire the existing actions unchanged — no auto-insert.
  • Bottom toolbar: place the Design View action family as buttons (tag / untag selected for mode, activate Arrange, activate Design, toggle active mode, show-both), firing the existing registered Design View actions.
  • Footer: shrink the [ Arrange | Design ] segmented toggle to fit-its-text width and move it to the footer left of Prune (narrow-segment geometry in mode_switch); carry the per-mode count as a compact adjacent label.
  • Footer: convert the Tail click-zone into a proper kit button (rest/hover/pressed states; click cycles the tail setting as today).
  • Footer order: [Arrange|Design] · Tail · … · Prune (warn, set apart at the right); pure footer-strip layout covered by CTest.

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; ungated, extension-side;
                        sequences AFTER the palette revision merges to dev)

Phase S (separate team, ungated): S-work builds with current drawing, adopts L1 when it lands.

L1 and L2 have landed. L3 waits for Phase S to reach dev. L4 is ungated and extension-side but sequences after the in-flight palette-revision branch merges to dev (both rework bank_panel). 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 against the post-palette-revision bank_panel: confirm the landed action_bar row-layout module and the mode_switch segmented-toggle geometry extend cleanly to a top toolbar + a narrow footer toggle, and that the Design View action family's command ids are all live for the bottom-toolbar buttons. Do not build until the palette revision is on dev (shared-file collision).

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). Phase Q is gated on the tree being otherwise quiescent. It does not begin until Phase S has merged to dev, Phase L (L2 + L3) has merged to dev, any D2 residuals are closed, and M9 is landed-or-abandoned. Why: Phase Q touches nearly every file in src/ (relocate into subdirectories, re-namespace every header, split the four largest TUs). Every large in-flight branch (Phase S on its worktree, Phase L's bank_panel-touching L2/L3) 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.

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

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.

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.
  • 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/.
  • 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 (L2+L3) + D2 residuals + M9 all merged/closed to dev (tree quiescent)
  │
  ▼
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), and any other cross-lib name that collides once flattened into granular namespaces. Resolve by each symbol's new subsystem home before landing W1. (audit §2.4; docs/product/code-organization.md §6 Q-4.)
  • The GATE — do not begin any Q point until Phase S + Phase L (L2/L3) + D2 residuals + M9 are merged/closed and the tree is quiescent. Re-confirm quiescence against dev before W1.