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,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.
|
||||
Reference in New Issue
Block a user