# 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: `core/` never includes REAPER or VST3 SDK types, `shell/` is where those hosts are actually touched, `app/` is the extension entry point. Every REAPER API name cited in project docs is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. Per-module detail — what each file owns, its invariants — lives in the twenty-one per-directory `src/**/CLAUDE.md` files; see the compact map in "Architecture: the load-bearing split" below to find the right one. Landed-phase history lives in `docs/ARCHIVE.md`; current work lives in `docs/COMPLETED.md`, `docs/TODO.md`, and `docs/TODO-1.0.md` — see "Project docs" below. ## Settled decisions - **Capture modes:** offline render (deterministic, the default) AND realtime record (for hardware / performed FX). The two backends deliberately share no interface — their lifecycles genuinely differ (offline is synchronous, realtime is async begin/tick/abort) — but both produce identical bank entries. - **Bank scope:** per-project, travels with the `.rpp`. Files live in a project-relative subfolder; the index persists in project ext state. No absolute paths anywhere in the index. - **Material:** must handle full-mix/stem bounces, chops/one-shots, and single-cycle/wavetable grabs equally. That means exact sample-accurate bounds, explicit tail control, channel-count preservation, and loop/zero-crossing handling all matter from day one. ## 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 On a multi-config generator (Visual Studio, Xcode) the bare `ctest` command above reports every test as "Not Run" — add `-C Debug` (or whichever config was built) to resolve the test executables. Single-config generators (Ninja, Make) need no such flag. Every pure module has a corresponding `_tests` executable target that runs without REAPER or a DAW. Targets are declared per directory: each `src/**/CMakeLists.txt` owns its own libraries and their test targets, added via `add_subdirectory` from the root, which keeps only repo-global settings (version, channel, vendor paths). `cmake/reasampler_targets.cmake` holds the two shared declaration helpers. 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 `src/app/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 `core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `_tests` target that runs without a DAW. `shell/` holds the REAPER/host-facing shells — where those SDK types are actually touched. `app/` is the extension entry point. Each of the twenty-one directories below carries its own `CLAUDE.md` with the full module list and that area's invariants — open the relevant one for detail; this file states only repo-wide truth. | Directory | Scope | |---|---| | `src/app/` | REAPER extension entry point | | `src/core/audio/` | pure audio-data math | | `src/core/capture/` | pure logic behind the capture pillar | | `src/core/instrument/` | pure VST3-instrument core (engine / map / ui) | | `src/core/instrument/engine/filter/` | pure per-voice resonant TPT/SVF filter (HP→BP→LP / HP→notch→LP morph, drive stage), run by each `Voice` between the pitch and amp stages | | `src/core/json/` | the hand-rolled JSON lexical layer | | `src/core/model/` | the pure bank/sample index and its multi-bank container | | `src/core/reclaim/` | pure prune orphan computation | | `src/core/tracking/` | the consolidated file-tracking system — birth/lineage records and the one authority answering prune's protected set and the resample's replace-vs-add | | `src/core/ui/` | pure UI geometry, palette, and interaction-decision modules | | `src/core/util/` | small shared pure utilities | | `src/core/version/` | version/channel identity | | `src/core/view/` | pure Design View mode model | | `src/core/wire/` | pure ext-state and wire-format codecs, cross-artifact contracts | | `src/shell/actions/` | bindable REAPER actions, drag/drop shells, ingest | | `src/shell/bank_ops/` | promptless bank-mutation verbs | | `src/shell/capture/` | REAPER-facing capture backends and action bodies | | `src/shell/instrument/` | ReaSampler 9000 VST3 shells | | `src/shell/panel/` | the docked bank-panel shell + the shared LICE draw kit | | `src/shell/persist/` | project ext-state persistence, prune filesystem I/O, usage scan | | `src/shell/view/` | Design View mode application shell | ## Directory and namespace layout The top-level split is by the pure/shell discipline: `core/` never includes REAPER or VST3 SDK types; `shell/` is where those host types are actually touched — the discriminator is "may this file touch a host type, REAPER *or* VST3 SDK." Subsystem directories sit beneath `core/` (see the table above); `core/instrument/` further subdivides into `engine/` / `map/` / `ui/`. Namespaces mirror directories — `reasampler::` for `core/`, house style for `shell/`. `app/` holds `main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and dispatch. For the module list within any one directory — what lives there, its invariants — open that directory's own `CLAUDE.md` rather than looking here. ## Performance guardrails (HARD CONSTRAINT — Daniel's non-negotiable) The reorg must cost **zero runtime.** The two hot paths must keep their exact call/inline shape; the following are acceptance criteria on every point: - **`peaks` envelope compute** (`computeEnvelope` / `lastFrameAboveThreshold` over full PCM): **NO virtual dispatch, NO `peaks` interface, NO added header→TU indirection.** `computeEnvelope` stays a **free function on `const std::vector&`** so it inlines as today. Relocate + namespace only; never wrap in an abstraction. - **Audition / preview**: `panel_audition` may be its own TU, but the call stays a **direct call-through, not virtual.** - **Realtime-capture tick**: keep the idle fast-path a **single pointer test**; `realtime_lifecycle` must not change the tick's branch shape. - **JSON extraction** is **off all hot paths** — safe to abstract freely. - **`FxBypassGuard`** runs per-capture, not per-frame — keep it **stack RAII**; never heap-allocate or virtualize it. **Net:** every recommended split falls on a cold path or preserves call/inline shape on the two hot ones. *A split that would add a hot-path indirection is out of scope — rework it or drop it.* ## Structural heuristics (Daniel, 2026-07-28 — acceptance criteria phase-wide) Three heuristics postdate the original framing and bind every wave. They **generalize** the three-hot-path performance guardrail above — they do not replace it: 1. **More directories is a must; more files is good; ~600-line file ceiling.** SRP applies to namespaces, encapsulation, and file organization alike. The ceiling is the *bar*, the audit's named seams are the *method*: a file landing over ~600 needs a responsibility seam, not an arbitrary bisection (bisection-to-hit-the-number is rejected). A documented hot-path exception (`sampler_core.cpp`) is legitimate; silent overshoot is not. 2. **Templates are the right tool for compile-time dedup — use them where earned.** The LE byte-codec `putLE`/`readLE` is earned: compile-time dispatch, zero runtime cost, off the hot paths. The rect family is NOT: the types differ in name only, so one **concrete** `ui::Rect` — a template there would model nothing. 3. **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts *anywhere* — not just the three named hot paths; prefer static polymorphism where the types are compile-time-known. A by-class `sampler_core` split that would put virtual envelope `tick()`s on the per-voice-per-sample path is exactly the blowout this forbids. ## Comment conventions (Daniel, 2026-07-29 — driving the tree-wide comment-reduction pass) Comments carry *why*, and context where non-obvious — never *what* the code already says. - File headers stay brief (~5 lines); no titled prose sections inside them. - Don't restate invariants a directory's own `src/**/CLAUDE.md` already owns — those files are the home for area invariants. - No wave/ticket/milestone IDs in comments. A one-line "do not reintroduce X" warning is fine without the ticket number. - Keep: RT-safety, allocation, and threading constraints; SDK facts confirmed by experiment; compressed regression history; format/version-ladder semantics; warnings against a plausible-but-wrong change. - When unsure whether a comment is load-bearing, keep it. ## REAPER extension contract (src/app/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; **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`. ## Project docs Plan-style docs live under `docs/`: - `docs/ARCHIVE.md` — pre-1.0 history, rarely read, not a source of context for current work. - `docs/COMPLETED.md` — 1.x landed items. - `docs/TODO.md` — deferred follow-ups with recorded rationale. - `docs/TODO-1.0.md` — the raw 1.x work list, not yet structured into a plan. ## 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 — the tool's trust anchor. Ship as a verification action. (Verification action cut per `docs/product/provenance.md` — manual verification only.) - **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. ## Non-goals / guardrails - No auto-insertion of captures into the arrange (see the load-bearing principle). - Do not depend on REAPER's peak API for thumbnails; compute from the captured file. - Do not silently time-stretch on insert; conform is opt-in. - Do not trust cited REAPER API names blindly — verify against the SDK header.