docs: 1.0 documentation restructure

Split root CLAUDE.md into 19 per-directory files scoped to their source area.
Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md,
PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals
into docs/TODO.md.
This commit is contained in:
2026-07-29 15:09:48 -04:00
parent b34a543b81
commit 1f24c4b095
41 changed files with 2731 additions and 8108 deletions
+106 -89
View File
File diff suppressed because one or more lines are too long
-3258
View File
File diff suppressed because it is too large Load Diff
-1230
View File
File diff suppressed because it is too large Load Diff
-2779
View File
File diff suppressed because it is too large Load Diff
-619
View File
@@ -1,619 +0,0 @@
# PLAN.md — ReaSampler milestone roadmap
Living milestone roadmap for ReaSampler. Derived from CONTEXT.md's 11-step build
order; CONTEXT.md remains the authoritative spec — this file is the tickable
checklist, not a re-statement of the spec. When a point lands, doc-keeper removes
it here and appends it to `COMPLETED.md`.
**Conventions**
- One checkbox `- [ ]` = one discrete, independently-landable point.
- Each milestone opens with a **Goal** (one line) and a **Verify** criterion
(the acceptance gate; precision invariants pulled in where one applies).
- Verify-in-DAW points require a manual REAPER run; pure points are gated by CTest.
- "See CONTEXT.md §…" points at the authoritative detail — do not duplicate it here.
---
## Open questions to resolve during build
Carried from CONTEXT.md §Open questions — keep visible until each is closed by a
landed milestone.
- **`parseInt` narrowing hardening:** `src/bank_model.cpp` `parseInt` casts
`int64_t → int` via `static_cast` without a range check; integers that fit
in int64 but exceed `INT_MAX` are implementation-defined. Hardening candidate
— add bounds check before the cast when integer-field validation is in scope.
- **Capture send/routing isolation (TODO):** The FX-scope capture neutralizes out-of-scope FX, gain, and pan — but NOT aux **sends**. So a downstream coloring send (e.g. a folder → reverb-track send) still routes and blends the reverb into an item/track capture, past the intended isolation point. A true item-level capture should be taken at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan **and** before out-of-scope aux/parallel sends. The hard part: distinguish **source routing that must be preserved** (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from **coloring sends that must be excluded** (folder→reverb). Repro: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded. Likely approach: snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path — needs a rule for which sends are load-bearing.
---
# Phase D2 — Two-canvas (item-level mode projection; additive to D1)
> **Phase D2 is functionally complete** — D2-W1, D2-W2, D2-W3-A, D2-W3-B all landed; see `COMPLETED.md`.
>
> **Deferred:** panel UI indicator for per-track lane/mode state (a per-track
> lane-split marker). The mode switch already shows the active mode; no natural
> cheap home for a per-track indicator was found in the bank panel. Explicitly
> deferred — not silently dropped. Can be picked up later if wanted.
---
# Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact)
> **Landed on dev (merged 2026-07-27); DAW verification pending Daniel's smoke test.**
> S1S18 and the product-name/binary-rename work are all on dev. The cross-artifact
> ingest relay (S13 bullet) was explicitly DEGRADED and remains deferred — see below.
> Completed material archived in `COMPLETED.md`. Authoritative spec: **CONTEXT.md
> §MIDI-playback instrument — additive phase spec (Phase S)**. Product framing:
> `docs/product/midi-playback.md`.
>
## S13 — cross-artifact ingest relay (deferred)
> **SPIKE VERDICT (ps-w12, 2026-07-27): DEGRADED — relay deferred.** The instrument's
> REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; a relay would need a new
> instrument WRITE seam into ext-state and an extension-side timer poller servicing a
> drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race
> the S17 spec rejected for alternative (A). Both the read-only-instrument boundary and
> the new poller are load-bearing design calls, so the relay is deferred to a future wave.
> The shipped ingest gesture stays drop-onto-docked-panel (S8). The degrade path (editor
> shows a "drop files onto the ReaSampler bank panel to add them" affordance) landed as
> part of Phase S. See `COMPLETED.md` §S13 for full context.
- [ ] Cross-artifact ingest relay: the editor hands the dropped path + this instance's
identity to the extension as a bank-ingest request over an agreed seam. DEFERRED —
relay mechanism proved load-bearing to redesign; drop-onto-panel (S8) is the shipped
ingest path. Requires (a) a new instrument WRITE seam into ext-state and (b) an
extension-side timer poller + claim/clear nonce. A future wave when the design is ready.
## Phase S — product name (ReaSampler 9000)
The MIDI-playback instrument's product name is **ReaSampler 9000** (Daniel, 2026-07-26,
on DAW-testing the S1S6 instrument). The extension remains **ReaSampler**; the instrument
is **ReaSampler 9000**. Framing + propagation surfaces:
`docs/product/midi-playback.md` §Product name.
- [ ] **Compat verification (must-DAW-verify before shipping the rename):** the working
assumption is that REAPER **rebinds a saved instance by its VST3 class UID, not by the
module filename**, so a filename rename with an unchanged UID keeps saved projects working
(existing instances still resolve). **This is not yet confirmed from source** — a web
check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's binding is more nuanced
than "UID only" (it can involve an FXID match), so treat UID-rebind as **to-verify, not
asserted fact**. **DAW-verify:** save a project with a ReaSampler 9000 instance under the
old filename, rename the module, reopen — confirm the instance rebinds and restores its
state. If REAPER does key partly on filename, fall back to keeping the current filename
(display-strings-only) and record that as the shipped choice.
## Phase S — held and optional-forever (noted, not specified)
- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun),
full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once
Tier 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. **Note:** S16 lands the *pitch* envelope + the Varispeed/
Preserve pitch-engine mode early (Daniel's directive) — the Tier-3 "filter/pitch envelopes"
line now means the *filter* envelope + LFOs remainder.
- **Sinc Varispeed-quality upgrade (HELD — WDL_Resampler).** `WDL_Resampler`'s sinc mode
beats the core's 2-point linear interp for **Varispeed** base-repitch quality (see the S16
WDL finding). An optional per-voice quality toggle (linear default / sinc), RT-suitable but
heavier. Held as a Tier-2/3 quality option — not needed for S15/S16, not scheduled. (A
resampler couples duration, so it is a Varispeed-quality option only, **not** a Preserve
engine.)
- **WDL_SimplePitchShifter swap (HELD — fork S16-F2 route a).** `WDL_SimplePitchShifter`
as a drop-in swap for the `pitch_shift` pure module if the hand-rolled OLA onset latency
or warble proves musically unacceptable. Same `PitchEngine::Preserve` contract behind the
seam. WDL excluded from the shipped build by include-chain (windows.h); held as the
quality/latency alternative.
- **Trigger choke-on-note-off (HELD — fork S15-F1).** A future option for Trigger mode to
*cut* (choke) on note-off or on a same-group re-trigger (hi-hat open/closed). Deliberately
out of S15 scope (Trigger ignores note-off entirely there); a Tier-3 choke-group direction.
## Phase S — editor view-model redesign (three views: Sample / Browse / Zone)
> **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Re-partitions the ReaSampler
> 9000 editor from a two-view toggle into a **three-view model where the loaded sample is the
> home** — Sample (default face), Browse (modal picker over Sample), Zone (dedicated keymap
> surface). Adds three performance parameters (key-tracking, preview velocity, and the r10
> velocity→amp transfer curve) and three visual components (envelope overlay, real piano-key
> pattern, and the r10 velocity-curve editor), and frames two engineering prerequisites
> (drop-to-FX bug, default window size). An **editor** redesign — the S3 voice engine, keymap
> resolution, and read-only-over-bank contract are **unchanged**; component state extends
> additively for key-tracking; VST3 class UID unchanged. Authoritative spec: **CONTEXT.md §Phase
> S — editor view-model redesign (S-VIEW)**. Product framing: `docs/product/midi-playback.md`
> §Addendum r9. When a point lands, doc-keeper moves it to `COMPLETED.md`.
>
> **Depends on Phase S being on dev** (this redesigns Phase S editor shells —
> `reasampler_editor.cpp` + the pure `src/vst/` geometry modules). **Phase L L3 is FINISHED and
> merged** (2026-07-27, commit `c53683e``reasampler_editor.cpp` + `reasampler_embed.cpp`
> restyled through the L1 kit). S-VIEW builds the new three-view layout **directly on the current
> L3 look-and-feel as its baseline — one implementation pass, styled correctly from the start.**
> There is **no two-pass "land S-VIEW then restyle through L3"** — L3 already happened; every new
> S-VIEW surface (Sample face, Browse modal, Zone surface, envelope overlay + node handles,
> piano-key strip, preview cluster) is drawn through the L1 kit at build time, inheriting the L3
> palette and component-draw grammar. Restyle-after is not a concern.
**Goal:** A three-view editor that makes "pick a capture, tune it, play it" fast, easy, and fun —
Sample is home, Browse is an easy-to-summon modal picker, Zone is the deliberate keymap surface.
**Verify (in DAW):** open on a 1080p screen → full Sample face (hero waveform + envelope overlay +
fenced root + preview-trigger + control strip) with no scroll; Browse opens as a modal over
Sample, select+confirm loads a new capture; Zone opens on its own button, key-tracking + the
piano-key pattern work; drop-a-capture-onto-FX instantiates a playing instance.
### Phase S editor Wave B — Sample-face recomposition (r11; Daniel, 2026-07-27)
> Daniel's post-landing DAW pass on the S-VIEW editor: **all linear sliders → small radial
> knobs** grouped into a fenced knob deck (envelope controls grouped as a unit); the **mode
> toggles compact**, not full-width; the **inline velocity-curve box → a miniature curve
> preview button + full-size popup editor** (right-click deletes a node in the popup); the
> **hero waveform full-width**. Authoritative layout spec: **CONTEXT.md §S-VIEW → "The
> Sample-face recomposition (r11)"** (band order, group taxonomy, cell metrics, popup
> geometry, inventory contract — nothing silently dropped). Product framing:
> `docs/product/midi-playback.md` §Addendum r11. All drawing through the L1 kit; all
> layout/hit-test in pure geometry modules.
>
> **S-VIEW-11, S-VIEW-12, and S-VIEW-13 have all landed** — FB1 (merged 2026-07-27, suite
> 55/55) brought the knob deck + master gain + curve popup + full-width hero; FB2 (merged
> 2026-07-28, suite 55/55) brought Zone-panel parity. **Phase S editor Wave B (r11) is
> complete.** See `COMPLETED.md` for both FB entries.
**Open forks** — **R11-F1** (hero height vs. default window) SETTLED at FB1 build: elastic
hero, 840×620 default kept. **R11-F2** (Zone-panel parity) SETTLED at FB2 build: knob deck
+ curve popup adopted on the Zone panel; `param_slider` slider rows retired on that surface.
### Phase S editor redesign — forks (Daniel's to call)
*(r9/r10 forks all settled: S-VIEW-F1 and S-VIEW-F2 SETTLED 2026-07-27 — folded into S-VIEW-4 /
S-VIEW-3. F1: preview velocity persists via envelope-v6 `ComponentState`. F2: envelope nodes are
draggable via the pure `envelope_edit` module. R10-F1 SETTLED 2026-07-27 — Option A flat y=1,
folded into S-VIEW-9. S-VIEW-F3 SETTLED — full-window overlay, implemented as Browse renders as a
full-window modal over Sample (landed in S-VIEW-1/S-VIEW-5). **r11 forks all settled: R11-F1
SETTLED at FB1 build (elastic hero, 840×620 default kept); R11-F2 SETTLED at FB2 build (Zone
panel adopts knob deck + curve popup, `param_slider` slider rows retired on that surface).**)*
---
# Phase Q — Quality (structural reorganization; zero-runtime-cost)
> **New pillar, own lettered namespace, and — uniquely — the LAST structural pillar.** Phase
> Q is a **pure structural refactor**: it reorganizes `src/` into a healthier shape (more
> encapsulation, granular namespaces, `core/`/`shell/`/`app/` subdirectories) against a stated
> quality bar — *"mtytel Vital is my code reference for quality"* — to bring the codebase
> "into the realm of something I can stand to look at." It ships **no feature and changes no
> behavior**: the test suite passing unchanged is the proof of correctness. Namespaced **`Q`
> (Quality)** — M/D/B/R/V/S/L are all taken; `Q` names the *end* (the quality bar), the reorg
> being the *means*. Authoritative spec: **CONTEXT.md §Phase Q — structural reorganization
> (reorg spec)**. Product framing, the Vital-grounded target shape, the grep-verified SOLID
> audit that is the evidence base, and the settled/recommended fork record (Q-1..Q-6):
> `docs/product/code-organization.md`. When a point lands, doc-keeper moves it to
> `COMPLETED.md`.
>
> **THE GATE (load-bearing — state first; reconciled to reality 2026-07-27).** Phase Q is
> **gated on the tree being otherwise quiescent.** Daniel's plain readiness target: **"when
> Phase S and L3 are finished."** **As of 2026-07-27 both gate conditions are satisfied:**
> Phase S merged to dev (2026-07-27); Phase L L3 (the VST restyle) merged to dev
> (2026-07-27, commit `c53683e`) — **Phase L is complete** (L1/L2/L3/L4/L5/L6/L7 all
> landed, see `COMPLETED.md`). **D2** is functionally complete (D2-W1..W3-B landed; the
> lone open item, a per-track lane-split panel indicator, is *explicitly deferred*, not a
> blocking residual). **M9** (slots) is **abandoned** (Daniel, 2026-07-27) — will not be
> built. D2 is named in the gate only so that *reactivating* its deferred panel indicator
> re-arms the quiescence condition; neither D2 nor M9 blocks the gate today. *Why the gate:* Phase Q
> touches **nearly every file in `src/`** (relocate into subdirectories, re-namespace every
> header, split the four largest TUs, plus the §2b renames). Every large in-flight branch
> (Phase S on its worktree, and L3 once it lands) is diffed against the *current flat
> layout*; landing a rename-and-relocate-everything reorg mid-flight forces every open
> branch through the worst conflict class (every hunk moved, every qualified reference
> changed) — a combinatorial re-resolution, not a linear one. Phase Q is *last* precisely
> because it reshapes the ground every other pillar stands on. Landing it early taxes every
> subsequent phase; landing it last taxes nothing. **The gate is now satisfied — Phase Q
> may begin, entering at the Q-W0 audit wave** (the structural waves Q-W1+ do not begin until
> Q-W0's triage closes and Daniel signs off — see the Q-W0 sub-gate above).
>
> **M9 disposition — resolved (Daniel, 2026-07-27): abandoned.** M9 is out; it will not be
> reactivated. The gate remains satisfied; no re-arm condition applies.
>
> **PRE-RESTRUCTURE AUDIT WAVE (Q-W0 — added 2026-07-27, Daniel's ask; runs FIRST).** Before any
> structural point (Q-W1+) begins, Phase Q now opens with a **functional + DSP quality audit**
> (`Q-W0`) — a thorough static analysis of the code from a *functional-correctness* and
> *algorithm-quality* point of view, complementary to (not a repeat of) the grep-verified
> SOLID/naming audit that already grounds Q-W1..Q-W6. Q-W0 hunts DSP smells (close eye on the pitch
> engine), reinvented wheels, duplicate code, poor-quality algorithms, leaky pure/shell boundaries,
> and domain-modeling smells around env-coupled constants; it produces a written, triaged findings
> report. **The gate to begin Q-W1 is that Q-W0's triage is complete and Daniel has signed off on
> each finding's disposition** (fix-now vs. document-and-defer). Q-W0's findings may add or reshape
> downstream Q-W1..Q-W6 points; fixes that Q-W0 classifies fix-now are remediated in Q-W0 (or folded
> into the wave that already touches the file), **not** deferred silently into the structural waves.
>
> **Q-W0 SIGN-OFF: COMPLETE (Daniel, 2026-07-28).** The audit ran as four parallel tracks (T1 DSP,
> T2 architecture, T3 env-coupled constants, T4 sizing/placement — **59 findings**; report
> `docs/product/code-quality-audit.md`, appendices `docs/product/audit-notes/`), and **all 59
> findings' dispositions are approved as proposed.** The Q-W1 sub-gate is satisfied **once the six
> approved fix-now remediations land** (in flight on branch `pq-w0-fixes`, Q-W0-scoped): T1-01,
> T1-03, T1-09, T2-01(a), T3-01, T3-03. The audit's §3 plan reshape is **folded into the waves
> below** (Q-W2 6→8 seams; NEW wave **Q-W2v** parallel with Q-W2; Q-W3 3→4 hoists + riders; Q-W5
> + the ext-state-loop dedupe), and its §4 decision list is settled — see the settlement block
> below. The Q-11 question is answered by the audit: the SOLA pitch engine is **sound — no
> technique replacement warranted**; every pitch finding is a bounded in-technique fix or a
> documented operating limit.
>
> **Settled (Q-1, this-doc):** the phase is **`Q` (Quality)**; point-id family `Q1..Qn`, wave
> prefixes `Q-W0` (the pre-restructure audit) then `Q-W1..Q-W6` (the structural reorg; **+
> `Q-W2v`**, the VST god-module wave added at the Q-W0 sign-off, 2026-07-28).
> **Settled (Q-10/Q-11, Daniel 2026-07-27):** Q-10 audit-report home = a **committed doc**
> (`docs/product/code-quality-audit.md`, not a tracked issue list); Q-11 pitch-remediation depth =
> **defer to findings** (default document-and-defer; weigh a bounded OLA fix before a technique
> replacement; a technique replacement is a Daniel decision at triage time, not an automatic Q-W0
> action).
> **Recommended, Daniel's to call (Q-2..Q-9, see
> `docs/product/code-organization.md` §6):** Q-2 JSON extraction in scope + first (rec: yes);
> Q-3 directory shape `core/`/`shell/`/`app/` top-split with subsystem dirs beneath (rec: this
> over pure-Vital subsystem-first — it makes the pure/shell invariant *structural*); Q-4
> sub-namespace to match sub-directory (rec: both); Q-5 split god-modules to the audit's named
> seams, no finer (rec: yes); Q-6 OCP registration-table as the final wave (rec: in, last);
> **Q-7 naming rides the relocation waves, no dedicated naming wave (rec: yes — forced once
> Q-3/Q-4 settle); Q-8 class/module renames — fix the two that actively mislead (`BankIndex`
> `BankModel`; the JSON `Parser``json::Reader`/`Writer`), leave the merely-quirky (rec);
> Q-9 align the `capture_realtime`/`realtime_record` shell↔core word order during W3 (rec: yes).**
>
> **SETTLED (Daniel, 2026-07-28 — with the Q-W0 sign-off; the REC record above kept as history):**
> **Q-5 SETTLED** — split to **seams-by-responsibility with the T4 seam lists adopted**
> (`bank_panel` 6→8 seams adding `panel_layout` + `panel_drag`, T4-01; `capture_orchestrator`
> further split with `capture_batch`, T4-02), and the **~600-line file ceiling is an acceptance
> criterion on every split wave** — seams are the method, the ceiling is the bar; arbitrary
> bisection to hit the number is rejected. **Q-6 SETTLED: in scope, last wave, as planned.**
> **Q-8 SETTLED: both renames**`BankIndex``BankModel` (W1) and the JSON parser minted as
> `json::Reader`/`json::Writer` (W1); additionally from the audit, **`ICaptureBackend` is deleted
> in Q-W3** (T4-26 — one deriver, zero polymorphic call sites; the CLAUDE.md/CONTEXT "two
> backends behind one interface" correction **rides Q-W3 itself**, recorded as a rider — the docs
> are not edited before that wave). **Q-9 SETTLED: yes** — align to stem `capture_realtime`,
> shell suffixed, during W3. **VST placement (audit §4a) SETTLED: T4-18**`src/vst/` integrates
> into the single `core/`/`shell/` top split as `core/instrument/{engine,map,ui}` +
> `shell/instrument/` (Q-3 directory map updated in CONTEXT.md §Phase Q). **WAV/RIFF
> consolidation (audit §4e) SETTLED:** a named rider on **Q-W3** — one pure **`wav_codec`** owner
> (walker + layout + build + patch), absorbing the T4-10 ingest extraction. **Q-W2v scheduling
> (audit §4f) SETTLED: parallel with Q-W2** (different artifact, zero file overlap; the serial
> "Q-W7" alternative set aside).
>
> **HARD CONSTRAINT — performance (see CONTEXT.md §Phase Q, `docs/product/code-organization.md`
> §3).** The reorg must cost **zero runtime.** On the three hot paths — `peaks` envelope
> compute, audition/preview, the realtime-capture tick — **no added virtual dispatch, no
> header→TU indirection, no changed call/inline or branch shape.** `computeEnvelope` stays a
> free function on `const std::vector<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.*
>
> **STRUCTURAL HEURISTICS (Daniel, 2026-07-28 — acceptance criteria on every wave; these
> *generalize* the three-hot-path guardrail above, they do not replace it):**
> (1) **More directories is a must, more files is good, ~600-line file ceiling** — SRP applies to
> namespaces, encapsulation, and file organization alike. The ceiling is the *bar*, the audit's
> named seams are the *method*: a file landing over ~600 needs a responsibility seam, not an
> arbitrary bisection; a documented hot-path exception (`sampler_core.cpp`, T4-14/T4-27) is
> legitimate, silent overshoot is not.
> (2) **Templates are the right tool for compile-time dedup — use them where earned** (the LE
> byte codec `putLE`/`readLE`, T4-20), not for name-only unification (the rect family is one
> **concrete** `ui::Rect`, NOT a template — T4-21's ruling).
> (3) **SOLID is great but saved CPU is better** — no dispatch-stack blowouts *anywhere*, not
> just the three named hot paths; prefer static polymorphism where types are compile-time-known
> (T4-27's warning is the canonical case: a by-class `sampler_core` split would put virtual
> envelope `tick()`s on the per-voice-per-sample path).
>
> **NAMING dimension (added 2026-07-27; grep-verified audit in `docs/product/code-organization.md`
> §2b).** Beyond giving symbols a directory + namespace *home* (Q-3/Q-4), Phase Q also gives
> poorly/inconsistently-named symbols a consistent *name*, against the same Vital bar. The audit
> found: four `class Parser` copies collapsing to one `json::Parser` (Q-W1); shared pure-UI rect
> types (`FooterRect`/`ButtonRect`) that the codebase already hand-checks for collision
> (`footer_bar.h`'s "NAME NOTE") — resolved by the Q-4 sub-namespaces for free; the
> `bank_model`/`BankIndex` file↔class word-mismatch (Q-8); and the `capture_realtime`/
> `realtime_record` shell↔core word-order inversion (Q-9). **Renames ride the wave that already
> relocates/splits the file — no dedicated naming wave (Q-7);** the geometry-mirror
> `compute*`/`hitTest*` verb vocabulary and the `_tests` suffix are already consistent and are
> preserved verbatim. Naming changes are zero-behavior-change like the rest of Phase Q, and the
> FOREVER-STABLE contract strings (command ids, action names, ext-state namespace/keys, VST3 UID)
> are **not** C++ symbols and are never renamed.
>
> **Every point is independently landable and CTest-green at EVERY step.** The CMake
> per-module static-lib + per-module test-executable seams already draw the module boundaries;
> a file move + namespace change is mechanically verifiable — `ctest --test-dir build` is green
> or it isn't. **Green-CTest-at-every-point is an acceptance criterion.** Big-bang is rejected;
> the reorg is risk-ordered waves (W1 safe opener → W2/W2vW5 god-module splits → W6 OCP finish).
>
> **PHASE STATUS (2026-07-29): all seven waves (Q-W0..Q-W6 incl. Q-W2v) are structurally
> COMPLETE.** Remaining before the phase closes and merges to `dev`: (1) Daniel's in-DAW
> verification batch — the full deferred list across all waves (panel parity, editor/processor
> parity, stereo Preserve listening, null test, bit-identical repeats, capture flows, action
> families, one-op-one-Ctrl-Z, prune fail-safes, save/load/relocation) — now unblocked since the
> tree is stable; (2) the phase-close CLAUDE.md architecture refresh (module map still describes
> some pre-Q homes); (3) the phase-q → dev merge on Daniel's sign-off. See `COMPLETED.md` for
> each wave's full landed narrative.
## Q-W0 — pre-restructure functional + DSP quality audit (runs FIRST; gates Q-W1)
**STATUS (2026-07-29): audit COMPLETE, triage COMPLETE, sign-off COMPLETE, fix-now
remediations LANDED — Q-W0 is fully closed.** The findings report is committed
(`docs/product/code-quality-audit.md`; track appendices in `docs/product/audit-notes/` — T1
DSP, T2 architecture, T3 env-constants, T4 sizing/placement; 59 findings). Daniel approved
every disposition 2026-07-28. The six approved fix-now remediations plus seven review riders
landed 2026-07-28 (merge `546927e`) — see `COMPLETED.md`. **Q-W1 has since landed on top of
this closure** (see `COMPLETED.md`).
**Goal:** Before a single structural point moves, perform a **thorough static/functional audit** of
the codebase and produce a **written, triaged findings report**. This is the *functional-correctness
and algorithm-quality* complement to the grep-verified SOLID/naming audit that already grounds
Q-W1..Q-W6 (§2/§2b of `docs/product/code-organization.md`) — it goes deeper on *does the code do the
right thing well*, not *where does the responsibility live*. It hunts, across the whole `src/` tree:
duplicate code, reinvented wheels, poor-quality algorithms (**close eye on the pitch engine**),
numerical-robustness and artifact hazards in the DSP paths, leaky pure/shell boundaries, and
**domain-modeling smells around env-coupled constants** (values stored in a frame/rate/DPI/tick-coupled
domain that should be stored rate-free and resolved at use). Every finding is **classified for
disposition**: *eliminate-before-restructure* (fix-now) vs. *document-and-defer* (with rationale).
Nothing structural (Q-W1+) begins until this wave closes and Daniel signs off on the disposition of
every finding. CONTEXT.md §Phase Q (Q-W0 audit scope + triage). See
`docs/product/code-organization.md` §2c.
**Verify:** A findings report exists and is complete over the named audit surfaces (DSP/audio incl.
pitch; architecture smells; env-coupled-constant domain-modeling smells). **Every finding is
triaged** (fix-now vs. documented-and-deferred, each with a one-line rationale). Fix-now findings are
either remediated in this wave or explicitly assigned to the downstream wave that already touches the
file (recorded per finding); no fix-now finding is left implicit. Any behavior-changing remediation
lands with its module's CTest executable green and (where a DSP path changes audibly) a stated
before/after listening or null check. **The gate to Q-W1 is: triage complete + Daniel signed off.**
**Depends on:** the GATE (tree quiescent). Precedes every other Q wave — Q-W1 depends on Q-W0.
- [ ] **DSP / audio audit — close eye on pitch.** Assess *algorithm quality* (correctness,
artifacts, numerical robustness, interpolation quality, reinvented-wheel vs. established DSP
technique) across: `src/vst/pitch_shift` (hand-rolled OLA pitch-preserve — window/overlap choice,
phase handling, transient/formant behavior, buffer edges), `sampler_core` (repitch ratio math,
interpolation order/quality, loop-point crossfade, voice-stealing correctness/click behavior),
`peaks` (envelope min/max binning), `wav_trim` (decay-scan threshold + truncate plan), and the
capture/tail paths. **Pitch-remediation depth (Q-11 SETTLED, Daniel 2026-07-27): defer to
findings** — default document-and-defer; only if the audit surfaces artifacts that matter, weigh a
bounded OLA fix (window/overlap/edge tuning) **before** a technique replacement. A technique
replacement (phase-vocoder / WSOLA) reshapes `pitch_shift`, spills a downstream Q-wave point, and is
a **Daniel decision at triage time, not an automatic Q-W0 action** — Q-W0 surfaces the
recommendation, Daniel calls it.
- [ ] **Architecture-smell audit.** Duplicate code, reinvented wheels, poor abstractions, and any
leaky pure/shell boundary (a `core/` module reaching a REAPER/host type, or geometry/algorithm
math living untestable in a shell). Cross-check against — but do not merely restate — the §2/§2b
SOLID/naming findings; Q-W0's contribution is the *functional* smells those audits didn't target.
- [ ] **Env-coupled-constant domain-modeling audit (explicit smell category).** Flag ANY value
stored in a frame/rate/DPI/tick-coupled domain that should be stored **rate-free and resolved at
the point of use** — per the load-bearing invariant that wall-clock times are rate-free SECONDS
resolved against the live project rate (`sample_map`), with NO hardcoded sample rates in `src/`.
Treat this as **domain-modeling** (store rate-free, resolve at use), **not** "rescale by rate."
There was a prior incident here — envelope times stored in the frame domain — so this is a
first-class category, not a footnote. Sweep envelope times, loop points, fade lengths, tail
lengths, and any UI geometry constant that silently assumes a DPI/rate.
- [ ] **Triage + report.** Write the findings report as a **committed doc,
`docs/product/code-quality-audit.md`** (Q-10 SETTLED, Daniel 2026-07-27 — beside the SOLID/naming
audit, not a tracked issue list); classify each finding fix-now vs. document-and-defer with a
one-line rationale; for each fix-now, note whether it is remediated in Q-W0 or assigned to the
downstream wave that already opens the file. Deferred findings carry a documented rationale so they
are a decision, not an omission.
- [ ] **Sign-off gate.** Daniel reviews the triage and signs off on each disposition. Q-W1 does not
begin until this is done; fold any new/reshaped downstream points the audit surfaces into
Q-W1..Q-W6 before starting them. **DONE (Daniel, 2026-07-28): all 59 dispositions approved as
proposed; the §3 plan reshape and §4 decisions are folded into Q-W1..Q-W6 + Q-W2v below.**
## Q-W1 — safe opener: extract `core/json` + impose the directory/namespace layout on clean modules
> **Landed on `phase-q` (2026-07-29). Integrated suite 60/60 green.** `core/json`
> (`json::Reader`/`json::Writer`) extracted; the five hand-rolled JSON decoders (incl.
> `tail_control`'s) deleted; the wire-`Cursor` family collapsed into `core/wire`; the shared
> `readFileBytes` helper added; ~50 clean modules relocated into `core/{model,view,capture,
> audio,ui,reclaim,version,json,util,wire}/`, `core/instrument/{engine,map,ui}/`,
> `shell/{capture,panel,view,persist,actions,instrument}/`, `app/main.cpp`; sub-namespaces
> applied; one concrete `ui::Rect` + aliases (LTRB fork + `footer_bar` NAME NOTE retired);
> `slot_map` extracted from `bank_book`; `clamp01` deduped; `BankIndex``BankModel`;
> `reasampler_uid.h` relocated to `core/wire/`. See `COMPLETED.md` for the full narrative.
>
> **Skipped/deferred riders:** T4-22 (`hitIndex` hit-test template) — not trivial, deferred as
> an opportunistic follow-on once the rect unification is in use downstream; T4-06
> (`view_mode_model` planner split) — optional, deferred; T4-09 (`view_lanes` split) —
> deferred (in scope only if a later wave touches `view.cpp` anyway).
>
> **Open residual — `bank_book.cpp` still 737 LOC.** The serialize/deserialize seam is
> identified but blocked on a `nameKey` linkage design decision, escalated to Daniel and
> **pending** as of 2026-07-29. Downstream waves touching `bank_book` should check this
> residual before assuming the split is finished.
>
> An interim `core/namespaces.h` shim covers the six not-yet-split god TUs; each downstream
> split wave (Q-W2 onward) retires its own includes of it as that module splits.
## Q-W2 — split `bank_panel.cpp` (the biggest extension god-module — 3459 LOC at the Q-W0 census)
> **Landed on `phase-q` (2026-07-29, merge of `pq-w2-panel`). Integrated suite 61/61 green,
> reviewed-approved.** `bank_panel.cpp` (3459 LOC) split into eight TUs under `shell/panel/`:
> `panel_render` / `panel_thumbnails` / `panel_audition` / `panel_input` / `panel_bank_ops` /
> `panel_window` / `panel_layout` / `panel_drag`, plus per-seam public headers and internal
> `panel_state.h`; audition stays a direct call-through; the one-bank-op-one-undo invariant is
> preserved; ~50 TU-private helpers wrapped in anonymous namespaces (a review follow-up). See
> `COMPLETED.md` for the full narrative.
>
> **Recorded ceiling overages (reviewer-endorsed, preserved as a durable record per CONTEXT's
> "silent overshoot is not legitimate" rule):** `panel_input.cpp` 636, `panel_render.cpp` 613,
> `panel_state.h` 608 — the overage is comment volume; non-comment lines are ~322369 per file;
> no honest seam remains; bisection was rejected.
>
> **Review note for the Q-W4 planning record:** `panel_bank_ops`'s verbs still embed
> prompts/panel-state nudges — Q-W4's dedupe needs promptless inner verbs (`renameBank(id,name)`
> etc.), not a call-site swap; `promptText`/`mintBankId` are byte-identical twins with
> `actions.cpp` and are the cheapest first dedupe.
>
> **In-DAW verification (panel parity) is PENDING on `phase-q`** — deferred by design, not yet
> performed.
## Q-W2v — split the VST god-modules (NEW wave — Q-W0 T4 §1.5; runs parallel with Q-W2)
> **Landed on `phase-q` (2026-07-29, merge of `pq-w2v-vst`). Integrated suite 61/61 green,
> reviewed-approved.** `reasampler_editor.cpp` (3084 LOC, the largest file in the repo) split
> into eight face-axis TUs under `shell/instrument/`, with pure layout hoisted into
> `core/instrument/ui/editor_geometry` (discharges T2-06, newly tested); `reasampler_processor.cpp`
> split into `processor_state` / `processor_reload` / lifecycle+`process()` kept whole (no
> virtual seam, T4-29); `sample_map` split into a resolution core + `component_state_io` codec
> (the extension preset path no longer links the voice engine — link-proven; T4-13 ≡ T2-07);
> `sampler_core.cpp` stays whole with the documented hot-path exception comment (T4-14/T4-27);
> `zone_params.h` split out; `core/wire/bytes.h` (`putLE`/`ByteReader`) lands (T4-20);
> `ThumbnailKey` adopted (T2-10); a golden full-blob v11 fixture pins the codec bytes. The
> `src/vst/` directory is gone. See `COMPLETED.md` for the full narrative.
>
> **Deferred/known:** `component_state_io.h` still includes `sample_map.h``sampler_core.h`
> transitively (T2-07's header half — future work); the `engine` namespace is deferred
> (`sampler_core` stays flat `reasampler`); capture-side LE rewires are left for the capture
> family.
>
> **In-DAW verification (editor/processor parity) is PENDING on `phase-q`** — deferred by
> design, not yet performed.
## Q-W3 — split `main.cpp` (hoist orchestration; leave main = pointers + entry + dispatch)
> **Landed on `phase-q` (2026-07-29, merge of `pq-w3-main`). Integrated suite 61/61 green,
> reviewed-approved.** `app/main.cpp` reduced 1897 → 653 LOC (pointers + entry + dispatch; the
> remaining bulk is the registration residue Q-W6 dissolves) via four hoists into
> `shell/capture/`: `capture_orchestrator`, `capture_batch`, `scope_resolve`,
> `realtime_lifecycle`; `FxBypassGuard` moved intact as a stack RAII object; the realtime idle
> tick stays a single pointer test; `ICaptureBackend` deleted (T4-26) with the
> CLAUDE.md/CONTEXT-ARCHIVE corrections landed in the same commit; the Q-9 rename done (pure
> `core/capture/capture_realtime`, shell `capture_realtime_shell` + `capture_realtime_finalize`
> split, T4-08); `stampCaptureSample` dedupe (T2-09, divergent time-sig behavior preserved via
> caller arg); `makeUniqueTag` gains a per-session monotonic counter (T1-11 behavior fix — stems
> now `<epoch>-<n>` / `rt-<epoch>-<n>`; the per-process residual is documented in-code); one pure
> `wav_codec` RIFF owner absorbs `wav_trim` + `ingest`'s WAV build + content hashes, with golden
> hash literals pinned (`wav_codec_tests` replaces `wav_trim_tests`; `capture_realtime_tests`
> replaces `realtime_record_tests`). See `COMPLETED.md` for the full narrative.
>
> **Known open:** `wav_trim.h`'s transitional forwarding shim still has three live includers
> (`sample_map.h`, `editor_session.cpp`, `processor_reload.cpp`) — repoint-and-retire is a named
> follow-up; `ingest.cpp` is trimmed to 567 LOC but keeps the `namespaces.h` shim (`ingest` +
> `view` remain the shim's unowned consumers).
>
> **In-DAW verification (null test, bit-identical repeats) is PENDING on `phase-q`** — deferred
> by design, not yet performed.
## Q-W4 — split `actions.cpp` + dedupe bank verbs against `panel_bank_ops`
> **Landed on `phase-q` (2026-07-29, merge of `pq-w4-actions`). Integrated suite 61/61 green,
> reviewed-approved.** `actions.cpp` (1019 LOC) split into `design_view_actions` / `bank_actions`
> / `prune_action`, plus a fourth shared `action_registry` TU, all under `shell/actions/`;
> `promptText`/`mintBankId` deduped against `panel_bank_ops`; bank verbs reshaped to promptless
> inner verbs (one mutation home, two UX skins — panel and actions each keep their exact prior
> UX); command-id suffixes/display phrases verified byte-identical in review; `prune_action`
> stays a clean deletion-authority isolate (no `Undo_*`, no ext-state writes);
> `persistBankOp`/`persistBook` gain null-session guards; `promptText` renamed `promptBankName`.
> See `COMPLETED.md` for the full narrative.
>
> **Review note (🟡, resolved in Q-W6):** two session pointers / a null-session-as-model-rejection
> misreport (unreachable today) was resolved by Q-W6's `bank_ops` lift.
>
> **In-DAW verification (action families, one-op-one-Ctrl-Z, prune fail-safes) is PENDING on
> `phase-q`** — deferred by design, not yet performed.
## Q-W5 — split `persist.cpp` (isolate the single file-deletion authority into `prune_fs`)
> **Landed on `phase-q` (2026-07-29, merge of `pq-w5-persist`). Integrated suite 61/61 green,
> reviewed-approved.** `persist.cpp` (853 LOC) split into `session` / `ext_state_io` / `prune_fs`
> under `shell/persist/` + `persist_internal.h`; the file-deletion authority is concentrated —
> `SHFileOperationW`/orphan-remove lives in exactly one anonymous-namespace function in
> `prune_fs.cpp`, verified tree-wide; the prune fail-safe chain stays byte-intact. T2-04's
> `GetProjExtState` grow-loop is unified as a header-only template, with all three hand-rolled
> copies rewired (`usage_scan`'s start cap raised 4KB→64KB, allocation-only, verified
> equivalent). The Q-W1 `bank_book_json` residual lands via a private static `nameKey`
> (Daniel-approved option a) — `bank_book.cpp` is now ~462 LOC. `persist.h` is kept as a compat
> umbrella for parallel safety (retired in Q-W6); deletion-authority wording is scoped precisely
> in headers; the grow-loop gains a defensive NUL. See `COMPLETED.md` for the full narrative.
>
> **In-DAW verification (save/load/undo-reload, ext-state round-trip, folder relocation, prune
> deletion) is PENDING on `phase-q`** — deferred by design, not yet performed.
## Q-W6 — OCP registration-table + residual fat-header (I) splits
> **Landed on `phase-q` (2026-07-29). Integrated suite 61/61 green, reviewed-approved.** Action
> registration/gaccel/hookcommand-dispatch/mirror-unregister all iterate one `ActionTableRow`
> table (flat function-pointer dispatch, no `std::function`/virtual); adding a new action now
> touches one table row only; `main.cpp` shrinks 653→404. FOREVER-STABLE suffixes/phrases/
> retired-ids verified byte-identical row-by-row in review; capture rows derive their
> suffix+phrase from the pure `captureActionTable()` (the parallel-list risk is gone by
> construction). See `COMPLETED.md` for the full narrative.
>
> **Phase-end cleanup riders (landed in this wave):** `bankOp*` verbs + `persistBankOp` lifted to
> new `shell/bank_ops` taking `ReaSamplerSession&` (dissolves the Q-W4 🟡 review note);
> `persist.h` umbrella retired (13 callers repointed); `capture.h`'s realtime seam moved to
> `capture_realtime_shell.h`; the `wav_trim.h` shim + its INTERFACE target deleted;
> **`core/namespaces.h` DELETED** (the interim Q-W1 shim's contract fulfilled — ~26 includers
> rewired); the grow-loop rehomed to `core/wire/ext_state_read.h`; a stale-comment sweep
> (`persist.cpp`/`bank_panel.cpp` refs); CLAUDE.md's persist/bank_book/actions/wav_codec bullets
> corrected in-wave.
>
> **Review-noted follow-on (not landed, deferred):** extending the table pattern to the
> design_view/bank/ingest families' hand-registration; `channelIdFor`'s shared string-store scan
> is correct-by-prefix-disjointness — a suffix-keyed map would make it structural, but isn't
> required; `view_mode_model.h` (748 LOC) remains the largest header (T4-06's planner split
> stays optional/deferred).
>
> **In-DAW verification (all action families, registration/fire/unregister parity) is PENDING on
> `phase-q`** — deferred by design, not yet performed.
## Phase Q — sequencing
```
GATE: Phase S + Phase L L3 merged to dev (D2 complete, M9 abandoned) — tree quiescent
("when Phase S and L3 are finished" — L1/L2/L3/L4L7 all landed — GATE SATISFIED)
Q-W0 (audit + triage + report — COMPLETE; all 59 dispositions signed off 2026-07-28;
│ fix-now remediations LANDED 2026-07-28)
Q-W1 (safe opener: core/json ×5 + wire codec + rect unification + relocation incl. ~20 VST
│ pure libs under core/instrument/{engine,map,ui} + riders — LANDED 2026-07-29)
├─► Q-W2 (split bank_panel — 8 seams — LANDED 2026-07-29)
│ └─► Q-W4 (split actions + dedupe vs panel_bank_ops — LANDED 2026-07-29)
├─► Q-W2v (VST god-modules — editor 8 TUs / processor 3 TUs / component_state_io;
│ sampler_core TU whole — documented exception — LANDED 2026-07-29)
│ [parallel with Q-W2: zero overlap]
├─► Q-W3 (split main — 4 hoists incl. capture_batch; + wav_codec, ICaptureBackend deletion,
│ stamp dedupe, T1-11, capture_realtime_finalize — LANDED 2026-07-29)
│ └─► Q-W6 (OCP registration-table — LANDED 2026-07-29)
└─► Q-W5 (split persist; + ext-state-loop dedupe — LANDED 2026-07-29) [best after Q-W4]
STATUS (2026-07-29): all seven waves (Q-W0..Q-W6 incl. Q-W2v) structurally COMPLETE, 61/61
integrated suite green. Remaining: Daniel's in-DAW verification batch, the phase-close
CLAUDE.md architecture refresh, and the phase-q → dev merge on sign-off.
```
Q-W0 ran and closed 2026-07-28 (its six fix-now remediations landed the same day). W1 was the
safe, high-leverage structural opener (all later waves assumed the layout — including the T4-18
`instrument/` placement — it establishes). The god-module splits (W2, W2v, W3, W5) were
risk-ordered and mostly parallel-safe; **Q-W2v ran parallel with Q-W2** (different artifact, zero
file overlap — audit §4f SETTLED); W4 depended on W2's `panel_bank_ops`, W6 depended on W3's
isolated registration code. Big-bang was rejected — every wave landed independently,
CTest-green throughout. **All seven waves landed on `phase-q` by 2026-07-29 — Phase Q is
structurally complete** (see the phase preamble's PHASE STATUS block for what remains before the
phase closes and merges to `dev`).
## Phase Q — must-verify-before-build
- **Q-W0 closed before any structural point** — the functional/DSP audit's findings report exists,
every finding is triaged (fix-now vs. document-and-defer, each with rationale), fix-now findings
are remediated or assigned to the wave that opens their file, and **Daniel has signed off on every
disposition.** Q-W1 does not begin otherwise. (CONTEXT.md §Phase Q Q-W0; naming/DSP smell
categories §2c of `docs/product/code-organization.md`.) **Status 2026-07-28: triage + sign-off
COMPLETE (all 59 dispositions); the sub-gate closes when the six fix-now remediations land
(`pq-w0-fixes`).**
- **~600-line ceiling on every split wave** — every TU a split wave ships lands under ~600 LOC,
with `sampler_core.cpp` the single documented exception (T4-14/T4-27). Seams are the method,
the ceiling is the bar; arbitrary bisection to hit the number is rejected (Q-5 settlement,
2026-07-28).
- **No dispatch-stack blowouts anywhere** — heuristic (3) generalizes the hot-path guardrail
beyond the three named paths: prefer static polymorphism where types are compile-time-known;
templates only where earned for compile-time dedup (T4-20 yes; T4-21's rect NO-template
ruling).
- **Hot-path call/inline shape** — before landing each split, confirm no virtual dispatch and no
header→TU indirection was added on `peaks` envelope compute, audition/preview, or the realtime
tick. `computeEnvelope` stays a free function on `const std::vector<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-namespacing**`Sample` (model) vs `AudioSample` (audio) vs the
unified `Parser` (json), plus the shared pure-UI rect types `FooterRect`/`ButtonRect` (already
hand-collision-checked in `footer_bar.h`), and any other cross-lib name that collides once
flattened into granular namespaces. Resolve by each symbol's new subsystem home before landing
W1. (naming audit §2b.2; `docs/product/code-organization.md` §6 Q-4/Q-8.)
- **Naming stays zero-behavior-change and off the wire** — the §2b renames touch C++ symbols only;
no `command_id` string, action display name, ext-state namespace/key, `reasampler:` lane prefix,
or VST3 class UID is renamed (naming audit §2b.5). A rename that would alter a shipped contract
literal is out of scope, full stop.
- **The GATE** — do not begin any Q point until **Phase S + Phase L L3** are merged to dev and the
tree is quiescent (D2 complete, M9 abandoned). Re-confirm quiescence against dev before W1.
-17
View File
@@ -1,17 +0,0 @@
# TODO
Forward-looking follow-ups. Deferred by decision, not oversight — each entry records why it was deferred and what "done" looks like.
## Persist ReaSampler 9000 instance identity to let prune reclaim de-referenced captures after reopen
**Context (what shipped — Phase S usage-detection).** Each ReaSampler 9000 instance publishes the captures it holds to project ext-state (`rsusage_<guid>` keys, ComponentState v11). The extension's prune reads those records and unions every live instance's held captures into the referenced-set, so a capture any live instance holds can never be pruned. Fail-safe: unreadable/ambiguous usage state aborts prune (deletes nothing). Airtight on safety.
**The wart.** The per-instance identity token is minted fresh each incarnation and is NOT persisted. After save→reopen, an instance cannot recognize its OWN prior-session usage record — it looks foreign, so the instance defensively unions and marks the record append-only (poisoned). Net effect: after any reopen, prune stops reclaiming captures an instance once loaded but no longer uses. Safe (never deletes a used capture), but the bank folder grows without bound.
**Intended fix.** Persist the instance identity in ComponentState so an instance recognizes its own last-session record and does a clean-replace instead of union/poison → prune reclaims de-referenced instance-touched captures normally.
**The constraint the fix MUST handle (why deferred).** VST3 provides no stable per-instance identity, and Ctrl+D / in-place FX duplication clones plugin state. A persisted identity is inherited by an in-place duplicate → two live instances in one project share one `rsusage_<guid>` key. Harmless while both hold the same capture; the risk is a divergent clone — the copies load DIFFERENT captures, and last-writer-wins drops the other's held capture from the record, exposing it to prune. The fix must detect a genuine live same-identity collision and protect the union in that case, WITHOUT reintroducing the sibling-drop bug the fresh-per-session token was originally added to prevent. (Whole-project copies are a non-issue — bank files are cloned with the project and ext-state is per-project.)
**Priority / risk.** Low / deferred. Current behavior is safe; the only cost is unbounded bank-folder growth after reopens. Decided 2026-07-28 to ship the safe version and defer this.
**Done looks like.** Save → reopen → de-reference a capture from an instance → prune reclaims it. And: in-place-duplicate + diverge + delete-from-bank never deletes a capture a live instance holds.
+1065
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
# COMPLETED.md — ReaSampler landed milestones
Completed milestone entries removed from `PLAN.md`. Each entry preserves its
original Goal, Verify, and checklist points with boxes marked done.
This file holds the current (1.x) cycle's landed milestones only. Nothing has
landed in the 1.x cycle yet. For all pre-1.0 (version-0) history, see
`docs/ARCHIVE.md`.
View File
+133
View File
@@ -0,0 +1,133 @@
# TODO
Forward-looking follow-ups. Deferred by decision, not oversight — each entry records why it was deferred and what "done" looks like.
## Persist ReaSampler 9000 instance identity to let prune reclaim de-referenced captures after reopen
**Context (what shipped — Phase S usage-detection).** Each ReaSampler 9000 instance publishes the captures it holds to project ext-state (`rsusage_<guid>` keys, ComponentState v11). The extension's prune reads those records and unions every live instance's held captures into the referenced-set, so a capture any live instance holds can never be pruned. Fail-safe: unreadable/ambiguous usage state aborts prune (deletes nothing). Airtight on safety.
**The wart.** The per-instance identity token is minted fresh each incarnation and is NOT persisted. After save→reopen, an instance cannot recognize its OWN prior-session usage record — it looks foreign, so the instance defensively unions and marks the record append-only (poisoned). Net effect: after any reopen, prune stops reclaiming captures an instance once loaded but no longer uses. Safe (never deletes a used capture), but the bank folder grows without bound.
**Intended fix.** Persist the instance identity in ComponentState so an instance recognizes its own last-session record and does a clean-replace instead of union/poison → prune reclaims de-referenced instance-touched captures normally.
**The constraint the fix MUST handle (why deferred).** VST3 provides no stable per-instance identity, and Ctrl+D / in-place FX duplication clones plugin state. A persisted identity is inherited by an in-place duplicate → two live instances in one project share one `rsusage_<guid>` key. Harmless while both hold the same capture; the risk is a divergent clone — the copies load DIFFERENT captures, and last-writer-wins drops the other's held capture from the record, exposing it to prune. The fix must detect a genuine live same-identity collision and protect the union in that case, WITHOUT reintroducing the sibling-drop bug the fresh-per-session token was originally added to prevent. (Whole-project copies are a non-issue — bank files are cloned with the project and ext-state is per-project.)
**Priority / risk.** Low / deferred. Current behavior is safe; the only cost is unbounded bank-folder growth after reopens. Decided 2026-07-28 to ship the safe version and defer this.
**Done looks like.** Save → reopen → de-reference a capture from an instance → prune reclaims it. And: in-place-duplicate + diverge + delete-from-bank never deletes a capture a live instance holds.
## Isolate capture from out-of-scope aux/parallel sends, not just FX/gain/pan
**Context (carried from PLAN.md's "Open questions to resolve during build").** The FX-scope capture neutralizes out-of-scope FX, gain, and pan for both item and track scope — root `CLAUDE.md`'s "Capture FX scope" precision invariant states this precisely: the out-of-scope chain (ancestors + master track, plus the item's own track for item scope) has its FX, gain, and pan/width/pan-law/mode neutralized to unity. **Aux/parallel sends are conspicuously absent from that enumerated list** — the invariant as currently written does not cover them, which is the gap this item exists to close.
**The wart.** A downstream coloring send (e.g. a folder → reverb-track send) still routes and blends into an item/track capture, past the intended isolation point. Repro from PLAN.md: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, but should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded.
**Intended fix.** Likely approach (PLAN.md): snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path.
**The constraint the fix MUST handle.** Distinguish **source routing that must be preserved** (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from **coloring sends that must be excluded** (folder→reverb). PLAN.md notes this is "the hard part" and that a rule is needed for which sends are load-bearing.
**Settled rule (Daniel, 2026-07-29).** The classification rule for which sends are load-bearing: (1) **ancestor sends are excluded** from the capture — the folder parent is *up* the folder tree but *downstream* in signal flow, so this item deliberately says "ancestor," not "upstream," which would read backwards to anyone applying signal-flow convention; (2) **sibling sends are preserved and captured** — a sibling is a track sharing the capture scope's parent; (3) **if the send's destination has a different parent, ignore it in the capture** — it isn't a sibling, so it isn't captured. Applied to the repro above: the T1→T2 MIDI send is a sibling send (T1 and T2 share parent F1) and is preserved — T2's synth is where the item's audio is actually produced; the F1→T3 reverb send is an ancestor send and is excluded. This resolves the repro.
**Still open.** The rule above settles *which sends to mute*, but the repro is only fully resolved together with a second, unspecified lever — *where the capture taps*. PLAN.md's own framing of this open question says a true item-level capture should be taken "at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan and before out-of-scope aux/parallel sends." How the mute-rule above and the tap-point interact is not yet specified.
**Priority / risk.** Not stated in PLAN.md (recorded there as an open `(TODO)` question, not yet triaged to a priority). Flagged here as mattering more than the other four carried-over items because it exposes a live gap in a stated precision invariant, not just a deferred feature.
**Done looks like.** Capturing the MIDI item on T1 in the repro above is isolated to T2's synth output pre-F1 — the T1→T2 MIDI send is preserved and the F1→reverb send is excluded — and the CLAUDE.md "Capture FX scope" invariant's neutralization list is extended to name sends explicitly.
## Confirm REAPER's VST3 UID-vs-filename instance rebind behavior (Phase S compat verification)
**Context.** PLAN.md, under "Phase S — product name (ReaSampler 9000)": the working assumption is that REAPER rebinds a saved instance by its VST3 class UID, not by the module filename, so a filename rename with an unchanged UID keeps saved projects working (existing instances still resolve).
**The wart.** This is not yet confirmed from source — PLAN.md records that a web check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's binding is more nuanced than "UID only" (it can involve an FXID match), so UID-rebind is to-verify, not asserted fact.
**Intended fix / required check.** DAW-verify: save a project with a ReaSampler 9000 instance under the old filename, rename the module, reopen — confirm the instance rebinds and restores its state.
**The constraint the fix MUST handle.** If REAPER does key partly on filename, the fallback is to keep the current filename (display-strings-only) and record that as the shipped choice.
**Priority / risk.** Marked "must-DAW-verify before shipping the rename" in PLAN.md; no explicit priority level stated beyond that.
**Done looks like.** The save→rename→reopen DAW check is performed and its result (rebind confirmed, or filename-revert fallback taken) is recorded.
## S13 — cross-artifact ingest relay (deferred, spike verdict DEGRADED)
**Context.** PLAN.md records the ps-w12 (2026-07-27) spike verdict: DEGRADED — relay deferred. The instrument's REAPER bridge (`reaper_bridge`) is deliberately READ-ONLY; a relay would need a new instrument WRITE seam into ext-state and an extension-side timer poller servicing a drop-ingest inbox key with a claim/clear nonce — the same cross-process handshake race the S17 spec rejected for alternative (A). The shipped ingest gesture stays drop-onto-docked-panel (S8); the editor shows a "drop files onto the ReaSampler bank panel to add them" affordance as the degrade path.
**The wart.** Dropping a file directly onto the editor/instrument does not ingest it into the bank — only drop-onto-docked-panel does.
**Intended fix.** The editor hands the dropped path + this instance's identity to the extension as a bank-ingest request over an agreed seam.
**The constraint the fix MUST handle.** Requires (a) a new instrument WRITE seam into ext-state (breaking the current read-only-bridge invariant) and (b) an extension-side timer poller + claim/clear nonce — both are load-bearing design calls that need to be made deliberately, not as a call-site patch. Both the read-only-instrument boundary and the new poller were judged load-bearing enough that the relay is deferred to a future wave rather than pushed through now.
**Priority / risk.** PLAN.md marks this DEFERRED, awaiting a future wave, with no priority assigned.
**Done looks like.** Not stated in PLAN.md beyond "a future wave when the design is ready."
## Phase D2 — per-track lane/mode-state panel indicator (deferred)
**Context.** PLAN.md: Phase D2 is functionally complete (D2-W1, D2-W2, D2-W3-A, D2-W3-B all landed). One item was deferred out of that completion: a panel UI indicator for per-track lane/mode state (a per-track lane-split marker).
**The wart.** The mode switch already shows the active mode, but there is no per-track indicator; PLAN.md records that no natural cheap home for one was found in the bank panel.
**Intended fix.** Not specified in PLAN.md beyond the goal (a per-track lane-split marker in the bank panel) — the design is unresolved, which is part of why it was deferred rather than built.
**The constraint the fix MUST handle.** Finding a home for the indicator in the bank panel's existing layout, which PLAN.md notes doesn't currently have a natural cheap spot for it.
**Priority / risk.** Not stated in PLAN.md. PLAN.md's own framing: "Explicitly deferred — not silently dropped. Can be picked up later if wanted."
**Done looks like.** Not stated in PLAN.md.
## FX-GUID keying for `restoreFxOffline` (Design View park/restore)
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): the bulk of reconcile residuals shipped (`ViewModeModel::reconcile(liveGuids)` prunes orphaned snapshots on every toggle/load; folder restructure is self-healing because the tree is rebuilt each toggle; membership is intentionally kept so undo-delete preserves the tag). Two sub-items were left deferred out of that; this is the first.
**The wart.** `restoreFxOffline` currently restores per-FX offline state by slot index. If the FX chain is reshuffled while a track is parked, restore lands on whatever plugin now occupies that slot rather than the plugin it was originally captured from.
**Intended fix.** FX-GUID keying — key the per-FX offline snapshot entries by FX identity rather than slot index.
**The constraint the fix MUST handle.** The keying change requires a snapshot-schema migration; CONTEXT.md names this alongside the keying change as the reason the fix was deferred rather than folded into the reconcile-residuals work.
**Priority / risk.** Not stated in the source.
**Done looks like.** Not stated in the source beyond the fix description above.
## Dormant membership entries in persisted `view_state`
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section), the second of the two sub-items left deferred after the reconcile-residuals ship described above.
**The wart.** Truly-deleted tracks accumulate stale entries in persisted `view_state`.
**Intended fix.** A future user-initiated "compact" action to remove the stale entries.
**The constraint the fix MUST handle.** Must NOT be automatic pruning — automatic pruning would reintroduce the undo-delete tag-loss that the deliberate membership-retention was designed to prevent.
**Priority / risk.** Not stated as a priority level; the source characterizes the wart itself as "harmless and bounded."
**Done looks like.** Not stated in the source beyond "a future user-initiated 'compact' action."
## Confirm no fight between Design View flags and screenset recall
**Context.** CONTEXT.md's "Open questions to resolve during build" (Design View section): Design View drives the same track flags a screenset recall would drive, and last writer wins between the two.
**The wart.** Not a defect — this is a verification task, not a code change. The open concern is confirming there is no surprising interaction between Design View's flag-driving and an active screenset recall.
**Intended fix.** N/A — no fix is proposed; the task is to confirm no surprising fight between the two mechanisms.
**The constraint the fix MUST handle.** N/A — verification only.
**Priority / risk.** Not stated in the source.
**Done looks like.** Not stated in the source beyond "confirm no surprising fight."
## Active-bank indicator placement (B4 polish)
**Context.** CONTEXT-ARCHIVE.md's "Open questions to resolve during build" (B4 panel section): forks 15 are all settled; one panel-polish detail remains open. Fork 4 already settled that the active-bank indicator must be "visually unmistakable" — only its placement is undecided.
**The wart.** No placement chosen yet among three candidates: per-region headers, a single header readout, or a lit-tab treatment.
**Intended fix.** Not stated in the source beyond the three candidate placements above — the choice among them is the open item. This is explicitly a panel-polish detail.
**The constraint the fix MUST handle.** Not stated in the source.
**Priority / risk.** Not stated as a priority level; the source characterizes this as a "panel-polish detail."
**Done looks like.** Not stated in the source beyond choosing one of the three placement options.
@@ -10,7 +10,8 @@ flatness, naming families) are **not restated**; where a finding below touches t
is because the functional mechanism is new.
Every claim below was verified by grep/read of the actual tree. Line numbers are as of this
audit's snapshot. Wave assignments reference PLAN.md §Q-W1..Q-W6.
audit's snapshot. Wave assignments reference the Q-W1..Q-W6 waves (landed history in
`docs/ARCHIVE.md`; sequencing in `docs/product/code-organization.md` §5).
---
@@ -2,9 +2,9 @@
Static analysis, 2026-07-28, branch `pq-w0-audit`. Scope: any value stored in an
environment-coupled domain — frames, sample rate, DPI, pixels, tick cadence — that should be
stored **rate-free / device-free and resolved at the point of use** (PLAN.md §Q-W0 env-coupled
bullet; `docs/product/code-organization.md` §2c.3; the load-bearing `sample_map` seconds
invariant). Findings are domain-modeling calls, not "rescale by rate" patches. The judgment bar
stored **rate-free / device-free and resolved at the point of use** (`docs/product/code-organization.md`
§2c.3 env-coupled bullet; the load-bearing `sample_map` seconds invariant, documented in
`src/core/instrument/CLAUDE.md`). Findings are domain-modeling calls, not "rescale by rate" patches. The judgment bar
applied: a finding requires (a) an env-coupled *stored* domain AND (b) an environment that can
actually change under it. Frame counts computed transiently from seconds at the use site are
correct and are not reported.
@@ -63,7 +63,7 @@ fix-now findings in those files must be remediated in Q-W0 itself.
normalize the Trigger fade-in/out knobs.
- **Stored vs. correct domain:** the fade **storage** domain (int64 SOURCE frames, persisted in
the zones payload) is settled and correct — a source-timeline fact, invariant under project-
rate change (PLAN.md §S15). The *UI ceiling*, however, encodes a wall-clock intent ("2-second
rate change (`docs/ARCHIVE.md` §S15). The *UI ceiling*, however, encodes a wall-clock intent ("2-second
max fade throw") as a frame count at an assumed 44.1 kHz source. `88200` is a rate-derived
literal in `src/`, brushing the no-hardcoded-rate ruling even though it never touches disk.
- **What breaks when the environment shifts:** the environment here is the **source file's
@@ -165,7 +165,7 @@ fix-now findings in those files must be remediated in Q-W0 itself.
`SampleRefs` (paths + root/loop/channels intrinsics), `instanceGuid` — all rate-free or
file-fact domains. Clean.
- **Trigger `fadeInFrames`/`fadeOutFrames`/`startPoint`/`SampleLoop.start/end` persisted as
int64 SOURCE frames:** deliberate, settled source-timeline facts (PLAN.md §S15;
int64 SOURCE frames:** deliberate, settled source-timeline facts (`docs/ARCHIVE.md` §S15;
`bank_model.h:66-72` documents the loop rationale) — frames *of the file* are invariant under
project-rate change; the file's own rate is stored alongside and resolved at decode. Correct
domain, not a finding.
+16 -15
View File
@@ -1,19 +1,20 @@
# Capture tail — spec
Authoritative spec for the **capture-tail** feature: preserving reverb/release
tails that decay past the end of a capture range. The tickable milestone lives in
`PLAN.md` (Milestone T); this doc holds the full technical detail **and** the
product framing.
tails that decay past the end of a capture range. The tickable milestone's landed
history is in `docs/ARCHIVE.md` (Milestone T); this doc holds the full technical
detail **and** the product framing.
> **Why this doc carries the technical spec (not `CONTEXT.md`).** Every other
> pillar (capture M0M11, Design View, Multi-bank) keeps its authoritative
> technical spec as a `CONTEXT.md §` section and its *why* in a `docs/product/`
> note. Capture-tail is a rider on the already-shipped offline-render path
> (M3/M7), not a standalone pillar, and it is being specced without reopening
> `CONTEXT.md`. So the authoritative detail lands **here**, house-styled to match
> the CONTEXT specs; when the tail work lands, doc-keeper may fold the invariant
> deltas into `CONTEXT.md §Precision invariants` as landed history. Same standing
> discipline applies: **verify every REAPER API name/flag against
> **Why this doc carries the technical spec (not the architecture docs).** Every
> other pillar (capture M0M11, Design View, Multi-bank) keeps its authoritative
> technical spec as a per-directory `src/**/CLAUDE.md` section and its *why* in a
> `docs/product/` note. Capture-tail is a rider on the already-shipped
> offline-render path (M3/M7), not a standalone pillar, and it was specced without
> reopening the architecture spec. So the authoritative detail lands **here**,
> house-styled to match those specs; the landed invariant deltas are folded into
> root `CLAUDE.md` §Precision invariants and `src/core/capture/CLAUDE.md` /
> `src/shell/capture/CLAUDE.md` as landed history. Same standing discipline
> applies: **verify every REAPER API name/flag against
> `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use** — the flag values
> below are transcribed from that header (line numbers cited) and are not guesses.
@@ -252,7 +253,7 @@ held to bit-identical repeats.
- **Opt-in beyond the region.** The tail only ever adds audio past the range end
when a tail is explicitly requested (Auto or Manual). This is exactly the existing
invariant: *"no added silence unless a tail is explicitly requested"*
(`CONTEXT.md §Precision invariants`, `CLAUDE.md §Exact bounds`). Auto-trim
(`CLAUDE.md §Precision invariants §Exact bounds`). Auto-trim
strengthens it — the tail added is decay, not silence, and the silence past the
decay is trimmed off.
- **The null test uses NO tail.** The null-test / verify capture (M10) and any
@@ -274,8 +275,8 @@ held to bit-identical repeats.
the tail is **the track's own reverb/delay decay**, not the parent bus's. A track
with a reverb plugin captures that reverb's tail; a track feeding a folder reverb
does **not** capture the folder reverb's tail (that send is out of scope — and
note the pre-existing send-isolation caveat in `PLAN.md §Open questions`, which
the tail inherits unchanged, does not worsen). Correct and consistent.
note the pre-existing send-isolation caveat, which the tail inherits
unchanged, does not worsen). Correct and consistent.
- **Item scope:** item/take FX only, self-track + ancestors + master bypassed →
the tail is the **item/take FX decay only**. An item with a take reverb captures
its tail; the track's reverb does not ring into it. Correct.
+9 -7
View File
@@ -9,8 +9,10 @@ subdirectories) **without sacrificing runtime performance**, against a stated qu
>
> Bring the codebase "**into the realm of something I can stand to look at.**"
Its build roadmap lives in **PLAN.md §Phase Q** and its authoritative spec in
**CONTEXT.md §Phase Q**. This doc holds the *why* — the quality bar, the evidence base
Its build roadmap's landed history lives in **`docs/ARCHIVE.md`** ("Phase Q — Quality:
the decision record") and its authoritative spec now lives in root **`CLAUDE.md`**
(§Directory and namespace layout, §Performance guardrails, §Structural heuristics)
plus the per-directory `src/**/CLAUDE.md` files. This doc holds the *why* — the quality bar, the evidence base
(a grep-verified SOLID audit), the target directory/namespace shape grounded in the Vital
reference, and the numbered fork decisions.
@@ -321,10 +323,10 @@ re-namespaced, and split, not carried forward untouched into a tidier tree. "Som
look at" is partly shape (§2/§2b) and partly *functional soundness* (§2c).
Q-W0 is **not executed by product-designer** — this doc *defines the wave and its scope*; the audit
itself is staff-engineer/DSP work. The authoritative wave definition lives in **PLAN.md §Q-W0** and
its spec framing in **CONTEXT.md §"The pre-restructure audit wave (Q-W0)"**; this section is the
evidence-doc framing (the *why* and the smell taxonomy), matching how §2/§2b frame the structural
audits.
itself is staff-engineer/DSP work. The wave's landed history lives in **`docs/ARCHIVE.md`**
("Q-W0 fix-now remediations" and "Phase Q — Quality: the decision record"), and its findings live in
**`docs/product/code-quality-audit.md`**; this section is the evidence-doc framing (the *why* and
the smell taxonomy), matching how §2/§2b frame the structural audits.
### 2c.1 Audit surfaces (the named targets)
@@ -451,7 +453,7 @@ namespace of every header, splitting the four largest TUs). Meanwhile:
- **Phase L** has **L3** (VST editor + embed-strip restyle, gated on Phase S landing on dev)
still to land — it touches the Phase S draw shells (`reasampler_editor` / `reasampler_embed`),
which arrive on dev with Phase S. (L1/L2/L4/L5/L6/L7 have **already landed** — see
`COMPLETED.md`; the once-listed "L2 pending" is stale and has been corrected here.)
`docs/ARCHIVE.md`; the once-listed "L2 pending" is stale and has been corrected here.)
- **D2** is **functionally complete** (D2-W1..W3-B landed; the only open item — a per-track
lane-split panel indicator — is *explicitly deferred*, not a blocking residual). **M9** (slots)
is **abandoned** (Daniel, 2026-07-27) — will not be built. D2 is named in the gate only so a
+2 -1
View File
@@ -3,7 +3,8 @@
Date: 2026-07-28 · Branch: `pq-w0-audit` · Static analysis only; no code changed by the audit.
This is the committed Q-W0 findings report (Q-10 SETTLED: a committed doc beside the SOLID/naming
audit — `docs/product/code-organization.md` §2c.3; deliverable contract in PLAN.md §Q-W0). It
audit — `docs/product/code-organization.md` §2c.3; deliverable contract landed per `docs/ARCHIVE.md`
§Phase Q). It
synthesizes four parallel audit tracks; the full track notes remain in the tree as appendices and
are the evidence base for every claim here — this report cites finding IDs and does not restate
mechanisms in full:
+4 -4
View File
@@ -1,8 +1,9 @@
# Design View — product notes
Framing, rationale, and design-direction calls behind the **Design View** phase.
The tickable spec lives in `PLAN.md` (Phase D) and the authoritative technical
detail in `CONTEXT.md` (§Design View). This doc holds the *why* — the workflow
The tickable spec's landed history lives in `docs/ARCHIVE.md` (Phase D) and the
architecture detail in `src/core/view/CLAUDE.md` + `src/shell/view/CLAUDE.md`
(§Design View). This doc holds the *why* — the workflow
narrative, the N-mode reasoning, the screenset differentiation, and the
design-direction recommendations — so those don't clutter the build docs.
@@ -339,8 +340,7 @@ tooltip on the switch.
`docs/design/`), say so and it moves. Flagging because establishing the
directory is a project convention, not mine to assume silently.
2. **Phase namespace.** Proposed **Phase D** (a lettered namespace) rather than
M12 — see PLAN.md rationale. Confirm you're happy with letters for parallel,
non-capture phases.
M12. Confirm you're happy with letters for parallel, non-capture phases.
3. **SDK name correction.** The brief cited `B_SHOWINMCP`; the verified SDK flag
for mixer-panel visibility is **`B_SHOWINMIXER`** (SDK header line 2235). The
spec uses the verified name. Just flagging the discrepancy so it isn't a
+65 -53
View File
@@ -2,10 +2,11 @@
Framing for a **MIDI-triggered audio sampler** that plays back ReaSampler's captured
banks. This began as a discussion-shaping doc; with all forks now settled it has become
the **product framing behind a scoped phase**. Its build roadmap lives in **PLAN.md
§Phase S** and its authoritative spec in **CONTEXT.md §Phase S** — this doc holds the
*why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the settled
decision record).
the **product framing behind a scoped phase**. Its build roadmap lives in
**`docs/ARCHIVE.md` §Phase S** and its authoritative spec in
**`src/core/instrument/CLAUDE.md`** and **`src/shell/instrument/CLAUDE.md`** — this
doc holds the *why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the
settled decision record).
Status: framed by product-designer (2026-07-26), **revised 2026-07-27 (r11)**. r11 records the
**Sample-face recomposition** (Daniel's post-landing DAW pass, 2026-07-27): all linear sliders →
@@ -42,10 +43,11 @@ demoted to an opt-in Zones panel — see the r6 Addendum in §4. r6 also settles
S1S6 instrument: the product name **ReaSampler 9000** and the **"better than RS5K" UX
overhaul** (Phase S points S10S13) — see the r5 Addendum in §4. r4 (below) settled the four
residual forks D-A..D-D. The
"no PLAN.md footprint" era is **over** — with D-A through D-D settled (below), the
"no landed-roadmap footprint" era is **over** — with D-A through D-D settled (below), the
instrument was scoped into **Phase S** (codename Daniel's: "S" for Sampler, because "D"
collides with the existing Design View phase). **PLAN.md §Phase S is now the
authoritative roadmap; CONTEXT.md §Phase S is the authoritative spec.** This doc is the
collides with the existing Design View phase). **`docs/ARCHIVE.md` §Phase S now records
the landed roadmap; `src/core/instrument/CLAUDE.md` and `src/shell/instrument/CLAUDE.md`
are the authoritative spec.** This doc is the
framing/decision record they point back to. Prior revisions (a) established that a REAPER
*extension* cannot be a MIDI instrument, (b) corrected a material omission — REAPER's
**VST-host bridge**, which lets a VST3 plugin *hosted inside REAPER* call back into
@@ -410,8 +412,8 @@ it doesn't carry" — is unchanged. What the bridge settles is *where that mappi
between extension and instrument as **live shared `"reasampler"` state**, not a file one
writes and the other re-parses.
**What the current index carries** (from `bank_model`'s `Sample`, per CONTEXT.md §Data
model): id, display name, relative path, source range, channel count, sample rate,
**What the current index carries** (from `bank_model`'s `Sample`): id, display name,
relative path, source range, channel count, sample rate,
length, capture tempo, an **optional key**, peak/RMS/LUFS, content hash, tier,
provenance, timestamp. Notably it *already* has an optional key field and capture
tempo — the seeds of pitch-mapping are there.
@@ -426,8 +428,8 @@ tempo — the seeds of pitch-mapping are there.
to velocity zones).
- **Round-robin groups** (cycle through N samples on repeated same-note hits).
- **Loop points** (sustain loop start/end for held notes; sample-accurate,
zero-crossing-aware — CONTEXT already flags loop/zero-crossing handling as
day-one-relevant for wavetable material).
zero-crossing-aware — loop/zero-crossing handling is day-one-relevant for
wavetable material).
- **Amplitude envelope** (ADSR) and optionally filter/pitch envelopes.
- **Tuning/gain trim** per sample.
@@ -559,7 +561,7 @@ ReaSampler-native way to build it and it's assumed, not debated, going forward.
All four residual decisions are now called. Each is marked **SETTLED** with Daniel's
choice and the reasoning kept as the record of *why* — do not re-litigate. They are
scoped into **PLAN.md §Phase S** / **CONTEXT.md §Phase S**.
scoped into **`docs/ARCHIVE.md` §Phase S** / **`src/core/instrument/CLAUDE.md`**.
**D-A — SETTLED: bare Steinberg VST3 SDK + LICE editor (no JUCE).** *(The central fork.
§1a is the assessment that fed it. The sub-question — who draws the editor? — was the
@@ -649,7 +651,7 @@ After Phase S was scoped (D-A..D-D), Daniel set two further directions. These ar
**settled directions**, not open forks — specced as new Phase S points (S7S9), not
re-litigated. Recorded here per the doc's settled-decisions convention.
**D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ PLAN.md S7).**
**D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ `docs/ARCHIVE.md` §S7).**
Captures are often stereo; the current mono downmix is a Tier-0 simplification. The
engine gets a **per-instance channel-mode toggle (1 mono / 2 stereo)** that "works with
the REAPER audio bus automatically" — the VST3 declares/negotiates its output bus
@@ -662,8 +664,8 @@ Cross-mode policy: mono-source-in-stereo → dual-mono; stereo-source-in-mono
never a bank fact). Sequenced **first after the editor/embed work** because it touches the
engine Daniel smoke-tests.
**Ingest routes through the bank — "option 1"; the extension owns ingest (→ PLAN.md
S8 + S9).** Loading a sample into the sampler is **one gesture**: capture/import-into-bank
**Ingest routes through the bank — "option 1"; the extension owns ingest (→
`docs/ARCHIVE.md` §S8 + §S9).** Loading a sample into the sampler is **one gesture**: capture/import-into-bank
+ auto-assign to the active instance. The **extension owns ingest** (it has arrange
access, Media-Explorer access, and the drop-target surface on its own panels); the
**instrument stays a read-only bank consumer** — it never captures or imports. Sub-parts,
@@ -690,7 +692,7 @@ with the honest SDK reality verified against the vendored headers:
*The genuine spikes flagged (not decisions Daniel owes, just build-time unknowns):* the
ME merely-selected-file read (b), and the drop-onto-editor cross-artifact relay (c). Both
are honestly-flagged as spikes in PLAN.md S8, not promised.
are honestly-flagged as spikes in `docs/ARCHIVE.md` §S8, not promised.
### Addendum — product name + UX overhaul (Daniel, 2026-07-26, post-S1S6 DAW test)
@@ -700,7 +702,7 @@ open forks (the two flagged forks below are the only calls left to Daniel).
**The instrument's product name is `ReaSampler 9000`.** The extension stays **ReaSampler**
(capture + organization); the instrument is **ReaSampler 9000** (playback). Propagation is
a checklist item (PLAN.md §Phase S — product name; CONTEXT.md §Product name): the VST3
recorded in `docs/ARCHIVE.md` §Phase S — product name: the VST3
class **display name** string, the `IPlugView` editor title band (today "ReaSampler
Instrument"), the S6 embed-strip label, and the docs. **Compat guard (load-bearing):** the
**VST3 class UID must NOT change** — instances in saved projects key off it; a UID change
@@ -747,8 +749,9 @@ names the editor as the wound.
After the r5 UX-overhaul directive was specced (keymap-first S10), Daniel reframed the
workflow before S10 was implemented. This **revises S10** and settles S-NAME-1. Settled
directions, not open forks — recorded here per the doc's settled-decisions convention; PLAN.md
§S10 and CONTEXT.md §Phase S (workflow hierarchy) carry the spec.
directions, not open forks — recorded here per the doc's settled-decisions convention;
`docs/ARCHIVE.md` §S10 records what landed and `src/core/instrument/CLAUDE.md` (the
editor `ui/` modules) documents the current architecture.
**The reframe, verbatim (Daniel, 2026-07-26):** *"We need to think hard about the workflow
with this plugin. Have a giant list of 'item' blocks is visually useless. When the plugin is
@@ -806,8 +809,8 @@ partly on filename, fall back to keeping the filename and record that as shipped
Daniel directed a set of engine features for the sampler, specced as **new Phase S points
S15 (Trigger vs Gate) and S16 (pitch envelope)**. **The feature set is settled** — recorded
here per the doc's settled-decisions convention; PLAN.md §S15/S16 and CONTEXT.md §Sampling
modes carry the spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param
here per the doc's settled-decisions convention; `docs/ARCHIVE.md` §S15 / §S16 records what
landed and `src/core/instrument/CLAUDE.md` §Sampling modes documents the current spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param
granularity); the WDL question was resolved by inspection.
**Directive, verbatim (Daniel, 2026-07-26):** *"let's have product spec out some features
@@ -927,9 +930,10 @@ This reshapes S16 and **flips the r7 WDL verdict** on `WDL_SimplePitchShifter`.
`process` allocation; measure per-voice CPU + onset latency against the polyphony cap. Treat
S16's Preserve-engine point as the phase's next real DSP spike, not a thin envelope add-on.
**Where the spec lives:** PLAN.md §S16 (reshaped to "pitch engine modes + pitch envelope",
with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 interaction note;
CONTEXT.md §Pitch engine modes — Varispeed vs Preserve + the corrected WDL surface finding.
**Where the spec lives:** `docs/ARCHIVE.md` §S16 (reshaped to "pitch engine modes + pitch
envelope", with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16
interaction note; `src/core/instrument/CLAUDE.md` §Sampling modes — Varispeed vs Preserve
+ the WDL surface finding.
### Addendum — VST channel isolation (Daniel, 2026-07-26)
@@ -972,9 +976,9 @@ with or right after the in-flight waves (S9 ext_keys, S15/S16 processor/editor)
channel's banks; stable-project + beta-VST = clean empty (not error); the S-NAME-1
rename/rebind test extends to the beta UID.
**Where the spec lives:** PLAN.md §S18; CONTEXT.md §VST3 channel identity — the UID pair + the
pairing surface. The pairing surface's data half is already load-bearing V4 machinery; S18
adds only the identity fork on top.
**Where the spec lives:** `docs/ARCHIVE.md` §S18; `src/shell/instrument/CLAUDE.md` §VST3
channel identity — the UID pair + the pairing surface. The pairing surface's data half is
already load-bearing V4 machinery; S18 adds only the identity fork on top.
---
@@ -986,8 +990,9 @@ good — but the two-view editor (today's "Browser" + "Zones" toggle) misallocat
the default window is undersized for a 1080p world, and the drop-a-capture-onto-FX gesture is
broken in practice. The directive: **make the one job — pick a capture, tune it, play it —
fast, easy, and fun. Style is a critical ingredient. No spreadsheet aesthetics.** These are the
`r9` calls. Authoritative spec: **CONTEXT.md §Phase S — editor view-model redesign (S-VIEW)**;
build roadmap: **PLAN.md §Phase S — editor view-model redesign**.
`r9` calls. Current architecture: **`src/core/instrument/CLAUDE.md`** and
**`src/shell/instrument/CLAUDE.md`**; landed record: **`docs/ARCHIVE.md` §Phase S — editor
view-model redesign**.
**The reference devices (the north star for control density).** Daniel named Ableton **Simpler**
and a Kilohearts/Phase-Plant **sampler group** as the composition targets. Both share one
@@ -1021,7 +1026,7 @@ grammar, and it is the grammar the redesign adopts:
loading a new one is a distinct act), not a three-way radio. *Why the reframe matters:* it
makes "I just want to play this capture" the zero-click default, and "I want a different one"
a single deliberate gesture, instead of making the user re-choose their whole stance every
time. See CONTEXT.md §S-VIEW for the precise navigation model.
time. See `docs/ARCHIVE.md` §S-VIEW-1 for the precise navigation model as landed.
2. **The Sample view earns the hero treatment; Browse gets ruthlessly cut.** Browse today
carries a waveform preview, root-note piano-roll, loop-point labels, a track-root message, and
@@ -1037,10 +1042,10 @@ grammar, and it is the grammar the redesign adopts:
3. **Two engineering prerequisites, framed but routed to implementation.** The **drop-to-FX bug**
(dropping a capture onto a track's FX chain does not instantiate + init ReaSampler 9000) and
the **undersized default window** are not design decisions — they are a bug and a one-line
default. Both are framed in CONTEXT.md §S-VIEW with the SDK reality swept (drop-to-FX: the S17
machinery is SDK-correct, so this is a *diagnosis* task, not a redesign; window size: the
`getSize`/`checkSizeConstraint` mechanism is verified), and both are flagged for
staff-engineer, not for a product fork.
default. Both are recorded landed in `docs/ARCHIVE.md` §S-VIEW-BUG-1 (drop-to-FX: the S17
machinery is SDK-correct, so this is a *diagnosis* task, not a redesign) and §S-VIEW-SIZE-1
(window size: the `getSize`/`checkSizeConstraint` mechanism is verified), and both were
flagged for staff-engineer, not for a product fork.
**New parameters this introduces (both instrument performance state, D-B — never bank facts):**
@@ -1064,7 +1069,7 @@ grammar, and it is the grammar the redesign adopts:
new top-level `previewVelocity` field), **not** the extension's `persist` project ext-state —
that module is REAPER-project-scoped and extension-owned, so it would make the level
project-global instead of per-instance and route an instrument concern through a bank-read-only
seam. See CONTEXT.md §S-VIEW for the round-trip and back-compat lift. This is what makes the
seam. See `docs/ARCHIVE.md` §S-VIEW-4 for the round-trip and back-compat lift as landed. This is what makes the
preview button *fun*: tap it hard or soft without reaching for a controller — and it remembers.
**Two visual components the redesign commits to:**
@@ -1101,8 +1106,10 @@ persisted fields is not a compat event; saved instances rebind and restore. And
(extended additively — `keyTrack` per-zone, `previewVelocity` per-instance via an envelope bump to
v6, both with back-compat defaults on read) are the same load-bearing core.
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW); PLAN.md
§Phase S — editor view-model redesign. This Addendum is the *why*; those are the *what/how*.
**Where the spec lives:** `src/core/instrument/CLAUDE.md` (envelope overlay, key-tracking,
preview-velocity ownership) documents the current architecture; `docs/ARCHIVE.md` §Phase S
— editor view-model redesign records what landed (S-VIEW-1 through S-VIEW-10). This
Addendum is the *why*; those are the *what/how*.
---
@@ -1217,8 +1224,9 @@ for preview velocity — a different struct on a different version axis). Concre
the L1 kit, routing mouse through `velocity_curve`), gated on the foundation track and composing
with the S-VIEW-2 Sample face + S-VIEW-3 envelope-overlay work.
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW), velocity-curve
sub-section; PLAN.md §Phase S — editor view-model redesign (S-VIEW-9/S-VIEW-10 + fork R10-F1). This
**Where the spec lives:** `src/core/instrument/CLAUDE.md` (the `velocity_curve` module, its
engine application point, and its ownership rules) documents the current architecture;
`docs/ARCHIVE.md` §S-VIEW-9 / §S-VIEW-10 records what landed (fork R10-F1 resolved). This
Addendum is the *why*; those are the *what/how*.
---
@@ -1278,11 +1286,13 @@ recomposition of *existing* controls; no new params, no component-state bump, VS
unchanged. All drawing through the L1 kit by palette role; all layout/hit-test in new pure modules
(`knob_deck`, `curve_popup` — mirrors of `action_bar`/`overflow_menu`); the knobs and the hero's
envelope nodes remain two surfaces on one param model (S-VIEW-F2's structural sync, untouched).
The full inventory contract (every landed element → its r11 home) is in the CONTEXT.md spec.
The full inventory of what landed (every element → its r11 home) is recorded in
`docs/ARCHIVE.md` §FB1 and §FB2.
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW) → "The
Sample-face recomposition (r11)"; PLAN.md §Phase S — editor Wave B (S-VIEW-11/12/13 + forks
R11-F1/R11-F2). This Addendum is the *why*; those are the *what/how*.
**Where the spec lives:** `src/core/instrument/CLAUDE.md` (the `knob_deck`/`curve_popup`/
`master_gain` modules) documents the current architecture; `docs/ARCHIVE.md` §FB1 and §FB2
record what landed (S-VIEW-11/12/13 + forks R11-F1/R11-F2 resolved). This Addendum is the
*why*; those are the *what/how*.
---
@@ -1329,7 +1339,8 @@ Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Adden
frameworks), DS-2 (Direction B "Neon Console" + Direction C's spectral keyboard strip), and
DS-3 (thorough panel layout) are all **SETTLED (2026-07-26)**. Framing + palette + the three
visual directions + forks: `docs/product/visual-design-language.md` (on `dev`); roadmap +
spec: **PLAN.md §Phase L + CONTEXT.md §Phase L** (on `dev`). **S10S13 build with the
spec: **`docs/ARCHIVE.md` §Phase L** (landed record) and **`src/core/ui/CLAUDE.md`**
(current architecture) (on `dev`). **S10S13 build with the
current drawing and adopt the L1 kit when it lands — not gated on Phase L.** Answers
Daniel's "the VST is dogshit / temple os / does Cockos have a toolkit" (2026-07-26,
post-S1S6 DAW test).
@@ -1350,15 +1361,16 @@ Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Adden
S15-F1 (choke, held) / S15-F2 (param granularity, lean per-zone). Feature set settled;
the engine default is Daniel's fork.
**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1S6 the original
dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded
UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10S13** the ReaSampler
9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes engine features);
**CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the channel-mode
/ ingest / bank-generation / sampling-mode / pitch-engine contracts, the UX-overhaul spec,
the product-name convention, the pure/shell split, the WDL finding, the must-verify
SDK/bridge surfaces). This doc is the framing/decision record they point back to. The "no
PLAN.md footprint" era is over.
**Authoritative from here:** **`docs/ARCHIVE.md` §Phase S** is the landed roadmap (S1S6
the original dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier
1 → embedded UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10S13** the
ReaSampler 9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes engine
features); **`src/core/instrument/CLAUDE.md`**, **`src/shell/instrument/CLAUDE.md`**, and
**`src/core/wire/CLAUDE.md`** are the current spec (seam-field semantics, scope contracts,
the channel-mode / ingest / bank-generation / sampling-mode / pitch-engine contracts, the
UX-overhaul spec, the product-name convention, the pure/shell split, the WDL finding, the
must-verify SDK/bridge surfaces). This doc is the framing/decision record they point back
to. The "no landed-roadmap footprint" era is over.
---
+3 -2
View File
@@ -1,8 +1,9 @@
# Multi-bank — product notes
Framing, rationale, and design-direction calls behind the **Multi-bank** phase.
The tickable spec lives in `PLAN.md` (Phase B) and the authoritative technical
detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow
The tickable spec's landed history lives in `docs/ARCHIVE.md` (Phase B) and the
architecture detail in `src/core/model/CLAUDE.md` + `src/shell/bank_ops/CLAUDE.md`
(§Multi-bank). This doc holds the *why* — the workflow
narrative, the pool-privilege reasoning, the movement semantics, and the
design-direction recommendations — so those don't clutter the build docs.
+4 -4
View File
@@ -1,10 +1,10 @@
# Provenance — product notes
Framing, rationale, and the dual-canvas reconciliation behind the reshaped
**Milestone 10 (provenance)**. The tickable spec lives in `PLAN.md` (M10); the
authoritative technical detail is `CONTEXT.md` (§Data model, §capture) plus this
note for the reconciliation calls. This doc holds the *why* and the open forks so
they don't clutter the build docs.
**Milestone 10 (provenance)**. The tickable spec's landed history is in
`docs/ARCHIVE.md` (M10); the architecture detail is in `src/core/model/CLAUDE.md`
and `src/shell/capture/CLAUDE.md` plus this note for the reconciliation calls.
This doc holds the *why* and the open forks so they don't clutter the build docs.
Status: **IMPLEMENTED (2026-07-26).** Settled 2026-07-23; landed 2026-07-26.
Reshaped from the old "provenance + null-test verify" M10. Two decisions were fixed
+17 -14
View File
@@ -2,24 +2,26 @@
Framing, rationale, and open forks behind the two missing removal capabilities:
**sample-remove** (a sample-level index verb) and **prune** (the file-lifecycle
path CONTEXT.md keeps forward-referencing but never scoped). The tickable spec
lives in `PLAN.md` (Phase B point B5 for remove; **Phase R** for prune) and the
authoritative technical detail in `CONTEXT.md` (§Sample removal, §Prune — file
lifecycle). This doc holds the *why* — the workflow, the guardrails, the
index-vs-file boundary, and the forks that need a Daniel decision.
path the spec kept forward-referencing but never scoped). The tickable spec's
landed history is in `docs/ARCHIVE.md` (Phase B point B5 for remove; **Phase R**
for prune) and the architecture detail lives in `src/core/model/CLAUDE.md` +
`src/shell/bank_ops/CLAUDE.md` (§Sample removal) and `src/core/reclaim/CLAUDE.md`
+ `src/shell/persist/CLAUDE.md` (§Prune — file lifecycle). This doc holds the
*why* — the workflow, the guardrails, the index-vs-file boundary, and the forks
that need a Daniel decision.
Status: framed by product-designer (2026-07-23); **all five forks settled by Daniel
(2026-07-24)** — R-A this-bank-primary, R-B batched REAPER undo points
(Phase-B-wide), R-C trash-preferred-with-unlink-fallback, R-D owned-file manifest
(seam lands early in Phase B / capture), R-E manual action + panel button. The
decisions are folded into the fork sections below and into the B5 / Phase R spec
prose in CONTEXT.md and the tickable points in PLAN.md.
decisions are folded into the fork sections below and into the B5 / Phase R
history in `docs/ARCHIVE.md` and the architecture docs above.
---
## The one boundary that governs everything: index vs. file
ReaSampler already draws a hard line, stated repeatedly in CONTEXT.md: **a bank
ReaSampler already draws a hard line: **a bank
operation touches the *index*, never the *file*.** Move, copy, evacuate, and
delete-bank are all index-only; files persist on disk "until prune." Every
removal capability below sits on exactly one side of that line, and keeping the
@@ -120,7 +122,7 @@ line. It is the verb Phase B forgot, not a new pillar.
---
## Prune — the file-lifecycle path CONTEXT.md kept promising
## Prune — the file-lifecycle path the spec kept promising
### What the user is doing
@@ -131,7 +133,7 @@ purpose. Over a long project the bank folder accumulates dead `.wav` files that
cost disk and clutter. **Prune is the reclaim pass**: "sweep the bank folder,
delete the files nothing references, tell me what you reclaimed."
This is the path CONTEXT.md forward-references in at least four places ("files
This is the path the spec forward-references in at least four places ("files
persist on disk until prune," "the capture/prune path reclaims it") but never
scopes. It is a real, promised capability with **no phase, no module, no point**
— a dangling reference the plan has to make good on.
@@ -141,8 +143,8 @@ scopes. It is a real, promised capability with **no phase, no module, no point**
ReaSampler already shipped this exact shape once. Design View's `view_mode_model`
has **`ViewModeModel::reconcile(liveGuids)`** — a pure function fed the live set
(the tracks that still exist), returning the residual membership entries to drop
(CONTEXT.md §Design View: "prunes orphaned snapshots on every toggle/load;
tolerates unknown/stale GUIDs (prune on reconcile)"). Prune is the **file-pool
(`src/core/view/CLAUDE.md`: "tolerates unknown/stale GUIDs (pruned on reconcile
via `ViewModeModel::reconcile(liveGuids)`)"). Prune is the **file-pool
mirror of that pure pattern**:
> `reconcile(liveGuids)` reconciles *membership entries* against *live tracks*.
@@ -224,7 +226,7 @@ Prune is **not** a Phase B point. Three reasons it earns its own lettered phase
1. **It is a different pillar.** Phase B is the *bank container* pillar
(index-only, non-destructive, above the file). Prune is the *file lifecycle*
pillar (the one path that deletes files). CONTEXT.md already names it as a
pillar (the one path that deletes files). The spec already named it as a
separate concern every time it says "the capture/**prune** path" — file
lifecycle is spoken of as its own thing, owned by neither the capture nor the
bank layer. Giving it its own phase matches how the spec already talks about it.
@@ -337,7 +339,8 @@ capture).** The book tracks the set of files it has created; prune reclaims
ext-state**, and Phase R's R1/R2 *consume* that manifest. The exact persistence
shape — a sibling ext-state key vs. folded into the `banks` blob — is a small
residual to settle at build; the **manifest-now decision is firm**. (Settled
2026-07-24; the up-front point is added to Phase B / the capture path in PLAN.md.)
2026-07-24; the up-front point is recorded in `docs/ARCHIVE.md` under Phase B /
the capture path.)
**Fork R-E — prune trigger: manual-only vs. offer-on-orphaning vs. periodic.
SETTLED: MANUAL ACTION + PANEL BUTTON.** Prune runs via a bindable manual action
+3 -3
View File
@@ -8,8 +8,8 @@ concrete and decidable:
2. **A beta side-channel** — so development can continue and a beta build run
*alongside* the stable one without the beta clobbering the release.
This doc holds the *why*, the forks, and a recommendation. When Daniel picks, the
tickable points land in `PLAN.md` and the deploy/build wiring hands off to dev-ops.
This doc holds the *why*, the forks, and a recommendation. The tickable points'
landed history is in `docs/ARCHIVE.md`; the deploy/build wiring hands off to dev-ops.
This is a framing note; it changes no source or CMake.
Status: framed by product-designer (2026-07-26); **all four forks settled by Daniel
@@ -54,7 +54,7 @@ Two sharp edges follow directly and recur throughout this note:
Both binaries reading/writing the same namespace on the same open project means a
**beta can read — and rewrite — a stable project's saved bank/view state.** Given
the forward-only migrations already in the design (legacy `bank_index` retired
after promotion; `banks` authoritative thereafter — CONTEXT.md §Multi-bank), a
after promotion; `banks` authoritative thereafter — `docs/ARCHIVE.md` §Phase B), a
beta that writes a newer schema into a project a user then reopens in stable is a
real corruption path, not a theoretical one.
+8 -7
View File
@@ -16,11 +16,12 @@ written after Daniel DAW-tested the S1S6 instrument:
revised 2026-07-26 (Daniel)** from the single electric-cyan accent to a **three-accent
pastel system** (primary/secondary/tertiary = pastel lime green / pastel teal / pastel
purple) — see §6. (A stylish-font upgrade was considered and **declined** by Daniel — no
font bundling/redistribution; the kit keeps its current cached-font face.) The build points it feeds live in
**PLAN.md §Phase L** (**L1** the shared LICE drawing kit, **L2** the dock-panel layout
redesign, **L3** the VST editor + embed-strip restyle) and **CONTEXT.md §Phase L** (the
design-system spec). Toolkit facts below are **verified against the vendored `vendor/WDL`
tree**, not lore.
font bundling/redistribution; the kit keeps its current cached-font face.) The build points it
fed have landed; their history is in **`docs/ARCHIVE.md`** ("Phase L — Look-and-feel", **L1** the
shared LICE drawing kit, **L2** the dock-panel layout redesign, **L3** the VST editor +
embed-strip restyle) and the architecture now lives in **`src/core/ui/CLAUDE.md`** /
**`src/shell/panel/CLAUDE.md`** (the design-system implementation). Toolkit facts below are
**verified against the vendored `vendor/WDL` tree**, not lore.
> **Phase-boundary note (2026-07-26).** This look-and-feel effort was originally drafted
> as Phase S points **S0-DS** (the shared kit) and **S14** (the panel refresh). It was
@@ -497,7 +498,7 @@ within the pastel intent — is one file.
---
## 5. The build shape (feeds PLAN.md / CONTEXT.md §Phase L)
## 5. The build shape (landed history in `docs/ARCHIVE.md` §Phase L)
Three points and their sequencing.
@@ -547,7 +548,7 @@ draw it.
affordances, per-selection **move / copy / remove** sample menu.
- **Prune** button (R-E) — the byte-deleting action, `warn`-colored.
*M11 adds (dev PLAN.md M11 — merging now):*
*M11 adds (landed — `docs/ARCHIVE.md` Milestone 11):*
- **Action trigger buttons** — clickable buttons that fire the capture + provenance
action family directly (capture item / capture track scopes, re-capture from source,
resample-and-mute-source, batch capture, conform-on-insert, insert-at-cursor, drag-out,
+35
View File
@@ -0,0 +1,35 @@
# src/app — REAPER extension entry point
## Scope
Contains only `main.cpp`. Since the Phase Q hoists (Q-W3 onward), this TU is ONLY
pointers + entry + dispatch — the actual capture/panel/persist/action orchestration
lives in `shell/`. `main.cpp` owns: receiving REAPER's dispatch struct
(`ReaperPluginEntry`), resolving the REAPER API function pointers
(`REAPERAPI_LoadAPI`), the globals other files reference via `extern` (`g_hInst`,
`g_rec`), the `ReaSamplerSession` instance, its own bindable-action family via the
Q-W6 data-driven registration table (`shell/actions/action_registry`), and invoking
the other action families' (`design_view` / `bank` / `ingest`) own
register/handle/unregister triples at load and unload.
Exactly **one** translation unit defines `REAPERAPI_IMPLEMENT` — that is `main.cpp`.
Every other `.cpp` includes `reaper_plugin_functions.h` without the define and gets
`extern` declarations for the global API function pointers.
See root `CLAUDE.md`'s "REAPER extension contract" section for the full four-step
action-registration contract (`command_id` / `gaccel` / `hookcommand` / unload
mirror-unregister) that both this file's own action-table rows and the other
families' register/handle/unregister triples follow.
## Modules
- `main.cpp` — the REAPER extension's entry point and the sole `REAPERAPI_IMPLEMENT` TU; see root `CLAUDE.md`'s "REAPER extension contract" section for the registration contract this file implements.
## Gotchas
- This is intentionally a thin TU post-Phase-Q. Adding a new bindable action to
`main.cpp`'s own family means adding one row to its `ActionTableRow` table and a
flat handler function — do not hand-roll a parallel register/hookcommand/unregister
mechanism alongside the table.
- Never let a second `.cpp` define `REAPERAPI_IMPLEMENT` — that would double-allocate
the global REAPER API function pointers.
+13
View File
@@ -0,0 +1,13 @@
# src/core/audio — pure audio-data math
## Scope
Pure, REAPER-free audio-data math with no dependence on REAPER's own peak-cache
API. Currently one module: waveform min/max bin computation from raw PCM. Does
**not** include: LICE waveform drawing (`draw_kit`, `shell/panel`), the editor's
waveform/marker geometry (`waveform_view`, `core/instrument/ui`), or PCM
decoding itself.
## Modules
- `peaks` — waveform min/max bin computation from raw PCM; does not depend on REAPER's peak API.
+69
View File
@@ -0,0 +1,69 @@
# src/core/capture — pure logic behind the capture pillar
## Scope
Pure, REAPER-free logic behind the capture pillar: path arithmetic, the RIFF/WAV
codec, render-settings/FX-scope/tail-mode mapping, `InsertMedia` mode-bit
computation, the realtime-record state machine, and batch-capture planning.
Does **not** include: the REAPER-bound capture backends themselves
(`shell/capture`), the docked panel's tail-toggle window/click-handling
(`shell/panel`), or the `InsertMedia` call/undo-block mechanics
(`shell/capture`'s `insert.cpp`).
## Invariants
The repo-wide precision invariants (null test, bit-identical repeats,
non-destructive, exact bounds, relative-paths-only, capture FX scope) are
authoritative in root `CLAUDE.md` — reference them, don't re-copy them.
Detail specific to these pure modules:
- **No silent time-stretch, made checkable.** `insert_plan` never sets the &4
("stretch/loop to fit time sel") bit; `kStretchToTimeSelBit` is exposed
precisely so a test can assert it is never present in any computed
`InsertMedia` mode.
- **Tail is a three-state mode (`docs/product/capture-tail.md`), not a
per-action variant:** None (exact bounds, byte-identical, the only mode for
null-test/verify captures), Auto (generous 8 s tail then trim trailing
silence to -72 dB surgical normalize), Manual (fixed length, clamped to the 8
s cap, no trim). `render_settings` owns the offline RENDER_* mapping;
`tail_control` owns the panel-facing toggle/cycle/clamp/label logic sharing
the same `TailMode` enum and the same 8 s / -72 dB constants (single source
of truth — do not hardcode a second copy in either module).
- **Capture FX scope is enforced via FX-bypass + gain-neutralize, not a render
bit.** `render_settings::fxBypassPlanFor` selects which tracks (self /
ancestors / master) get their FX bypassed for a given `CaptureScope`; there
is no master capture scope (to capture the master, render a track instead).
- **Relative paths only, by construction.** `capture_paths::BankPaths`
separates the absolute render directory REAPER needs from the
project-relative path the `BankIndex` stores; `bankRelativeForName` spells an
enumerated folder entry the identical way `deriveBankPaths` spelled it at
capture time, so the prune core's exact-string match cannot drift.
- **Project-identity transition is GUID-primary.** `capture_paths`'s
`classifyProjectTransition` checks the minted GUID before the live
`ReaProject*` object, specifically because REAPER can recycle a closed
project's pointer address onto an unrelated project.
## Modules
- `wav_codec` — chunk walker + layout parse + float32 build + size-field patch + content hashes; the single pure RIFF/WAV owner (`wav_trim` is retired; `wav_codec` is the sole owner).
- `capture_realtime` (`core/capture`, **renamed from `realtime_record` in Q-W3** — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matching `drag_out`/`drag_out_win`) — the M8 realtime-record pure logic: capture scope + FX-tap point → `I_RECMODE`/`I_RECMODE_FLAGS` values, wet/dry → tap point, the recorded-file → `Sample` mapping, and the async record-phase state machine. Depends on `bank_model` for the plain `Sample`/`SourceMode` types. The transport/temp-track/send recipe lives in the shell (`shell/capture/capture_realtime_shell.cpp` + `capture_realtime_finalize.cpp`).
- `batch_capture` — pure batch-capture planner: maps source ranges to capture units and aggregates results.
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition``NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`.
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode``RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), and the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family.
- `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project.
## Gotchas
- `render_settings`'s `RENDER_SETTINGS`/`RENDER_NORMALIZE`/`RENDER_TAILFLAG`/
`RENDER_TRIMEND` bit values are transcribed verbatim from the SDK header
(`reaper_plugin_functions.h` lines ~3041/~3047/~3051/~3062) — re-verify
against the header before changing any bit value, per the root `CLAUDE.md`
API-verification rule.
- `kRenderPreFaderStems` (&8192) is deliberately **not** used — REAPER offline
render has no true pre-FX "dry" bit; FX scoping is done entirely by the
FX-bypass-around-render mechanism, never by a render bit.
- `tail_control`'s `kDefaultManualTailMs`/`kManualStepMs` and
`render_settings`'s `kMaxTailMs`/`kAutoTrimThresholdDb` are separate constants
in separate files by design (panel-facing default/step vs. runaway-guard cap)
— don't conflate them when touching either.
+223
View File
@@ -0,0 +1,223 @@
# src/core/instrument — pure VST3-instrument core (engine / map / ui)
## Scope
The ReaSampler 9000 instrument's pure, REAPER-free, VST3-free, unit-tested core, in three
subdirectories:
- **`engine/`** — the polyphonic voice engine, per-zone play params, pitch shifting,
velocity curve, and master-gain taper math.
- **`map/`** — the zone/keymap payload, the cross-artifact `ComponentState` codec, and the
small pure helpers the engine/shell share (bank-generation sync, bridge-read
marshalling, note-name parsing, Trigger frame↔fraction conversion).
- **`ui/`** — pure editor geometry/hit-test modules (layout, waveform, keyboard strip,
capture browser, param controls, envelope overlay/edit). These are geometry-and-math
only; the LICE draw + REAPER/VST3 plumbing is the `shell/instrument` editor shell,
**out of scope for this file** (owned by a parallel dispatch), along with the VST3
processor, `reaper_bridge`, `reasampler_embed`, and `vst_entry`.
## Invariants
### The three locked decisions this spec assumes (settled 2026-07-26)
- **D1 — native VST3.** Not JSFX. Full sampler sophistication, clean integration, and
access to the REAPER VST-host bridge.
- **D5 — Windows-only, VST3-only, REAPER-only.** No cross-platform DSP/build/signing
matrix, no multi-format wrapper, no standalone-in-other-hosts concern.
- **D6 — two products, tightly integrated.** A separate artifact, but not a divorced
file-only companion: via the VST-host bridge it reads the live `"reasampler"` project
ext-state and is project-aware. (The bridge mechanism itself is documented in
`src/core/wire/CLAUDE.md`.)
### The two seams (audio via files, mapping via live state)
- **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float
WAVs — project-relative, travelling with the `.rpp`. The instrument resolves those
paths the same way `persist` does (a shared convention, not a re-implementation).
There is no live PCM stream across the bridge, by design.
- **Live-state seam (the mapping, via the bridge).** For everything that is not raw
audio — the bank index, the mapping, which project is active — the instrument reads
the live `"reasampler"` ext-state via the bridge.
### The seam fields — what becomes a bank intrinsic (D-B, settled 2026-07-26)
The split model is the settled answer, mirroring the capture/placement separation:
- **Bank intrinsics (facts about the captured file) live on `Sample`.** Root note (the
MIDI note the sample was recorded at) and loop points (sustain-loop start/end for held
notes) are facts about the file, added as an additive field extension (same shape as
`provenance`).
- **The performance map (a creative arrangement) lives in the instrument.** Key zones,
velocity layers, round-robin groups, amplitude envelopes, and per-sample tuning/gain
trim are a performance choice, not a fact about a file — they belong to the instrument,
not the bank. This "who owns which field" rule (D-B) governs every performance-map
field added since, including play mode/AHDSR/Trigger params (S15), pitch engine mode
and pitch envelope (S16), key-tracking, preview velocity, and the velocity curve
(S-VIEW) — all are per-instance/per-zone `ComponentState`, never written to `Sample` or
the bank.
### The pure core (D3 — the load-bearing split)
The sampler's voice engine, envelope math, key/velocity mapping, repitch/interpolation,
and keymap resolution are a pure, REAPER-free, DAW-free, unit-tested module — the mirror
of `bank_model`/`peaks`/`view_mode_model`/`bank_book`. The VST3 wrapper (the
`SingleComponentEffect` subclass, bus setup, `process` marshalling, the `IPlugView` LICE
editor, and the bridge calls) is the thin shell — the only part that touches VST3 or
REAPER at all. Any VST3 or REAPER type leaking into this core is a bug.
- **The bank is one source; the instrument is another view of it (never a fork).** The
instrument is a pure consumer of the bank — it does not copy samples, does not own a
private sample store, and does not mutate the bank.
- **`Sample` field additions are additive and lossless.** No existing `Sample` field
changes; no `BankIndex` behavior changes.
- **Relative-paths-only survives.** The instrument resolves audio via the project-relative
machinery; it introduces no absolute paths.
### Channel mode — current reality
**Current reality (root `CLAUDE.md`, GA post-launch pass): the output bus is
permanently stereo.** `ChannelMode` is decode-only; the dynamic mono↔stereo bus
renegotiation (`setBusArrangements` per-instance toggle) has been deleted. Channel mode
auto-defaults from the loaded capture's channel count via a pure `channelModeFor` helper,
gated by a persisted `channelModeExplicit` flag (`ComponentState` v9). Mono source +
stereo mode → dual-mono (same signal both channels, centered); stereo source + mono mode
→ downmix (existing decode-side policy).
> **Superseded design, do not reintroduce:** an earlier "Channel mode — mono |
> stereo (D-E)" design specified a per-instance toggle that **dynamically
> renegotiates the REAPER audio bus** via `setBusArrangements`/`getBusArrangement`
> (the instrument reporting mono or stereo per instance and REAPER's routing
> following). That dynamic-bus-negotiation design was superseded by the GA fix
> above; root `CLAUDE.md` is current and wins.
### Sampling modes — Gate vs Trigger, pitch engine, pitch envelope (S15/S16 — settled, landed)
Daniel's directive (2026-07-26, verbatim): *"Sampling mode: Trigger vs Gate. Gate has an
AHDSR envelope. Trigger has fade in, % length, and fade out. Both modes have modifiable
start point, Gate has modifiable loop points too. In addition to amp env, there will be a
pitch envelope/curve (AD?) which is off by default."*
- **Gate — classic held note.** Note-on enters the amp envelope; note-off enters
release; a sustain loop applies for held notes. Envelope is **AHDSR**: `0→1` over
attack, hold at 1 over `holdFrames`, `1→sustain` over decay, hold sustain until
note-off, `level→0` over release. `holdFrames == 0` is exactly the pre-Gate ADSR — a
back-compat degenerate.
- **Trigger — one-shot drum-pad.** Note-on fires playback of a defined `%` of sample
length with a fade-in and fade-out ramp; note-off is ignored (the voice plays through,
no sustain loop). Frame span `[startFrame, playEnd)` where `playEnd = startFrame +
round(lengthFraction·(frames startFrame))`; amplitude ramps `0→1` over
`fadeInFrames` at the head and `1→0` over `fadeOutFrames` anchored to `playEnd`; fades
clamp so `fadeInFrames + fadeOutFrames ≤ play length`. Fade curve is equal-power
(constant-power sin/cos). **Note-off in Trigger is a no-op** — choke-on-note-off is
held/out of scope (fork S15-F1).
- **Both modes: modifiable start point.** Playback begins at `startFrame` (clamped `0 ≤
startFrame < frames`). Gate additionally has modifiable loop points; Trigger has none.
- **Pitch engine — Varispeed vs Preserve (per-zone toggle, S16).** Varispeed (current/
classic path): `ratio_ = pitchRatio(note,root)`, `readPos_ += ratio_` with linear
interp — resampling that couples pitch and duration; cheap, zero-latency, musically
right for drums/one-shots. Preserve (duration-preserving): the read advances at the
source rate while a pitch shifter transposes the output — musically right for
tempo-locked loops/phrases; **the engine default leans Preserve** (fork S16-F1).
Contract for Gate's sustain loop under Preserve: *loop the source, shift the output*
(loop points stay source-frame facts). `WDL_Resampler` is **not** a Preserve engine (it
is a resampler that couples duration) — never wire it as the duration-preserving path.
- **Pitch envelope — AD, off by default.** A short attack-decay pitch-offset curve
(`peakSemitones` over `attackFrames`, decaying to 0 over `decayFrames`) riding on top of
whichever pitch engine; a zero attack gives a pure percussive pitch drop. **Off by
default** — a regression that applies pitch modulation when the envelope is disabled is
a bug. Under Varispeed the offset is a per-frame multiply of `ratio_`; under Preserve it
is added to the shifter's shift amount.
- **Preserve RT discipline.** The shifter pre-warms at voice-allocation; no allocation in
`process()` in steady state. **Note (supersedes an earlier framing):** the
shifter's onset latency (~25 ms, half-window) was once described as "an
accepted property, not a defect." Root `CLAUDE.md`'s GA2 pass **eliminated** that onset
latency (ring buffer primed with the actual upcoming source at note-on instead of
zero-filled, so Preserve now speaks on frame 0, matching Varispeed) — a
cold-started/un-pre-warmed shifter producing a click or smear remains a bug.
- **S15/S16 stay channel-count-agnostic.** The mode/envelope logic is per-frame amplitude
and read-rate, independent of the stereo channel dimension — any S15/S16 code that
assumes a fixed (mono) channel count rather than operating per-frame pre-mix is a bug.
- **S15/S16 are Tier 01 engine features, not Tier 2/3** — do not let the held Tier-2
feature list (velocity layers / round-robin / filter work) drive their build shape.
### Non-goals / guardrails (instrument-specific; repo-wide invariants live in root CLAUDE.md)
- **No cross-platform / multi-format.** Windows-only, VST3-only, REAPER-only (D5). Do not
add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target.
- **The pure core stays REAPER-free *and* VST3-free.** Any VST3 or REAPER type leaking
into the voice engine / envelope / keymap / repitch module is a bug (the D3 split).
- **Channel mode is a performance choice, not a bank fact.** Never written to `Sample` or
the bank.
- **Do not spec Tier 2/3** from this directory. Tier 2 is held, Tier 3 is
optional-forever; don't let their feature lists drive Tier 01's build shape.
### Envelope overlay + draggable nodes (S-VIEW, settled 2026-07-27, landed)
The amp envelope is drawn as a curve over the Sample view's hero waveform at the shared
time base — Gate → the AHDSR shape, Trigger → the fade-in/unity/%-length/fade-out shape
anchored to `playEnd`. **The overlay is directly editable — draggable nodes
(SETTLED, S-VIEW-F2).** Dragging a node and the existing sliders are two surfaces onto
one model: both read/write the same zone envelope fields, so a drag updates the params,
the sliders reflect them live, and a slider edit re-lays the nodes — one source of truth,
structural (re-read-every-paint), not a listener chain. Nodes are monotonic in time (a
node cannot be dragged past its neighbours) and range-clamped to the same per-param
min/max the sliders enforce, so node-drag can never produce a param the slider couldn't.
Two pure modules split the forward (draw) and inverse (edit) maps — see `envelope_overlay`
and `envelope_edit` in Modules below.
### New performance-map parameters — ownership and persistence (D-B)
- **Key-tracking** — per-zone, additive/version-bumped component state, default 100%
(absent field on an older blob lifts to 100%, bit-identical playback).
- **Preview velocity** — a per-instance utility setting for the Sample view's
preview-trigger button (not a musical parameter of the capture); **persists across
reloads** via the instrument's own `ComponentState` (envelope-bumped), never via the
extension's `persist` ext-state module (that would make it project-global rather than
per-instance and leak an instrument concern into the extension's key space).
- **Velocity curve** — per-zone; the one non-back-compat surface in S-VIEW: an
already-saved zone with no stored curve now plays every velocity at unity under the
flat-default (Option A), not bit-identical to the old linear `velocity/127` mapping —
a deliberate, Daniel-approved behavior change (see `velocity_curve` in Modules).
## Modules
### `engine/`
- `sampler_core` — polyphonic voice engine with bounded stealing, user-parameterized voice count (132, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` Retrigger/Legato toggle), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots); per-zone `ZonePlayParams` (Gate/Trigger, AHDSR, pitch engine Varispeed/Preserve, AD pitch mod envelope), repitch/interpolation with loop-point-aware sustain. Preview injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` — no dedicated `PreviewCard`; preview obeys polyphony/mono/voice-stealing/envelopes.
- `zone_params.h` (`core/instrument/engine`) is the sibling header split out of `sampler_core.h` (T4-14/T4-17): the per-zone play-parameter value structs (`ZonePlayParams`/`AdsrParams`/`TriggerParams`/`PitchEnvParams`) and the per-instance mode enums (`ChannelMode`/`VoiceMode`/`MonoTrigger`) the engine, the codec, and the editor all share.
- `pitch_shift` — hand-rolled **correlation-aligned SOLA** (splice-overlap-add) pitch shifter for the Preserve playback mode: one active read tap chases the write head at the shift ratio; each splice jump is refined by a cross-correlation search so the new read point is waveform-aligned, then old and new taps are crossfaded (raised-cosine, amplitude-complementary). Replaces the prior dual-tap OLA whose fixed half-window tap offset caused anti-phase cancellation on many source frequencies. **GA2:** ring buffer **primed with the actual upcoming source** at note-on (was zero-filled) → gap-free frame-0 onset, ~25 ms Preserve onset latency eliminated (Preserve now speaks on frame 0, matching Varispeed), and real-content-bounded tail (last-window tail-truncation gone). No third-party dependencies; RT-discipline: no allocation in `process()`.
- `velocity_curve` — pure velocity→amp transfer curve: `VelocityCurve` evaluated by a FritschCarlson monotone cubic Hermite spline (no overshoot outside [0,1]). `eval(velocity)` called once per note-on. `flat()` default (y=1, every velocity→unity) replaces the prior fixed `velocity/127` path — a deliberate non-back-compat behavior change (Daniel-approved).
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift.
### `map/`
- `sample_map` — zone payload: zones keyed by note range. **Wall-clock times stored as rate-free SECONDS, resolved against the live project rate — NO hardcoded sample rates in `src/`** (Daniel's standing ruling, load-bearing). JSON round-trip.
- `component_state_io` (`core/instrument/map`) — the `ComponentState` envelope + zones-payload binary codec (envelope v1…v11, zones-payload v1…v7), split out of `sample_map` (Q-W2v, T4-13 ≡ T2-07) so BOTH artifacts can link the codec without the extension pulling in the whole voice engine (`sampler_core`/`pitch_shift`) to serialize one preset blob — the extension's `instrument_drop` and the instrument's processor read/write the identical bytes, so the cross-artifact contract cannot drift.
- `bank_sync` — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects.
- `bridge_marshal` — pure marshalling helper for the REAPER VST-host bridge read: interprets the `GetProjExtState` int return against its filled buffer.
- `note_entry` — parses a raw string into a clamped MIDI note [0,127]; accepts plain decimal integers or note names (C4==60, DAW convention).
- `trigger_seam` — pure Trigger frames↔fraction converter: owns the shared formula for converting between engine source-frame fade counts and the overlay's fractional representation, threading `startFrame` correctly through pack and unpack directions.
### `ui/`
- `editor_geometry` (`core/instrument/ui`) — VST3 editor layout: aliases the shared `core::ui::Rect` (+ `contains()`) rather than defining its own; owns `EditorLayout`/`layoutEditor(w,h)`, the Tier-0/Tier-1 sample-list and keymap-editor row layout/hit-test, and — hoisted here off the former `reasampler_editor.cpp` god-TU (Q-W2v, T2-06) — the r11 Sample-face band layout (`SampleBands`/`ClusterRects`/`channelToggleRects`) and the Zone-face content/legend/deck layout, so the editor shell only draws + routes.
- `keyboard_strip` — piano-keyboard strip: MIDI-note→key rect mapping, black/white key layout, hit-test, zone highlight overlay geometry.
- `waveform_view` — waveform/marker geometry: maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap.
- `capture_browser` — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing.
- `browser_scroll` — scroll + type-to-filter layered over `capture_browser`: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types.
- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types.
- `envelope_overlay` — pure amp-envelope→polyline geometry for the Sample-view envelope overlay (read from `envelope_overlay.h`): maps Gate's AHDSR shape or Trigger's fade-in/unity/%-length/fade-out shape to a polyline inside a rect at the shared time base (Gate: a bounded param-domain schematic, sample-length-free; Trigger: PCM-aligned wall-clock), every vertex clamped in-canvas (`x`/`y` inside the rect). Shares the `EnvNode`/`AmpEnvelope`/`timeToX`/`levelToY` vocabulary with `envelope_edit` so the drawn handle and its grab region agree pixel-for-pixel. No VST3/REAPER/LICE types at the boundary.
- `envelope_edit` — pure node hit-test + pixel-delta→clamped-param inverse map for the draggable envelope nodes (read from `envelope_edit.h`): `nodeAtPoint` resolves a grab to the nearest node within a pick radius (Chebyshev distance, draw-order tie-break); `resolveNodeDrag` maps a pixel delta since grab to a new `AmpEnvelope`, enforcing monotonic-in-time ordering between neighbouring nodes and the same caller-supplied per-param clamp bounds the sliders use — a drag can never produce a param a slider couldn't. Mirror of `card_drag`/`waveform_view`; the inverse of `envelope_overlay`'s params→polyline forward map, so node-drag and slider-edit read/write one shared model and can never diverge.
## Gotchas
- **Gate's envelope-overlay x-axis is schematic, not PCM-aligned** (per `envelope_overlay.h`'s FA2 contract note) — it does NOT line up with the waveform under it; only Trigger's x-axis is wall-clock/PCM-aligned. Don't assume the Gate curve is time-accurate against the sample.
- **Trigger's fade fields require a non-trivial converter, not a field copy.** `TriggerParams` (engine) stores fades as source *frames*; `AmpEnvelope` (the overlay's view struct) stores them as *fractions* of the played span. A converter is owed on both the pack (draw) and unpack (commit) directions — `trigger_seam` owns this formula; do not copy the fields directly.
- **`param_slider`'s linear slider rows are retired on the Zone panel** — per root `CLAUDE.md`'s FB2 note, the `Knob` primitive (`editor_geometry`/knob deck grammar) is now the only live consumer of that half of `param_slider` on the Zone face. Don't assume `param_slider`'s SLIDER row type is still drawn there.
- **Two superseded designs are called out in Invariants above**: the earlier
Channel-mode (D-E) bus-renegotiation design and the earlier Preserve-onset-latency
framing in the S16 guardrails. Root `CLAUDE.md` is the current source of truth
for both — do not reintroduce either superseded design.
+20
View File
@@ -0,0 +1,20 @@
# src/core/json — the hand-rolled JSON lexical layer
## Scope
The ONE hand-rolled JSON lexical layer used across the pure core: string/number/
bool/null tokens, the scoped object `Writer`, and the bounds-checked `Reader`
cursor. Domain grammars — what fields a bank, view-mode, or manifest blob actually
has — stay in the consumers; this module owns lexing/emitting only.
## Modules
- `json` (`core/json`) — the ONE hand-rolled JSON lexical layer (Q-W1): string/number/bool/null tokens, the scoped object `Writer`, and the bounds-checked `Reader` cursor, byte-compatible with the five pre-extraction per-module writers it replaced (`bank_model` / `bank_book` / `view_mode_model` / `owned_manifest` / `tail_control`). Domain grammars stay in the consumers; this owns lexing/emitting only.
## Gotchas
- Byte-compatible with the five pre-extraction per-module writers it replaced
(`bank_model` / `bank_book` / `view_mode_model` / `owned_manifest` /
`tail_control`) — a change here risks silently breaking round-trip compatibility
with ext-state blobs already persisted by projects written before the Q-W1
extraction.
+81
View File
@@ -0,0 +1,81 @@
# src/core/model — the pure bank/sample index and its multi-bank container
## Scope
Pure (REAPER-free, unit-tested outside the DAW) sample-index models: the single-bank
index, the multi-bank registry that wraps it, its JSON codec, the gap-preserving
per-bank slot carrier, the owned-file manifest, and the capture-recipe fingerprint.
No REAPER types, no filesystem I/O — see root `CLAUDE.md` for the pure-core/shell
split this directory sits on.
## Invariants
**Pool privileges (multi-bank).**
- The pool is privileged, not special-cased: structurally one `BankIndex` among many
in `bank_book`; semantically it always exists, is un-deletable, and un-renamable
(fixed id + fixed display name "Pool"). New projects and migrated single-bank
projects start with the pool and zero named banks. Enforced in the pure rules
layer, not just the UI.
- No action path may delete or rename the pool, leave a project with zero banks, or
evacuate the pool (the pool is evacuation's destination, not a source).
**Bank identity, movement, dedup.**
- Bank id is the stable key (GUID-style, minted on create); display name and ordinal
are mutable. Display names are unique — trimmed + case-insensitive (ASCII) —
enforced by `createBank`/`renameBank`; the pool's reserved name "Pool" is protected
by the same check.
- Movement moves the index entry, not the file: move/copy between banks is
index-only (remove from A's `BankIndex`, add to B's); the underlying file stays in
the shared project bank folder. Per-bank subfolders on disk are an explicit
non-goal.
- Dedup-by-hash is per-bank. Moving a sample whose hash already exists in the
destination bank collapses onto the existing entry there. Cross-bank dedup is not
enforced — the same hash may exist in the pool and a named bank simultaneously.
- Move is the default (removes from source, adds to destination); copy is the
deliberate secondary act (adds to destination, leaves source intact).
- Delete drops members (files are not deleted); evacuate returns all of a bank's
members to the pool (index-only, same destination-collapse rule). Evacuate cannot
be applied to the pool. A plain delete of a non-empty bank orphans those members
out of every index until prune reclaims their files — the UI confirms on
non-empty delete and offers evacuate as the alternative.
**Sample removal.**
- Remove is index-only: drops one `Sample` entry from one `BankIndex`; mutates only
index + ext-state, no file written/moved/deleted, no timeline item touched.
- Remove can orphan a file — the same designed orphaned-until-prune state a
non-empty delete-bank produces — when it drops the last index reference to a
file. Reclaimed later by prune, never by remove.
- The pool's contents are removable; the pool container is not. Remove-from-pool is
allowed.
- Remove scope is this-bank only (settled 2026-07-24): drops the entry from this
bank, leaving copies in other banks untouched. `scope: this-bank | all-banks` is a
latent seam; only this-bank is a surfaced verb.
- Removes are silent — no confirm dialog. Recoverability comes from batched REAPER
undo (`Undo_BeginBlock`/`Undo_EndBlock`): one Ctrl-Z restores the index entry.
This undo-batching is Phase-B-wide (create/rename/reorder/delete-bank, move, copy,
evacuate, and remove all batch this way). `hashReferencedElsewhere` is a tested
model API retained for Phase R prune; it has no shell caller in the remove path.
**Precision implications.**
- Relative-paths-only survives unchanged: every `BankIndex` in the book keeps the
relative-path invariant at its `add` boundary; movement is index-only so files
never relocate.
- Non-destructive: bank create/rename/delete/activate/evacuate and sample
move/copy/remove mutate only index + ext-state; no file is written, moved, or
deleted, and no timeline item is touched.
## Modules
- `bank_model``Sample` metadata struct + `BankIndex` (add/remove/query/tier/dedup-by-hash + JSON round-trip). Test it hard — it is the heart.
- `bank_book` — multi-bank registry: an ordered set of banks each wrapping a `BankIndex`. **Pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model.** Owns create/rename/reorder/delete of named banks, active-bank id, and index-only move/copy/remove of a sample between banks. The JSON round-trip lives in the sibling `bank_book_json` TU (Q-W5 split; serialize/deserialize via a private static `nameKey` seam) — one model, one codec, same public surface.
- `slot_map` (`core/model`) — the gap-preserving display-position carrier for ONE bank (sample id → slot, ≥0), extracted from `bank_book` (Q-W1): append/remove/reorder (insert-before-and-shift)/`reconcile` against live membership, `resetDense` migration seed, JSON round-trip. Wrapped (not merged) by `bank_book`.
- `owned_manifest` — the set of project-relative files the capture path itself created, persisted under the `"owned_files"` ext-state key, so the prune path can distinguish the bank system's own orphans from hand-dropped files.
- `provenance` — capture-recipe fingerprint: build/encode/compare a `rsprov1` fingerprint of scope, range, tail, rate/channels, track GUIDs, and FX-chain identity. **A thin reproducibility fingerprint — NOT a serialized chain to restore.**
## Gotchas
- `bank_book` wraps `BankIndex`; it does not modify it (additive — no `bank-id`
field on `Sample`). Do not add per-bank subfolders on disk or a global
cross-bank dedup — both are rejected-in-review non-goals.
- `bank_book_json` is a sibling TU, not a separate module — its round-trip is part
of `bank_book`'s public surface, not a distinct thing to describe separately.
+41
View File
@@ -0,0 +1,41 @@
# src/core/reclaim — pure prune orphan computation
## Scope
Houses the safety-critical "which files are orphans" decision for the file-lifecycle
(prune) pillar — filesystem-free, unit-tested before any I/O exists. The filesystem
enumeration and the actual deletion live in `shell/persist` (`prune_fs`), not here.
## Invariants
- **The load-bearing rule: remove creates orphans; prune reclaims them.**
Sample-remove and delete-bank drop index entries and may leave a file referenced
by nothing. Prune is the single path that turns such an orphan back into free
disk space. No other operation deletes a file; prune deletes only files that no
index references.
- Prune reuses the shape Design View already shipped (`view_mode_model`'s
`reconcile(liveGuids)`): prune reconciles files on disk against referenced files
(the union of every bank's index) and returns the orphan set to delete — same
pure pattern, one level down (files instead of GUIDs).
- Referenced-set is the union across ALL banks, pool included: a file is an orphan
iff no bank in the book references it. This is the safety-critical computation —
the prune null test is *prune never deletes a file that any index references.*
- Orphan attribution is an owned-file manifest (fork R-D): the book tracks the set
of files it has created; prune reclaims `(owned ∩ on-disk) referenced`. This
rejects folder-sweep (which would delete hand-dropped files).
- **Prune null test:** a prune of a folder whose every file is referenced by some
bank deletes nothing; a prune deletes exactly the `present referenced` orphan
set and nothing else.
- Never a referenced file; never a non-bank file — the union-across-all-banks rule
protects referenced files; the ownership-attribution rule (fork R-D) protects
hand-dropped files.
## Modules
- `prune_reconcile` — pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) referenced`; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists. Gains `mergeReferenced(bankRefs, liveInstanceHeldPaths)` (pS-usage) — unions live instance holds into the prune referenced-set so the pure orphan computation includes them.
## Gotchas
- Dry-run/confirm UX, trash-preferred deletion mechanics (fork R-C), and the
manual-trigger guardrail (fork R-E) are deletion-*mechanics* concerns, not
orphan-*computation* ones — they live in `shell/persist`, not here.
+92
View File
@@ -0,0 +1,92 @@
# src/core/ui — pure UI geometry, palette, and interaction-decision modules
## Scope
Pure, REAPER-free UI geometry, palette, and interaction-decision modules shared by
the extension's docked bank panel and the VST3 instrument's editor/embed surfaces:
layout math, hit-testing, hover/drag-gesture-precedence decisions, and the
role-based color palette. Does **not** include: the actual LICE drawing (`draw_kit`
lives in `shell/panel`; the editor's own paint code lives in `shell/instrument`),
REAPER/SWELL window or dialog mechanics, or the DAW-side Design View flag
application (`shell/view`).
## Invariants
Look-and-feel — visual design language (Phase L) (settled decisions, 2026-07-26;
L7 sub-pass, 2026-07-27):
- **DS-1 — toolkit discipline.** Draw with LICE + reused WDL/vwnd pieces directly;
external frameworks (iPlug2/JUCE/VSTGUI) are rejected. "**Caution, not a ban:**
keep hit-test **geometry** in pure CTest-covered modules — do not import vwnd's
retained-mode object model wholesale (its controls own their hit-test internally,
which would move geometry into untestable shell code and undercut the pure/shell
split)." This directory is that pure-geometry seam.
- **DS-2 — palette is role-based, not hardcoded hue**, in one constants block
(`theme`): `bg/base`, `bg/panel`, `bg/cell`, `line/hairline`, `text/primary`,
`text/dim`, `accent/primary`, `accent/secondary`, `accent/tertiary`,
`accent/hot`, `warn`. Neutral ladder sits in REAPER's mid-grey theme family
(`bg/base``#2b2b2b`, `bg/panel``#333333`, `bg/cell``#3a3a3a`,
`line/hairline``#4a4a4a`, `text/primary``#dcdcdc`, `text/dim`
`~#a0a0a0`+), elevation-ladder discipline (base < panel < cell by a few %,
micro-gradient + inner highlight/shadow carry elevation, not hard borders).
Three pastel accents carry categorical meaning: **primary (pastel lime) =
live/active/selected**, secondary (pastel teal) + tertiary (pastel purple) =
supporting categorical distinctions (kinds, not intensity). `warn` (red/amber)
is reserved **only** for byte-deleting or clip states (prune, delete).
- **WCAG-floor discipline (tight pairs to re-verify on any palette change):**
`text/dim` on `bg/panel`/`bg/cell` is the classic mid-grey-on-mid-grey floor
failure — must clear AA 4.5:1 body text. The three pastels as state
indicators/active fills on `bg/cell` have a shrunken contrast cushion
(~6:17:1, still clears 3:1 but re-check on any hue nudge). Body text on a
pastel fill is a tight AA 4.5:1 pair. Take the most pastel value that still
clears the floor, approached from the soft side, never re-saturated toward
neon "to be safe."
- **"Speed is the selling point" — a design constraint on this geometry, not
just the draw layer.** Sub-frame hover/press/drag feedback must repaint
immediately on the input message. **No decorative animation** — no
tweens/fades/pulses; the only permitted motion is a level/meter readout
following audio directly. Any glow/bloom state is a static drawn state, never
a pulse.
- **Precision/invariant implications (Phase L does not change these):** "the
pure/shell split holds" — all layout/hit-test stays in pure CTest-covered
geometry modules; the kit's *draw* half is shell, its *geometry* half is pure,
even where a WDL piece is reused. Look-and-feel work never touches capture,
placement, or bank data ownership.
- **L7 drag-gesture precedence is a pure decision helper.** The rule — leave
client rect → OS drag-out; else drop on a tab/other bank → move/copy; else
same-bank grid → reorder-to-slot (empty slot = place, occupied + no modifier =
insert-before-and-shift, occupied + Alt = replace) — is "encoded in a pure
decision helper (mirror `drag_out::decideGesture`)"; the shell only reads live
pointer/focus/client-rect/modifier state and calls it, then maps the resolved
gesture to a cursor via `SetCursor`. No cue or precedence logic belongs in the
shell.
## Modules
- `rect` (`core/ui`, header-only) — the ONE concrete pixel rectangle (Q-W1): XYWH storage + `right()`/`bottom()`/`ltrb()`/`contains()`, replacing 12+ byte-identical role structs (`ButtonRect`/`FooterRect`/`CellRect`/`KitBox`/…) and the VST side's separate LTRB `Rect`; every prior role name survives as a `using` alias at its old site (e.g. `editor_geometry::Rect`).
- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag.
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + three-accent pastel system, WCAG contrast-floor helpers.
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement.
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace.
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped.
## Gotchas
- The WCAG contrast-floor pairs above are real math in `theme`'s tests, not a
visual eyeball check — any new hue or role needs its own contrast-floor
assertion.
- `card_drag`'s precedence order must stay a pure decision helper mirroring
`drag_out::decideGesture` — don't let a shell reimplement gesture precedence
ad hoc; the cursor-cue mapping in the shell must stay a thin lookup over the
pure result.
- `rect`'s prior role names survive only as `using` aliases at their old call
sites — changing `rect.h` itself ripples across every directory that aliases
it (e.g. `editor_geometry::Rect`); check all alias sites, not just this one.
+19
View File
@@ -0,0 +1,19 @@
# src/core/util — small shared pure utilities
## Scope
Tiny, dependency-free pure helpers linked by both artifacts: whole-file byte
loading and unit-interval clamping.
## Modules
- `file_bytes` (`core/util`) — the ONE whole-file byte loader (Q-W1), linked by both artifacts; blocking I/O, off-audio-thread only.
- `clamp01` (`core/util`, header-only) — the ONE unit-interval clamp (Q-W1), replacing four per-module static copies; NaN passes through unchanged rather than collapsing to a bound.
## Gotchas
- `clamp01` lets NaN pass through unchanged rather than collapsing it to a bound —
this is deliberate (it replaced four per-module static copies that already
behaved this way); don't "fix" it to clamp NaN to 0 or 1.
- `file_bytes` does blocking I/O — off-audio-thread only, never call it from
`process()`.
+20
View File
@@ -0,0 +1,20 @@
# src/core/version — version/channel identity
## Scope
REAPER-free version/channel identity consumed by both artifacts (the REAPER
extension and the VST3 instrument) to derive binary names, ext-state namespaces,
command-id prefixes, action-name prefixes, dock idents, and version display
strings.
## Modules
- `app_version` — REAPER-free version/channel identity: CMake-sourced semver constant, ext-state stamp value, and the full set of channel-derived identity accessors. All channel strings derive from one `REASAMPLER_CHANNEL_IS_BETA` bit; no scattered `#ifdef`s in the shells.
## Gotchas
- All channel strings derive from one `REASAMPLER_CHANNEL_IS_BETA` bit — route new
channel-specific behavior through this module's accessors rather than adding a
scattered `#ifdef` in a shell. See root `CLAUDE.md` "Beta channel build" for the
full fan-out (binary name, ext-state namespace, command-id prefix, action-name
prefix, dock ident, version display).
+104
View File
@@ -0,0 +1,104 @@
# src/core/view — pure Design View mode model
## Scope
Pure, REAPER-free Design View model: mode/track membership, folder-derived
visibility, snapshot-based park/restore planning, new-content (GUID) detection,
and the managed/manual lane-identity convention that underlies per-item mode
separation (fixed lanes). Does **not** include: the actual DAW-side flag
application (hide, CPU-park, per-FX offline, restore via `B_SHOWINTCP` /
`B_SHOWINMIXER` / `B_MAINSEND` / `I_FXEN`) or the never-touch-master/mute/solo
enforcement — those live in `shell/view`.
## Invariants
Design View — additive phase spec (settled decisions; Two-canvas sub-phase,
settled 2026-07-23):
- **Membership.** Default = Arrange; every untagged leaf belongs to it. Leaves
opt in to Design (or any mode). No track appears in two modes at once except
via an explicit show-both toggle or parent/folder derivation.
- **Parents are derived, never tagged.** A parent/folder track is visible in
mode M if either any descendant leaf is visible in M, or the parent belongs to
M by its own membership; an untagged parent is an Arrange member by default. A
parent is never parked in any mode it is visible in. Master track is always
visible and never touched.
- **N-mode model, two-mode UI.** The data model carries arbitrarily many modes;
the UI ships Arrange + Design. A mode is (stable id, display name, ordinal).
- **Persistence.** Membership index + last-active mode + per-track flag
snapshots ride in the existing `"reasampler"` project ext-state namespace and
travel with the `.rpp`.
- **Non-destructive restore (enforced in the pure layer).** For every flag the
tool drives, snapshot the prior value before parking; on toggle-back, restore
from the snapshot, never to a hardcoded default. Round-trip (snapshot → park →
restore) returns every driven flag to its captured value — this is the
phase's trust anchor, the analog of the capture null test.
- **GUID-keyed, reorder-safe.** Membership keys on track GUID (`GetTrackGUID`),
never track index; tolerates unknown/stale GUIDs (pruned on reconcile via
`ViewModeModel::reconcile(liveGuids)`).
- **Relative/portable state only** in the persisted view section (GUID strings,
mode ids — no absolute paths, no index positions).
- **Show-both semantics.** A per-track "pin visible across modes" flag that
re-enables processing whenever shown. A show-both leaf appears in every
mode's visible set and is never parked — its driven flags stay at
snapshot/restored values. Stored on the membership record; persists;
togglable per selection.
- **Non-goals / guardrails:** No literal second canvas — a second window or
duplicated project stays rejected; subproject/second-project-file approaches
and overloading item `D_POSITION` with mode semantics (timebase-offset
regions) are rejected paths (the latter collides with the capture null test).
Every leaf is managed: an untagged leaf is an Arrange member and, when the
active mode is not Arrange, is fully parked and snapshot-restored exactly
like a tagged leaf; show-both is the only way to opt a leaf out of parking.
Restore from snapshot, never to a default: no hardcoded "on" restores.
**Two-canvas sub-phase (Phase D2/E) — settled and landed parts only:**
- **Mechanism: fixed item lanes.** Map mode → lane; toggle drives per-lane
play/show so only the active mode's lane is present. Items keep their real
position and real track — nothing is moved in time or deleted.
- **Membership: adoption rule for new items; active mode for new tracks.** New
tracks are tagged to the active mode at creation. New items follow an
adoption rule: if the item's track has pre-existing managed-eligible content
spanning exactly one mode, the item adopts that mode; the active-mode
fallback applies only when the track is empty or already spans multiple
modes. Items in manual lanes are excluded from the prior-mode computation and
are not auto-tagged at all. Membership is exclusive per item except via
show-both.
- **Managed vs. manual lanes — indexed and distinct.** The tool maintains a
lane-ownership index — per (track GUID, lane): managed (which mode owns it)
vs. manual (user-minted, outside the mode system). Mode operations touch only
managed lanes; manual lanes are never shown, hidden, silenced, or re-laned by
a toggle, and their `C_LANEPLAYS` stays exactly as the user set it. The
ownership index rides in `"reasampler"` `view_state` alongside the membership
index, GUID-keyed and portable. **New invariant — mode operations touch only
managed lanes:** "the 'which lanes may this toggle touch' decision is a pure
query over the ownership index; only reading REAPER's live lane state is
shell."
- **REAPER floor: v7** for this sub-phase (fixed lanes shipped in v7); no
version-gate branch — below v7 the sub-phase is simply unavailable.
- Precision invariants (null test, bit-identical repeats, non-destructive,
exact bounds, relative-paths-only) are entirely unaffected by this
sub-phase — no capture path changes; lane assignment and `C_LANEPLAYS` are
reversible flags, never a destructive operation.
## Modules
- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, JSON round-trip.
- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell.
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
- `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects.
- `lane_keys` — the pure, REAPER-free convention mapping a fixed lane's durable REAPER name (`P_LANENAME:n`) to the opaque lane-key `view_mode_model` keys by, plus the managed/manual heuristic both the toggle-apply path and the new-content/auto-tag exemption path share: `kManagedLanePrefix` ("reasampler:") stamps every lane the tool mints; `isManagedLaneName`/`managedLaneKey`/`laneNameForMode`/`modeIdFromLaneName` round-trip a lane name ↔ its owning mode id; `isOnManualLane` is the single predicate governing which lanes a toggle may drive and which items are exempt from auto-tag.
## Gotchas
- REAPER exposes no durable per-lane GUID — the only lane identity is the
ordinal `I_FIXEDLANE`, which REAPER renumbers on reorder/delete. Lane
identity must ride on the durable `P_LANENAME` (`lane_keys`), never the raw
ordinal, or a reorder will silently corrupt managed/manual ownership.
- `kManagedLanePrefix` ("reasampler:") is stable-forever like an action-id
string — changing it strands the ownership of every already-minted lane in
every already-saved project.
- `guid_diff::GuidBaseline` must have `reset()` called on every detected
project switch, or the next `observe()` will diff across two unrelated
projects and mass-tag (or miss) content.
+94
View File
@@ -0,0 +1,94 @@
# src/core/wire — pure ext-state and wire-format codecs, cross-artifact contracts
## Scope
The lexical/wire layer underneath the domain grammars: length-prefixed ext-state field
codecs, the little-endian byte codec, the `GetProjExtState` grow-loop retry policy, the
FOREVER-FROZEN VST3 class-UID macros, and the two mirror-image cross-artifact wires
(`assignment_request` extension→instrument, `sample_usage` instrument→extension) plus
the FX-drop payload builder (`instrument_drop`). Domain grammars themselves (what the
fields *mean*) stay in their own modules (`bank_model`, `sample_map`, `provenance`,
`bank_sync`, …) — this directory owns lexing/emitting/marshalling only, not domain
semantics.
This directory owns two cross-artifact contracts specifically:
- `reasampler_uid.h` is the FOREVER-FROZEN VST3 class-UID header shared by the runtime
`FUID` (`reasampler_vst.h`) and the `.vstpreset` hex string (`instrument_drop`) — so
binary identity and preset identity cannot diverge.
- `assignment_request` and `sample_usage` are mirror-image wires: the former carries the
drop payload extension→instrument, the latter carries usage records instrument→extension.
## Invariants
- **The VST-host bridge (the integration mechanism, stated once here).** A VST3
hosted inside REAPER can call back into REAPER's own API by
resolving function pointers by name over the host callback (`hostcb(&effect,
0xdeadbeef, 0xdeadf00d, 0, "FunctionName", 0.0)` — the same string-keyed API table the
extension uses; verified in `video_processor.h` and the `reaper_plugin_functions.h`
`GetProjExtState`/`SetProjExtState`/`EnumProjExtState` entries). The plugin can also
fetch its host context — the track/take/project it was instantiated in (opcode
`0xdeadf00e`). Consequence: the instrument reads the *same* live `"reasampler"`
ext-state that `persist` writes, follows the active project, and needs no
"point me at the bank folder" wiring — it asks REAPER which project it is in. Confirm
the bridge opcodes and by-name resolution against the vendored `vendor/reaper-sdk/sdk/`
headers (`reaper_plugin.h`, `video_processor.h`, `reaper_plugin_functions.h`) before
relying on new opcodes.
- **Instance-usage wire — `rsusage_<instanceGuid>` (pS-usage; pure portions only —
the extension-side scan shell `usage_scan` and the prune-abort behavior are owned
by the `shell/persist` layer, a parallel dispatch).**
Each VST3 instance holds a per-instance GUID persisted in `ComponentState` v11
(`instanceGuid` field; pre-v11 blobs mint the guid on first publish). At the tail of
every `reloadInstrument` call (off audio thread) the processor publishes its held
`SampleRefs` paths to the ext-state key `rsusage_<instanceGuid>` in the `"reasampler"`
namespace via `reaper_bridge::writeUsageExtState` — an entry point that is
**prefix-guarded** (accepts only `rsusage_`-prefixed keys, refuses all others), so the
read-only-bank invariant is structurally enforced. Direction: the instrument writes
usage keys; the extension reads them — the one sanctioned instrument→ext-state write,
a deliberate exception analogous to `assignment_request` on the other wire.
Usage records are **never cleared by the instrument at teardown** (REAPER destroys the
plugin instance when an FX chain is set offline, including Design View's CPU-park, so a
terminate-time clear would strip a still-live instance's record); liveness is decided
extension-side at prune-scan time.
- **The pure fold (`sample_usage::foldUsageRecords` / `usageHeldPaths`).** A record
counts iff its publishing track still hosts at least one instance (offline FX
included); a record with no track context counts while any instance exists; and when
records exist but zero instances were identified, **every** record's paths are
protected (identity-failure net — a matcher failure must never degrade toward
delete). The guarantee: a capture held by any live instance can never be deleted; if
the prune cannot determine with certainty which captures are held, it aborts
entirely (deletes nothing). Over-protection is the accepted residual; under-protection
is a data-loss bug.
- **Collision safety (`planUsagePublish`).** A persisted GUID is copyable (FX copy /
track duplication). `ownerNonce` — a per-lifetime nonce minted fresh in memory at
instance creation, never persisted — proves "exactly this incarnation wrote the key
last." `unioned` — a sticky multi-writer poison: once a same-track sibling is
detected, the key enters union-forever mode (holds only accumulate, never drop).
Resolution always leans over-protect: same-nonce + not-unioned → clean replace;
same-track foreign nonce or unioned → union; cross-track foreign nonce → remint under
a fresh key. None of the three directions can under-protect.
- **Deferred follow-up (TODO.md, not this dispatch's scope):** `ownerNonce` is not
persisted, so after save→reopen an instance cannot recognize its own prior-session
usage record — it unions and marks the record `unioned` forever, so prune stops
reclaiming captures the instance once held but no longer uses (safe, but the bank
folder grows unbounded). Persisting the nonce is deferred because a persisted nonce
would be inherited by a Ctrl+D in-place FX duplicate, and a divergent clone must
still be detected and protected fail-safe without reintroducing the sibling-drop bug.
## Modules
- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory.
- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge.
- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge.
- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns the `infoNamesFxHotspot` prefix classifier for `GetThingFromPoint` tokens. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure.
- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW.
## Gotchas
- `bytes.h`, `ext_state_read.h`, and `reasampler_uid.h` are header-only (no sibling
`.cpp` / no dedicated `_tests` target of their own) — they are consumed directly by
the modules named in their bullets above; don't go looking for a standalone build
target for them.
- `sample_usage` is deliberately silent on liveness *enumeration* (which FX instances
are currently live) — that scan lives in `shell/persist`'s `usage_scan`, not here. This
directory owns only the wire format and the two pure fold/collision decisions.
+53
View File
@@ -0,0 +1,53 @@
# src/shell/actions — bindable REAPER actions, drag/drop shells, ingest
## Scope
The bindable action families routed through REAPER's `command_id`/`gaccel`/
`hookcommand` contract (Design View toggle actions, bank actions, the prune
action, and the shared registration plumbing/table), plus the OS drag-out and
FX-drop shells, plus the extension-side ingest-through-the-bank shell. This is
where user-facing REAPER actions and OS-level drag/drop live; the underlying
mutation logic (bank verbs, prune's orphan computation, view-mode reconciliation)
is owned by other directories and only skinned here.
## Invariants
- **Ingest is an extension act; the instrument is a read-only bank consumer.** Any
instrument code path that captures, imports, inserts a timeline item, or writes
back into the bank is a bug — the instrument reads and plays only.
- **Ingest NEVER inserts a timeline item.** Arrange capture→bank→assign reuses the
existing capture add-path and assigns the resulting `Sample` id to the target
instance; it never places anything on the timeline — capture/placement
separation is load-bearing here same as everywhere else. Only the arrange-capture
surface writes the `assignment_request` wire; Media-Explorer import and file-drop
onto the bank panel do not, and neither affects a live instance's selection.
- **Every bank index verb wraps its mutation in a batched REAPER undo point**
(`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is
one Ctrl-Z.
- **The prune action is the ONLY file-deletion action in the system**; it opens no
undo point (file deletion is not REAPER-undoable). It halts on
`abortedUnreadableUsage` and prints the offending `rsusage_*` key names.
## Modules
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). **pS-usage:** `BANK_PRUNE_FOLDER` halts on `abortedUnreadableUsage` and prints the offending `rsusage_*` key names with clear instructions.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.**
## Gotchas
- **Structural wart, not yet fixed:** `ingest.cpp` / `ingest.h`, plus `ext_keys.h`
and `resource.h`, physically live at `src/` root rather than under
`shell/actions/` — Phase Q's reorg did not re-home these files into
`core/`/`shell/`/`app/`. `ingest` is documented here as its nearest sibling by
role, but the files themselves are not in this directory. This is a code
organization issue, not a documentation one — see Open questions in the
originating dispatch report.
- Media-Explorer import is single-file, pull-on-action (`OpenMediaExplorer` +
`MediaExplorerGetLastPlayedFileInfo`) — there is no enumerate-selected-files or
register-a-drop-handler API on the Media Explorer surface.
- REAPER exposes no drag-drop registration API; drop handling is only on
ReaSampler's own HWNDs (`WM_DROPFILES`/`IDropTarget` on the docked `bank_panel`).
A drop onto the VST3 editor window relaying to the extension is an unproven
spike, not a shipped path.
+42
View File
@@ -0,0 +1,42 @@
# src/shell/bank_ops — promptless bank-mutation verbs
## Scope
The single home for bank-mutation logic: create/rename/delete/evacuate/activate/
transfer/remove a sample or bank, plus the undo-batched persist that follows a
mutation. No prompts, no message boxes, no panel-state reads — this is the verb
seam that `shell/panel/panel_bank_ops` (menu/prompt UX) and `shell/actions/
bank_actions` (bindable-action UX) both consume as thin skins, so the mutation
logic has exactly one home. The in-model enforcement of bank rules (pool
privileges, uniqueness, etc.) lives in `core/model` (`bank_book`) — this directory
calls into that model, it does not reimplement its rules.
## Invariants
- **Pool privileges are inviolable.** The pool always exists, is un-deletable, is
un-renamable (fixed id + fixed display name "Pool"), and a project can never be
left with zero banks. No verb in this directory may delete or rename the pool,
or evacuate it (the pool is evacuation's destination, not a source).
- **Delete drops members; evacuate returns them.** Deleting a named bank drops its
member index entries only — files are never deleted by a bank op (file lifecycle
stays owned by the capture/prune path). Evacuate moves all of a bank's members
back to the pool (index-only, same destination-collapse-by-hash as move), leaving
the bank empty. A plain delete of a non-empty bank orphans those members out of
every index until prune reclaims them — an accepted, designed window, not a bug.
- **Movement is index-only.** Moving/copying a sample between banks removes/adds
the index entry only; the underlying file never moves on disk. No bank operation
writes, moves, or deletes a file.
- **One bank operation is one Ctrl-Z.** Every bank index verb wraps its mutation in
a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`)
via `persistBankOp` — this is enforced in this directory, not left to callers.
## Modules
- `bank_ops` (`shell/bank_ops`) — the promptless bank-mutation verb seam (Q-W6 lift out of `panel_bank_ops`): `bankOpCreate`/`Rename`/`Delete`/`Evacuate`/`Activate`/`Transfer`/`Remove` + `persistBankOp` (the undo-batched ext-state persist), each taking a `ReaSamplerSession&` and returning whether the model accepted the mutation — no prompts, no message boxes, no panel-state reads. `shell/panel/panel_bank_ops` (menu/prompt UX) and `shell/actions/bank_actions` (bindable-action UX) both consume these as thin skins, so the mutation logic has exactly one home.
## Gotchas
- The pool-privilege rules and uniqueness rules are enforced in the pure
`core/model` `bank_book` layer, not re-checked here defensively — if a mutation
looks like it should be rejected but isn't, the bug is more likely in `bank_book`
than in this seam. Reference `core/model`, do not duplicate its rules here.
+55
View File
@@ -0,0 +1,55 @@
# src/shell/capture — REAPER-facing capture backends and action bodies
## Scope
Everything that turns a capture request into a rendered file + populated `Sample`,
plus the action bodies that drive capture from REAPER's UI/action list: the two
concrete capture backends (offline render, realtime record), scope/source
resolution, the realtime in-flight state machine, single- and batch-capture
orchestration, insert-to-timeline, provenance stamping, and the shared
`MediaItem*`/`MediaTrack*` GUID-read helpers. Pure decision logic (what counts as
an orphan, how a range maps to capture units, etc.) lives in the corresponding
`core/` modules this shell calls into — this directory is the REAPER API surface
only.
## Invariants
This directory implements, but does not restate, the repo-wide capture precision
invariants (null test, bit-identical repeats, non-destructive, exact bounds,
relative-paths-only) and the load-bearing capture/placement separation — see root
`CLAUDE.md` §Precision invariants and §The load-bearing principle. Shell-specific
detail not covered there:
- **FX-bypass guard ordering.** `scope_resolve` reads the M10 provenance-assembly
inputs (track/item selection, FX-chain identity) BEFORE the FX-bypass guard
neutralizes the in-scope chain — provenance must see the chain as it really is,
not as capture temporarily leaves it.
- **Realtime capture drives off REAPER's transport across timer ticks**
`capture_realtime_shell` cannot block REAPER's UI for the duration of a realtime
record, so `begin`/`tick`/`abort` are async by construction and the temp-track +
send recipe lives in the shell, not the pure core.
- **`RunInsertSelected` is the one deliberate exception to capture-never-places**
(see `capture_orchestrator` below) — every other capture entry point writes only
a file + index entry.
## Modules
- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`.
- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain).
- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + owned-manifest record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places).
- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action.
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`.
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.**
- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively.
- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys.
- `item_read` — the ONE place a `MediaItem*` is read for its canonical GUID string (`itemGuid`) and for the durable `P_LANENAME` of the fixed lane it sits on (`itemLaneName`); extracted from previously-duplicated `itemGuid`/`itemLaneName` pairs in `view.cpp` and `bank_panel.cpp` — the item-read analog of `track_guid`'s single `MediaTrack*`→GUID-key formatter. Callers must already know the track is fixed-lane (`I_FREEMODE==2`) before calling `itemLaneName`; the pure `isOnManualLane` predicate handles the non-fixed-lane case separately.
## Gotchas
- This directory's governing precision invariants are the repo-wide capture
invariants in root `CLAUDE.md`, not a standalone spec block here.
- `capture` and `capture_realtime_shell` deliberately share NO common interface with
each other (the former `ICaptureBackend` was removed) — do not reintroduce one
without a real second polymorphic call site.
+105
View File
@@ -0,0 +1,105 @@
# src/shell/instrument — ReaSampler 9000 VST3 shells
## Scope
The REAPER/VST3-facing shells for the ReaSampler 9000 instrument: the read-only bank
bridge, the processor, the editor, the embed strip, and the VST3 entry point — plus
two small identity/helper headers this directory owns outright
(`reasampler_vst.h`, `editor_internal.h`).
The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`,
`sample_map`, `component_state_io`, `zone_params.h`, `editor_geometry`,
`keyboard_strip`, `waveform_view`, `capture_browser`, `browser_scroll`, `note_entry`,
`param_slider`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
`curve_popup`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
`core/wire` and is documented there — this directory consumes it but does not own it.
## Invariants
**The build shape (D-A, settled 2026-07-26 — bare Steinberg VST3 SDK + LICE editor).**
Bare Steinberg VST3 SDK, no JUCE, with the editor drawn in the same LICE/SWELL stack
`bank_panel` already uses. `SingleComponentEffect` (the SDK's combined
processor+controller base) plus the SDK's factory macros is the audio-processing
scaffolding. Drawing the editor in a VST3 `IPlugView` that hosts a LICE surface reuses
the `bank_panel` docking muscle, keeps the look house-consistent, and avoids JUCE's
AGPL-or-pay license posture. The `IPlugView`↔LICE bridge (window lifecycle, sizing,
event routing from the host into the draw/hit-test loop) is the same class of work as
docking `bank_panel`, not a new competence.
**Embedded TCP/MCP UI (D-D) — `reasampler_embed`.** A REAPER-hosted VST3 can draw its
own UI inline in the track/mixer control panel via `reaper_plugin_fx_embed.h` (the
plugin implements `IReaperUIEmbedInterface` — the same Cockos surface REAPER's own
embedded FX use). Because this uses the same LICE-class drawing as the main editor
path, it composes naturally with the bare-SDK-plus-LICE build. **Must-verify:** the
`IReaperUIEmbedInterface` contract and embed message/lifecycle against
`vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`.
**Channel mode (D-E) — current reality.** An earlier design (D-E, settled
2026-07-26) specified a per-instance mono/stereo toggle negotiating the REAPER
audio bus via `setBusArrangements`/`getBusArrangement`, with mono-source+stereo-mode
→ dual-mono and stereo-source+mono-mode → downmix as the cross-mode policy. **This
was superseded by the GA post-launch DAW-fix pass**: the output bus is now
permanently stereo, `ChannelMode` is decode-only (the dynamic mono↔stereo bus
renegotiation from the earlier design was deleted), and channel mode auto-defaults
from the loaded capture via `ComponentState` v9's `channelModeExplicit` flag + the
pure `channelModeFor` helper. Root `CLAUDE.md` is authoritative for this behavior —
do not reintroduce per-instance bus renegotiation.
**VST3 channel identity — the UID pair + the pairing surface (S18).** A beta-built VST
pairs with the beta extension only, a stable VST with stable only, both installable
side-by-side in one REAPER — one channel per binary; all channel identity derives from
the ONE `REASAMPLER_CHANNEL_IS_BETA` bit via the pure `app_version` module (no
scattered `#ifdef`s in the VST shell, except the one described below).
- **The UID-pair invariant is a permanent commitment.** The VST3 class UID is the
plugin's identity — a saved REAPER project records it and rebinds a saved instance
by it. BOTH channel UIDs (`reasampler_uid.h`'s stable + beta pairs) are frozen
forever once shipped; the channel bit selects which one is compiled into this
binary (one `DEF_CLASS2`, one class per binary — never both classes in one binary).
The UID selection is the ONLY channel `#ifdef` in the VST shell, because an
`INLINE_UID` needs literal brace-init tokens and cannot route through
`app_version`'s runtime string accessors.
- **Binary + display identity are channel-derived**, sourced from `app_version`'s
VST-name accessors — never a literal in `reasampler_vst.h`/`vst_entry.cpp`.
- **The complete pairing surface is structural, not per-key.** Plugin identity
(UID + filename + display) is channel-forked, and all wire keys live under the
channel-derived ext-state namespace — the two together make pairing complete: no
per-key or per-seam isolation work is ever needed for a new wire key.
- **Verify** all identity/factory wiring against the vendored Steinberg SDK
(`DEF_CLASS2` / `INLINE_UID` / `FUID` from `pluginfactory.h` + `funknown.h`).
**Non-goals / guardrails.**
- The instrument never captures and never inserts into the arrange. Playback is a
read-only act over the bank. Any instrument path that captures, places a timeline
item, or writes back into the bank is a bug.
- The instrument never ingests. Capture, import, and drop-ingest are *extension*
acts; the instrument only reads and plays. A drop onto the editor window (if ever
shipped) is relayed to the extension as an ingest request — the instrument never
writes the bank itself.
- No cross-platform / multi-format. Windows-only, VST3-only, REAPER-only (D5). Do not
add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target.
- The pure core stays REAPER-free *and* VST3-free — the voice engine / envelope /
keymap / repitch module takes no VST3 or REAPER type at its boundary; the shell
marshals. Any VST3 or REAPER type leaking into `core/instrument` is a bug.
- Verify Steinberg SDK, bridge, embed, and LICE-view surfaces against the vendored
headers before use.
## Modules
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. **pS-usage:** gains `writeUsageExtState` (prefix-guarded — accepts only `rsusage_`-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant.
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded keymap via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
- `reasampler_editor` (`shell/instrument/`: eight face-axis TUs — `editor_session` session/bridge state, `editor_controls` parameter plumbing, `editor_paint_sample`/`editor_paint_browse_zone` paint, `editor_input_sample`/`editor_input_browse_zone` input, `editor_platform` IPlugView/Win32 window plumbing, plus the pure `editor_geometry` layout hoist as the eighth axis; shared internals in `editor_internal.h`, no TU of its own — Q-W2v, T4-11 split of the former god-TU) — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; default face is the capture browser, then single-capture setup, with opt-in zones panel. Drop-onto-editor ingest is NOT shipped (deferred).
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout/hit-test to `embed_strip`.
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family (Q-W2v split), included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / spectral strip / root marker / title band), label helpers, deck group ids, and the velocity-curve box derivation — the former god-TU's anonymous-namespace helpers that more than one split TU needs. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/editor_internal.h`'s own header comment and body.)*
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
## Gotchas
- `editor_internal.h` is include-only — it has no TU of its own and must never become
a public seam; only the eight `reasampler_editor` face-axis TUs include it.
- The two VST3 class UIDs (`core/wire/reasampler_uid.h`, consumed via
`reasampler_vst.h`) are FOREVER-FROZEN — never regenerate an already-shipped UID.
- The UID selection `#ifdef` in `reasampler_vst.h` is the one deliberate exception to
"channel identity derives from `app_version` accessors, no scattered `#ifdef`s" —
`INLINE_UID` needs literal brace-init tokens, so it can't route through a runtime
string accessor.
+70
View File
@@ -0,0 +1,70 @@
# src/shell/panel — the docked bank-panel shell + the shared LICE draw kit
## Scope
The REAPER-facing shell for the docked bank panel: the eight `bank_panel` split TUs
(`panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` /
`panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via
`panel_state.h`), plus `draw_kit`, the shared LICE draw shell also consumed by the
VST3 editor (`shell/instrument/`).
Pure layout/hit-test/palette modules the panel draws through (`theme`,
`component_geometry`, `bank_grid`, `tab_strip`, `mode_switch`, `action_bar`,
`footer_bar`, `overflow_menu`, `prune_button`, `mode_enable`, `tooltip`, `card_drag`,
`card_meta`) live in `core/ui` / `core/model` and are documented there — this
directory consumes them but does not own them. The promptless bank-mutation verbs
(`bankOpCreate`/`Rename`/`Delete`/… + `persistBankOp`) that `panel_bank_ops` skins
live in `shell/bank_ops`, a sibling directory, not here.
## Invariants
- **Draw through the kit, by palette ROLE, not hardcoded hue.** The shared LICE-based
drawing kit is the one source of drawing for the whole system — a button, row,
slider, or waveform looks identical in the bank panel, the embed strip, and the VST
editor because it is the same kit function, drawn against palette roles (`bg/base`,
`bg/panel`, `bg/cell`, `accent/primary`, `accent/secondary`, `accent/tertiary`,
`accent/hot`, `text/primary`, `text/dim`, `line/hairline`, `warn`) rather than a
literal color. The kit palette stays abstract (role→color, one constants block), so
a whole visual direction is a single-file change (Phase L, DS-1/DS-2).
- **Dock-panel layout is a thorough redesign, not a light re-skin (DS-3).** The panel
lays out the full button/affordance inventory intuitively and uncluttered, then
applies the kit — but this does **not** restructure the panel bones: the
vertical-split / grid / tab structure is sound and stays as-is; a redesign designs
the layout of the button inventory *around* it, not through it.
- **What Phase L does not change (Precision / invariant implications):**
- The pure/shell split holds — all layout/hit-test stays in pure CTest-covered
geometry modules; the kit's *draw* half is shell, its *geometry* half is pure,
even where a WDL piece is reused. No hit-test math moves into untestable code.
- RT discipline is untouched — the kit is draw-thread only; nothing here touches
`process` or any off-thread reload handoff (a VST3-instrument concern).
- Read-only-over-bank is untouched — this is look-and-feel; no data-ownership
change.
- The capture/placement load-bearing principle is untouched — Phase L draws; it
does not capture, place, or mutate the bank.
- VST3 class UID / component-state contract is unchanged.
- Windows-only (D5) — font/GDI/HFONT choices assume Windows; no cross-platform
font-fallback concern.
## Modules
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Maintenance → Placement via `action_bar`, short labels, More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
- `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in.
- `panel_layout` — toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read).
- `panel_render` — the WM_PAINT draw.
- `panel_input` — click/wheel/keyboard routing + the new-content auto-tag timer.
- `panel_drag` — the hover + card-drag state machine + drop dispatch.
- `panel_thumbnails` — the PCM→envelope thumbnail cache + the bank-change fingerprint pass.
- `panel_audition` — the preview-playback engine.
- `panel_bank_ops` — the menu/prompt UX skin over the promptless `shell/bank_ops` verbs.
- `draw_kit` — shared LICE draw shell: `fillSurface`, `drawButton`/`drawSlider`/`drawListRow`/`drawWaveform`, cached-font `text()`, full interaction-state model, double-buffer preserved. Consumes `theme` + `component_geometry`.
## Gotchas
- **No external UI framework.** iPlug2 / JUCE / VSTGUI are rejected (DS-1). LICE +
reused WDL pieces are the toolkit; reject any path that pulls in a new framework.
- **No hit-test geometry in untestable shell code.** Even when reusing a WDL piece
(e.g. a `vwnd` control for a long scroll list), layout/hit-test math stays in pure
CTest-covered modules — do not import a WDL control's retained-mode object model
wholesale, since its controls own their hit-test internally and that would move
geometry into untestable shell code.
- Verify every LICE/WDL/SWELL API name/signature against `vendor/WDL` before use.
+60
View File
@@ -0,0 +1,60 @@
# src/shell/persist — project ext-state persistence, prune filesystem I/O, usage scan
## Scope
The persist seam: project ext-state read/write (`session` / `ext_state_io`), the
prune path's filesystem half (`prune_fs`), and the extension-side instance-usage
scan (`usage_scan`) that feeds prune's referenced-set. Internal helpers shared only
within the persist TU family live in `persist_internal.h`. The pure orphan
computation is owned elsewhere (`core/reclaim`); the pure usage wire is owned
elsewhere (`core/wire`) — this directory is the REAPER/filesystem-facing half only.
## Invariants
- **Prune is the single, exclusive file-deletion authority.** No bank op, no
capture op, no Design View op deletes a file; if any path other than prune
deletes a bank file, reject it in review.
- **Dry-run first, always; no silent deletion.** Prune reports before it deletes
(orphan count, reclaimed size, and — for a small set — the files); actual
deletion is a confirmed second step. No periodic/background sweep.
- **Referenced-set is the union across ALL banks, pool included**, further unioned
(pS-usage) with every live instance's held paths via `usage_scan`
`prune_reconcile::mergeReferenced`. A file is an orphan iff no bank AND no live
instance references it.
- **Safest platform deletion available.** Trash-preferred, unlink fallback — Windows
routes through `SHFileOperationW` (`FOF_ALLOWUNDO`, verified against SDK
10.0.26100); macOS/Linux fall back to unlink (no portable SWELL trash surface).
`prune_fs` is the only module that calls this.
- **Manual, explicit trigger only** — a bindable action + a `bank_panel` button,
never a silent background sweep.
- **Instance-usage fail-safe (pS-usage):** a capture held by any live ReaSampler
9000 instance can never be deleted by prune. If any `rsusage_*` record is
unreadable or ambiguous, prune **aborts entirely and deletes nothing**
over-protection is the accepted residual, under-protection is a data-loss bug.
`usage_scan` decodes every `rsusage_*` key, enumerates every ReaSampler 9000 FX
instance (all tracks incl. master, normal + record/input chains, containers
recursively, take FX), and folds via the pure `sample_usage::foldUsageRecords` /
`usageHeldPaths` (a record with no live instance context protects all its paths —
identity-failure net, never degrades toward delete). This is read-only at
prune-scan time: `usage_scan` writes no ext-state.
- **`PruneReport` carries `abortedUnreadableUsage` + `offendingUsageKeys`**; dry-run,
orphan-set, and reclaim each independently abort (delete nothing) when usage
state is unreadable. `BANK_PRUNE_FOLDER` (in `shell/actions`) halts on this flag
and prints the offending keys.
## Modules
- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, `OwnedManifest` JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `prune_fs` hosts the prune dry-run / full-set orphan queries (supplying `referencedPaths()` + `owned().paths()` to the `prune_reconcile` pure core) and is **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. **pS-usage:** the prune scan unions instance usage via `usage_scan`; `PruneReport` carries `abortedUnreadableUsage` + `offendingUsageKeys`; dry-run / orphan-set / reclaim each independently abort (delete nothing) when usage state is unreadable.
- `usage_scan` — extension-side prune-scan shell (pS-usage): at prune-scan time, enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and folds with `sample_usage::foldUsageRecords` / `usageHeldPaths` to produce the set of held paths — or `abortPrune` when any record is unreadable (fail-safe: an unreadable record may protect anything, so the prune halts). Feeds `prune_reconcile::mergeReferenced`. Read-only: writes no ext-state.
- `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, project-dir derivation, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses.
## Gotchas
- The pure orphan computation (`prune_reconcile`, `(owned ∩ present) referenced`)
is documented under `core/reclaim`, not here — do not duplicate its spec in this
file.
- The pure usage wire (`sample_usage`: `UsageRecord`, `planUsagePublish`,
`foldUsageRecords`/`usageHeldPaths`, `identityMatches`) is documented under
`core/wire`, not here.
- `persist_internal.h` is an internal seam, not a public header — do not include it
outside `session.cpp` / `ext_state_io.cpp` / `prune_fs.cpp`.
+85
View File
@@ -0,0 +1,85 @@
# src/shell/view — Design View mode application shell
## Scope
The REAPER-facing half of Design View: applying a mode's visibility/processing
state to live tracks (park/restore), snapshotting flag values before parking, and
restoring from snapshot on toggle-back. The mode registry, membership derivation,
and the pure park/restore planner are owned by `core/view` (`view_mode_model`) —
this directory is the shell that reads/writes REAPER track flags, it does not
decide membership or mode rules.
## Invariants
- **Never touches master or `B_MUTE`/`I_SOLO`.** The tool owns only visibility,
`B_MAINSEND`, `I_FXEN`, and per-FX offline, on every managed leaf, tagged or
untagged. User mute/solo survives every toggle untouched; the master track's
visibility flags are never driven (the SDK forbids `B_SHOWINTCP`/`B_SHOWINMIXER`
on master).
- **Parking a track** (inactive-mode leaf) drives `B_SHOWINTCP=0`, `B_SHOWINMIXER=0`
(hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and
`TrackFX_SetOffline(track, fx, true)` for each FX (reclaim CPU) — full CPU-park,
not mix-removal-only.
- **Non-destructive restore.** For every flag the tool drives, snapshot the prior
value BEFORE parking; on toggle-back restore FROM the snapshot, never to a
hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag
to its captured value — the phase's trust anchor, the analog of the capture null
test.
- **GUID-keyed, reorder-safe.** Membership/snapshot keys on track GUID
(`GetTrackGUID`), never track index; tolerates unknown/stale GUIDs (pruned on
reconcile).
- **Relative/portable state only** in the persisted view section (GUID strings,
mode ids — no absolute paths, no index positions).
- **Documented caveat:** offlined FX re-instantiate when a track returns to the
active mode — stateful plugins (convolution, loaded samplers, tail-holding
effects) re-initialize on return (possible load hitch, un-persisted internal
state lost). Accepted cost of the CPU reclaim; surfaced at the toggle affordance
(tooltip).
- **Show-both semantics:** a per-track "pin visible across modes" flag re-enables
processing whenever shown. A show-both leaf appears in every mode's visible set
and is never parked — its driven flags stay at snapshot/restored values, FX
online, in the mix. ("Show but keep parked" is not offered.) Stored on the
membership record; persists; togglable per selection.
- **No literal second canvas.** A literal second arrange surface, a second window,
or a duplicated project stays rejected — reject any such path in review.
**Two-canvas sub-phase (Phase D2/E) — settled and landed parts, DAW-application half:**
- **Fixed-lane item-level separation mechanics.** Map mode → lane; toggle drives
per-lane play/show so only the active mode's lane is present. Items keep their
real position and real track — nothing is moved in time or deleted. SDK surface
(verified present in `vendor/reaper-sdk`): track-side `I_FREEMODE = 2`,
`I_NUMFIXEDLANES`, `C_LANEPLAYS:N`; item-side `I_FIXEDLANE`, `C_LANEPLAYS`,
`B_FIXEDLANE_HIDDEN`. `I_FREEMODE` changes require `UpdateTimeline()` to take
visible effect.
- **Inactive-mode content is hidden AND silenced.** The off-mode lane is set
`C_LANEPLAYS = 0` — neither shown nor played — consistent with exclusive
membership and with D1's "flipping modes is a real change, not cosmetic."
Show-both is the deliberate opt-out for a lane that must stay audible across
modes.
- **Capture placement is mode-aware.** An explicit placement while in Design mode
— including capture-and-place — lands the item in the Design lane; the same
rule governs manual insertion. The capture load-bearing principle is untouched:
capture still writes a file + index entry and never auto-inserts; this governs
only *where* an explicit placement lands.
- **REAPER floor: v7** for this sub-phase (fixed lanes shipped in v7); no
version-gate branch — below v7 the sub-phase is simply unavailable.
Item→lane membership rules (the adoption rule, exclusive-per-item membership, the
managed/manual lane distinction, and the lane-ownership index) are model concepts
owned by `core/view` — see that directory's Invariants; this directory only
applies the resulting lane state to live tracks.
## Modules
- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. **Never touches master or `B_MUTE`/`I_SOLO`.**
## Gotchas
- The pure mode model (`ViewModeModel`, membership, `reconcile(liveGuids)`, the
snapshot-based park/restore planner) lives in `core/view` — reference it, do not
duplicate its spec here.
- The Two-canvas sub-phase (Phase D2/E)'s settled DAW-application rules
(fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are
reflected in Invariants above; the membership/lane-ownership model concepts
it also covers live in `core/view`'s Invariants.