238 lines
18 KiB
Markdown
238 lines
18 KiB
Markdown
# 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-three 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.
|
|
|
|
On a multi-config generator, `cmake --build build` with no `--config` builds **Debug** —
|
|
there is no `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, and no IPO/LTO setting anywhere in
|
|
the build, so nothing is optimized or inlined at that default. The performance
|
|
guardrails and structural heuristics below (header-inline hot paths, "no LTO
|
|
configured") presume an **optimizing** build. Shipping, installing, or judging
|
|
performance requires the Release config explicitly:
|
|
|
|
cmake --build build --config Release
|
|
ctest --test-dir build -C Release
|
|
|
|
Every pure module has a corresponding `<module>_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 **Release** build's binary (`build/Release/` on a multi-config generator — not the default `Debug/` output) 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-three 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 (bake / engine / map / note / ui) |
|
|
| `src/core/instrument/bake/` | the resample bake's pure half — the programmed note resolved to a frame window, the offline render over a bake-only voice engine, and the post-bake reset |
|
|
| `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/instrument/note/` | the programmed capture-signal model — musical divisions, tempo resolution, anchored offsets |
|
|
| `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/` / `note/` / `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.
|
|
|
|
## 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.
|
|
|
|
## The resample bake — the one crossing from instrument into bank
|
|
|
|
A click inside the ReaSampler 9000 editor bakes the dialed sound into a bank capture. The
|
|
split is: **the instrument renders, the extension banks.** The instrument produces the audio
|
|
on its own voice path in its own process (so the bake is the object code that made the sound
|
|
the user approved, immune to engine-version skew between the two artifacts), stages it
|
|
outside the bank folder, and invokes ONE extension action over the VST3 host bridge; the
|
|
extension lands it and answers over the same per-instance key, synchronously, inside that
|
|
call. Consequences that bind:
|
|
|
|
- **The extension's link graph does not gain the voice engine.** `sampler_core` /
|
|
`pitch_shift` / the filter are NOT linked into `reaper_reasampler` — a link edge to any of
|
|
them means the design drifted back to an extension-side render.
|
|
- **No arrange mutation and no deletion.** The bake writes a file plus an index entry, like
|
|
every other capture. "Replace" means the bank entry now denotes the recapture; the
|
|
superseded file survives on disk until a prune reclaims it — the iterate loop's recovery
|
|
floor.
|
|
- **The bake adds nothing to `process()`.** It renders on the UI thread over a separate
|
|
`VoiceEngine`, with the live-parameter block detached.
|
|
|
|
## 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.
|