15 KiB
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 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.sdkThe VST3 target (
reasampler_vst) is gated onEXISTS .../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 twenty 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; no call site yet) |
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:
peaksenvelope compute (computeEnvelope/lastFrameAboveThresholdover full PCM): NO virtual dispatch, NOpeaksinterface, NO added header→TU indirection.computeEnvelopestays a free function onconst std::vector<float>&so it inlines as today. Relocate + namespace only; never wrap in an abstraction.- Audition / preview:
panel_auditionmay 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_lifecyclemust not change the tick's branch shape. - JSON extraction is off all hot paths — safe to abstract freely.
FxBypassGuardruns 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:
- 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. - Templates are the right tool for compile-time dedup — use them where earned. The LE
byte-codec
putLE/readLEis earned: compile-time dispatch, zero runtime cost, off the hot paths. The rect family is NOT: the types differ in name only, so one concreteui::Rect— a template there would model nothing. - 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_coresplit that would put virtual envelopetick()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.mdalready 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 ismain.cpp. Every other.cppincludesreaper_plugin_functions.hwithout the define and getsexterndeclarations for the global API function pointers. - REAPER dlopen()s any
reaper_*.dll|dylib|sofound inUserPlugins/and calls theReaperPluginEntryexport (produced byREAPER_PLUGIN_ENTRYPOINT).rec->GetFuncresolves API pointers;rec->Registerplugs 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 inshell/actions/action_registry(ActionTableRow: suffix, phrase, flat function-pointer handler) — registration,hookcommanddispatch, 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: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 viachannelCommandId(suffix)andchannelActionName(phrase)fromapp_version— the FOREVER-STABLE contract applies per channel (stable and beta each have their own permanent id family).rec->Register("gaccel", &accel)— puts the action in the Actions list.rec->Register("hookcommand", ...)— receives every action fired; claim only your own id, returnfalseotherwise.- 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.