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