Files
reasampler/CLAUDE.md
T

173 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repo identity and current state
**ReaSampler** is a per-project audio sample-bank capture tool that builds two artifacts: the REAPER extension (`reaper_reasampler`) and **ReaSampler 9000**, a Windows-only VST3 sampler instrument (`reasampler_9000.vst3`, `core/instrument/` + `shell/instrument/`, second CMake target `reasampler_vst`, gated on the vendored `vendor/vst3sdk` submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout. CONTEXT.md is the authoritative spec — settled decisions, invariants, guardrails, and not-yet-built specs; it is large, so locate the relevant phase section by grepping its headings and read only that section with an offset rather than reading it whole. Build detail for landed phases lives in CONTEXT-ARCHIVE.md. Every REAPER API name cited there is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. A post-S-VIEW DAW-fix pass has landed (all 52 suite tests green): envelope nodes fully editable in both modes (every Gate stage A/H/D/S/R + Trigger zero-fade-out node, param-domain schematic scaling, 8 px min node separation, all nodes clamped in-canvas); gap-free per-column waveform render (`columnMinMax` homed in `peaks`, `waveformColumnCount` in `component_geometry`, shared via `drawWaveform`); `param_slider` `Knob` primitive (7→5 o'clock arc, needle, vertical-drag); zone-bleed fix 3a (`reconcileSingleCaptureZones` in `sample_map`). The voice-system redesign is also landed: `sampler_core` gains user-parameterized voice count (132, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` retrigger/legato toggle), an isolated `PreviewCard` (dedicated preview voice outside the MIDI pool — never steals from/into it; unity-Preserve zero-latency bypass scoped to it), and two-tier panic (CC 123 = release, CC 120 = immediate hard-stop incl. Trigger one-shots); processor sums the preview card alongside the engine + drain, `retireIdleDrain()` retires fully-idle drain snapshots, and voice-param edits rebuild from the already-decoded PCM (no bank re-read/WAV re-decode) via the drain-slot swap; `ComponentState` envelope bumped v6→v7 (voiceCount/voiceMode/monoTrigger bytes; pre-v7 blobs lift to 16/Poly/Retrigger). **FB1 Sample-view recomposition (r11) has also landed** (suite 55/55 green): all linear sliders replaced by radial **knobs** in a fenced **knob deck** (groups: AMP ENVELOPE / PITCH / PITCH ENV / VOICE / MASTER); mode toggles are compact in the caption row, not full-width; the **hero waveform runs full-width** (elastic band, 840×620 default preserved); the inline velocity-curve box is replaced by a **28×28 curve preview button → centered popup** with right-click node delete; voice-band controls (count / Poly-Mono / Retrig-Legato) are placed in the VOICE deck group; a **post-mixer per-sample-ramped master gain** (−∞…+24 dB, no zipper) is placed in the MASTER deck group, persisted as `masterGainLinear``ComponentState` envelope bumped v7→v8 (pre-v8 blobs lift to unity gain). Three new pure `src/vst/` modules landed: `knob_deck` (group-box + caption-row + knob-cell geometry, deterministic wrap, hit-test), `curve_popup` (sheet/close/box geometry + outside-sheet dismissal test), `master_gain` (dB↔linear taper math, −∞…+24 dB). **FB2 Zone-panel parity (r11, 2026-07-28) has also landed** (suite 55/55 green): the Zone param panel now uses the same knob deck + curve-preview-button/popup grammar as the Sample face — one control grammar across both surfaces of the one per-zone storage site; Zone-authoring affordances (+Add Zone / Delete, the piano-key strip, Low/High/Root legend) are preserved; VOICE and MASTER groups remain Sample-only (per-instance). `param_slider`'s linear slider rows are retired on the Zone panel (the FA4 `Knob` primitive is now the only live consumer of that half of `param_slider`). **This completes the r11 editor recomposition (Wave B / Phase S editor redesign).** A **GA post-launch DAW-fix pass** has also landed (suite 55/55 green): `pitch_shift` rewritten from dual-tap OLA (anti-phase cancellation → spectral garbage on repitched notes) to **correlation-aligned SOLA splices** with a ratio-scaled raised-cosine fade (clean pitch shift past +24 st); `Voice::start` applies a **bounded blend** (`out*(1-w) + ref*w`, w decaying from 1.0) at takeover boundaries — mono retrig/fallback, poly at-cap steal, and preview re-trigger — superseding the earlier `(1-amp)` envelope-complement gate that zeroed the compensation on Trigger/zero-attack restarts; the output bus is now **permanently stereo** (`ChannelMode` is decode-only; the dynamic mono↔stereo bus renegotiation is deleted) with channel mode **auto-defaulting from the loaded capture** via new `ComponentState` **v9** (`channelModeExplicit` flag) + a pure `channelModeFor` helper; `SetCapture` moved to drag-arm in `bank_panel` so a first straight-out drag arms correctly; and the Design/Arrange mode-toggle action now calls `bankPanelInvalidate()` so the panel footer reflects the new mode without requiring a button click. **Preview via real MIDI note path (pS, 2026-07-28):** the dedicated `PreviewCard` is RETIRED; preview now injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` (same path host MIDI uses), so it obeys polyphony/mono/voice-stealing/envelopes; the processor no longer sums a separate preview voice; the unity-Varispeed-bypass demotion (GA2 primed shifter speaks on frame 0 anyway) is removed. **Self-contained playback (pS, 2026-07-28):** `ComponentState` bumped v9→**v10** with a `SampleRefs` table — per referenced sample, the instance owns 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 now a browser source (loading a capture copies its reference in); the reopen-heal timer + poll-to-play apparatus are removed; pre-v10 blobs lift to empty refs and re-save self-contained. **Usage-detection (pS-usage, 2026-07-28):** new pure module `sample_usage` + REAPER-facing shell `usage_scan` implement the instance-usage wire (`rsusage_<instanceGuid>` per-instance ext-state keys); each live ReaSampler 9000 instance publishes the captures it holds (from its `SampleRefs`) at `reloadInstrument` time; the extension reads every usage record at prune-scan time, liveness-folds against the live FX enumeration, and unions the surviving held paths into the prune's referenced set — so a capture held by any live instance can never be an orphan and `BANK_PRUNE_FOLDER` can never delete it; `prune_reconcile` gains `mergeReferenced`; `persist`'s `PruneReport` gains `abortedUnreadableUsage` + `offendingUsageKeys`; dry-run / orphan-set / reclaim each independently abort (delete nothing) on unreadable usage; `actions` halts and prints offending keys; `reaper_bridge` gains `writeUsageExtState` (prefix-guarded — refuses non-`rsusage_` keys); `ComponentState` bumped v10→**v11** (`instanceGuid` field; pre-v11 blobs mint guid on first publish); new `sample_usage_tests` build target (pure, no REAPER/DAW). **Phase Q (2026-07-29, landed on `phase-q`)** reorganized all of `src/` into `core/` (pure, subsystem-namespaced: `model`/`view`/`capture`/`audio`/`ui`/`reclaim`/`version`/`json`/`util`/`wire`/`instrument/{engine,map,ui}`), `shell/` (REAPER/host-facing: `capture`/`panel`/`view`/`persist`/`actions`/`instrument`/`bank_ops`), and `app/` (`main.cpp`), splitting several god-modules along the way (`bank_panel` into eight `shell/panel/` TUs, the VST3 processor/editor god-TUs, `capture.cpp`, and `persist.cpp`) under a soft ~600-line-per-TU ceiling; `src/vst/` no longer exists.
## One-time submodule setup
git submodule update --init
Vendors three submodules (see `.gitmodules`):
- `vendor/reaper-sdk``sdk/reaper_plugin.h`, `sdk/reaper_plugin_functions.h`, SWELL headers
- `vendor/WDL` — WDL utilities and the SWELL cross-platform Win32 layer
- `vendor/vst3sdk` — Steinberg VST3 SDK (Windows-only; requires a nested init after the top-level init):
git submodule update --init vendor/vst3sdk
cd vendor/vst3sdk && git submodule update --init pluginterfaces base public.sdk
The VST3 target (`reasampler_vst`) is gated on `EXISTS .../pluginfactory.cpp` — configure quietly omits it if the slice is absent.
## Build and test
cmake -B build -S .
cmake --build build
ctest --test-dir build
Every pure module has a corresponding `<module>_tests` executable target that runs without REAPER or a DAW. `CMakeLists.txt` is the authoritative target list. The two loadable-module targets are `reaper_reasampler` (the REAPER extension `.dll`/`.dylib`/`.so`) and `reasampler_vst` (the VST3 instrument; Windows-only, omitted if the `vendor/vst3sdk` slice is absent). The `sample_usage_tests` executable target runs the pure unit tests for `sample_usage` (no REAPER, no DAW).
### Beta channel build
To build the fully isolated beta binary (`reaper_reasampler_beta`), pass the channel flag at configure time:
cmake -B build-beta -S . -DREASAMPLER_CHANNEL=beta
cmake --build build-beta
The flag threads through `configure_file``version_generated.h` and fans out via `app_version` into the binary name, ext-state namespace (`"reasampler_beta"`), command-id prefix (`CEREBELLUM_REASAMPLER_BETA_`), action-name prefix (`"ReaSampler beta: "`), dock ident, and version display (the configured version string with a `-beta` suffix appended). The default build (no flag) is byte-identical to the stable identity.
The VST3 target forks identically: `REASAMPLER_CHANNEL=beta` produces `reasampler_9000_beta.vst3`; the default produces `reasampler_9000.vst3`. The beta VST pairs **only** with the beta extension — each channel carries its own per-channel VST3 class UID, preventing a saved instance from rebinding across channels.
### macOS / Linux: SWELL dialog resources
`src/resource.rc` must be pre-processed by SWELL's resgen once per platform:
php vendor/WDL/WDL/swell/swell_resgen.php src/resource.rc # macOS; Linux reuses the output
Add the generated file to the appropriate `APPLE` / Linux `target_sources` block in CMakeLists.txt. The SWS extension build is the canonical reference for this step.
### Install / reload
There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folder (Options → Show REAPER resource path) and restart REAPER. Extensions load at startup only.
## Architecture: the load-bearing split
**Pure core (no REAPER types, unit-testable outside the DAW):**
- `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.
- `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.
- `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.
- `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_model``Sample` metadata struct + `BankIndex` (add/remove/query/tier/dedup-by-hash + JSON round-trip). Test it hard — it is the heart.
- `peaks` — waveform min/max bin computation from raw PCM; does not depend on REAPER's peak API.
- `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.
- `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.
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
- `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`.
- `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.
- `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.
- `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.
- `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`).
- `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.**
- `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.
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
- `batch_capture` — pure batch-capture planner: maps source ranges to capture units and aggregates results.
- `action_buttons` — pure action-button strip layout/hit-test.
- `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.
- `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.
- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge.
- `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.
**REAPER-facing shells:**
- `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.**
- `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.
- `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.
- `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.
- `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`.**
- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys.
- `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.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `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.**
- `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.**
- `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`.
- `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.
**VST3 instrument (`core/instrument/{engine,map,ui}`, plus shared substrate in `core/{audio,ui,wire}`) — pure core:**
- `sampler_core` — polyphonic voice engine with bounded stealing, user-parameterized voice count (132, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` Retrigger/Legato toggle), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots); per-zone `ZonePlayParams` (Gate/Trigger, AHDSR, pitch engine Varispeed/Preserve, AD pitch mod envelope), repitch/interpolation with loop-point-aware sustain. Preview injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` — no dedicated `PreviewCard`; preview obeys polyphony/mono/voice-stealing/envelopes.
- `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. `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()`.
- `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.
- `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.
- `note_entry` — parses a raw string into a clamped MIDI note [0,127]; accepts plain decimal integers or note names (C4==60, DAW convention).
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
- `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.
- `velocity_curve` — pure velocity→amp transfer curve: `VelocityCurve` evaluated by a FritschCarlson monotone cubic Hermite spline (no overshoot outside [0,1]). `eval(velocity)` called once per note-on. `flat()` default (y=1, every velocity→unity) replaces the prior fixed `velocity/127` path — a deliberate non-back-compat behavior change (Daniel-approved).
- `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.
- `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.
- `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.
**VST3 instrument (`shell/instrument/`) — shells:**
- `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.
## REAPER extension contract (src/main.cpp)
- 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.
- REAPER dlopen()s any `reaper_*.dll|dylib|so` found in `UserPlugins/` and calls the `ReaperPluginEntry` export (produced by `REAPER_PLUGIN_ENTRYPOINT`). `rec->GetFunc` resolves API pointers; `rec->Register` plugs extension callbacks in.
- Action registration pattern (preserve this for all new actions). Since Q-W6, `main.cpp`'s own action family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is driven by a data-driven table in `shell/actions/action_registry` (`ActionTableRow`: suffix, phrase, flat function-pointer handler) — registration, `hookcommand` dispatch, and the unload mirror-unregister all iterate that SAME table, so adding an action touches one table row rather than three separate mechanisms. The other action families (`shell/actions/design_view_actions`, `bank_actions`, `prune_action`) register through their own TUs the same way. The per-step contract below is unchanged:
1. `rec->Register("command_id", (void*)"STABLE_FOREVER_STRING")` — mints a persistent command id. **Never change this string after shipping**; user keybindings key off it. Since Phase V (V4), ids and display names are composed via `channelCommandId(suffix)` and `channelActionName(phrase)` from `app_version` — the FOREVER-STABLE contract applies per channel (stable and beta each have their own permanent id family).
2. `rec->Register("gaccel", &accel)` — puts the action in the Actions list.
3. `rec->Register("hookcommand", ...)` — receives every action fired; claim only your own id, return `false` otherwise.
4. On unload (`rec == nullptr`), mirror-unregister everything with the same strings prefixed by `'-'`.
## Product design docs
`docs/product/` holds the product-design reasoning behind each phase — the "why we chose this" that predates the spec. They are large and are cited by section from `CONTEXT.md` and `PLAN.md`; **grep for the cited section rather than reading a file whole**. `docs/cmake-cheatsheet.md` is a standalone build-system reference.
Files: `capture-tail.md`, `code-organization.md`, `design-view.md`, `midi-playback.md`, `multi-bank.md`, `provenance.md`, `removal-and-prune.md`, `versioning-and-release.md`, `visual-design-language.md`.
## The load-bearing principle
**Capture and placement are separate acts.** Capturing audio writes a file to the bank and adds an index entry. It **never** puts an item in the arrange view. Placement is a distinct, on-demand action (`insert` module / `InsertMedia`). Any code path that auto-inserts a capture into the timeline violates the purpose of the tool and **must be rejected in review**.
## Precision invariants — required before any feature ships
- **Null test:** a dry offline capture of a range, re-inserted at its source position, nulls to silence against the source. Ship as a verification action.
- **Bit-identical repeats:** identical offline capture requests produce identical files.
- **Non-destructive:** capture never mutates source items or tracks; the realtime backend's temp track is created and removed cleanly, and source routing is restored.
- **Exact bounds:** no rounding of the requested range; no added silence unless a tail is explicitly requested; channel count preserved (no silent stereo fold).
- **Relative paths only** in the persisted `BankIndex`.
- **Capture FX scope:** two scopes only — item = item/take FX only; track = item FX + the selected track's own track FX. There is no master scope (to capture the master, render a track instead). For both scopes, the out-of-scope chain (ancestors + master track, plus the item's own track for item scope) has its FX, gain, and pan/width/pan-law/mode neutralized to unity — the master track is bypassed as out-of-scope chain, not captured as a scope. Range (time selection or razor) is orthogonal.