# 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 nineteen 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

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

`core/` holds pure, unit-testable logic — no REAPER or VST3 SDK types, each with a corresponding `<module>_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 nineteen 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/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/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::<subsystem>` 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<float>&`** 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.

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