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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 0–1 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 0–1'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 (1–32, 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 Fritsch–Carlson 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:1–7: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.
|
||||
@@ -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()`.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user