# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Repo identity and current state The CMake project and binary are now named `reaper_reasampler`. This is **ReaSampler** — a per-project audio sample-bank capture tool. The MPE modules (`mpe_model`, `mpe_view`) have been removed. M0–M8 are complete (bank_model, peaks, capture offline+realtime, persist, bank_panel, insert, capture action family, RealtimeRecordBackend, tail T1+T2+T1-followons). Phase B multi-bank (B1–B5, B-cap), Phase D1/D2 Design View (D1–D5, D2-W1–W3-B), Phase V versioning/beta-channel (V1/V3, V4), M10 provenance + re-capture from source, Phase R Reclaim (R1–R3: prune-reconcile core, dry-run shell, guarded deletion + action + panel button), and M11 in full (batch capture, action-button strip + keybinding labels, conform-on-insert verified-extant, native OS drag-out) have all landed. M9 slots deferred indefinitely. The *discipline* — pure REAPER-free testable core split from REAPER-facing shells — is **preserved** throughout. CONTEXT.md is the authoritative spec and build roadmap. Read it first for any non-trivial task. 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. ## One-time submodule setup git submodule update --init Vendors two 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 ## Build and test cmake -B build -S . cmake --build build ctest --test-dir build Key targets (see CMakeLists.txt for the full list): | Target | Kind | Purpose | |---|---|---| | `bank_model_tests` | executable | Pure unit tests for `bank_model` — no REAPER, no DAW. | | `peaks_tests` | executable | Pure unit tests for `peaks` — no REAPER, no DAW. | | `capture_paths_tests` | executable | Pure unit tests for `capture_paths` — no REAPER, no DAW. | | `view_mode_model_tests` | executable | Pure unit tests for `view_mode_model` — no REAPER, no DAW. | | `view_tree_tests` | executable | Pure unit tests for `view_tree` — no REAPER, no DAW. | | `mode_switch_tests` | executable | Pure unit tests for `mode_switch` — no REAPER, no DAW. | | `bank_book_tests` | executable | Pure unit tests for `bank_book` — no REAPER, no DAW. | | `wav_trim_tests` | executable | Pure unit tests for `wav_trim` — no REAPER, no DAW. | | `owned_manifest_tests` | executable | Pure unit tests for `owned_manifest` — no REAPER, no DAW. | | `app_version_tests` | executable | Pure unit tests for `app_version` — no REAPER, no DAW. | | `provenance_tests` | executable | Pure unit tests for `provenance` — no REAPER, no DAW. | | `prune_reconcile_tests` | executable | Pure unit tests for `prune_reconcile` — no REAPER, no DAW. | | `prune_button_tests` | executable | Pure unit tests for `prune_button` — no REAPER, no DAW. | | `batch_capture_tests` | executable | Pure unit tests for `batch_capture` — no REAPER, no DAW. | | `action_buttons_tests` | executable | Pure unit tests for `action_buttons` — no REAPER, no DAW. | | `drag_out_tests` | executable | Pure unit tests for `drag_out` — no REAPER, no DAW. | | `reaper_reasampler` | loadable module | The actual extension binary (`.dll` / `.dylib` / `.so`). | ### Beta channel build (Phase V, V4) 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 (`"0.9.01-beta"`). The default build (no flag) is byte-identical to the pre-V4 stable identity. ### macOS / Linux: SWELL dialog resources `src/resource.rc` must be pre-processed by SWELL's resgen once per platform: php vendor/WDL/WDL/swell/mac_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):** - `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. Fed a known signal, asserts envelope. 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. Mirror of `bank_model` for the Design View phase. - `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell; no REAPER types at the boundary. - `mode_switch` — REAPER-free segment layout + hit-test math for the bank_panel's Design View mode switch; divides a header rectangle into N equal segments and hit-tests a point to a segment. Mirror of `bank_grid`. - `bank_book` — multi-bank registry (Phase B): an ordered set of banks (pool seeded as bank-zero + named banks), each wrapping a `BankIndex`. Owns create/rename/reorder/delete of named banks, pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model, active-bank id, index-only move/copy/remove of a sample between or from banks, `hashReferencedElsewhere` cross-bank reference query, JSON round-trip + legacy-`bank_index`→pool migration. Wraps `BankIndex` (bank_model untouched; no `bankId` on `Sample`). - `owned_manifest` — owned-file manifest seam (Phase B B-cap): the set of project-relative files the capture path itself created, persisted under the `"owned_files"` ext-state key, so Phase R prune can distinguish the bank system's own orphans from hand-dropped files. Deliberately decoupled from `bank_book` — tracks files created, not index membership. Phase R (R1/R2) consumes it; no prune logic here. - `app_version` — REAPER-free version/channel identity (Phase V, V1+V4): CMake-sourced semver constant (`appVersion()`), ext-state stamp value (`stampVersion()` — numeric triple only, no channel suffix), `parseVersion`/`versionLess`/`classifyWritingVersion`, and the full set of channel-derived identity accessors (`extStateNamespace()`, `commandIdPrefix()`, `actionDisplayPrefix()`, `binaryName()`, `dockTitle()`, `dockIdent()`, `channelCommandId()`, `channelActionName()`). All channel strings derive from the one `REASAMPLER_CHANNEL_IS_BETA` bit threaded in via `configure_file` → `version_generated.h`; no scattered `#ifdef`s in the shells. - `wav_trim` — 32-bit-float WAV parse + header-aware truncate plan (RIFF/data size rewrite) for the realtime tail's PCM decay-scan trim (T2). Rejects WAVE_FORMAT_EXTENSIBLE with non-float SubFormat GUID. Depends on `peaks` for the `AudioSample` float alias. - `provenance` — capture-recipe fingerprint (M10): build/encode/compare a `rsprov1` length-prefixed fingerprint of scope, exact range, tail, rate/channels, track GUIDs, and order-sensitive FX-chain identity; parse/compare for drift detection on re-capture. A thin reproducibility fingerprint — NOT a serialized chain to restore. Drives `BankIndex::updateInPlace` / `BankBook::updateSampleInPlace` (order-preserving, id-stable) on re-capture. - `prune_reconcile` — Phase R pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) − referenced` (exact-string path match); `buildPruneReport` tallies count/bytes/display-capped file list; `pruneDeletePlan` produces the confirm-time staleness intersection (`confirmed ∩ freshOrphans`). REAPER-free, filesystem-free. The safety-critical "which files are orphans" decision — hard-tested here before any I/O exists. Also: `BankBook::referencedPaths()` additive const union query (all banks incl. pool, de-duped) added to `bank_book`. - `prune_button` — Phase R pure layout/hit-test for the `bank_panel` footer Prune button: `computePruneButton` (right-anchored, suppressed gracefully when footer is too narrow) + `hitTestPruneButton`. Mirror of `mode_switch` / `tab_strip`. - `batch_capture` — M11 pure batch-capture planner: maps a list of source ranges to ordinal capture units, drives per-unit capture via a shared `captureAndIndexOne` helper, and aggregates mixed results. No REAPER types at the boundary. - `action_buttons` — M11 pure action-button strip layout/hit-test: divides a strip rect into N action buttons, min-width overflow-hiding, label formatting with "(unbound)" fallback, `ActionButtonRect` struct. Mirror of `mode_switch` / `bank_grid`. - `drag_out` — M11 pure OS drag-out module: gesture-boundary decision (internal drag becomes OS-bound when the pointer leaves the panel client rect), path-list assembly with dedupe and missing-file skip. No REAPER types at the boundary. **REAPER-facing shells:** - `capture` — `ICaptureBackend` interface; `OfflineRenderBackend` (deterministic default) and `RealtimeRecordBackend`. Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`. - `insert` — placement via `InsertMedia`; conform-to-project-tempo is an explicit opt-in flag, never silent stretching. - `bank_panel` — docked LICE-drawn grid: thumbnails, audition, multi-select, keyboard navigation; a 28px action-button strip (M11) between the split body and tail footer that fires capture/insert/re-capture actions via `NamedCommandLookup` + `Main_OnCommand` and shows live keybinding labels via `kbd_getTextFromCmd`; OS drag-out hook (M11) initiating an OS-level copy drag when the pointer leaves the panel client rect, via `drag_out` + `drag_out_win`. - `persist` — project ext state (`SetProjExtState` / `GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON (`"banks"` key) + `ViewModeModel` JSON (`"view_state"` key) + `TailSetting` JSON (`"tail_setting"` key) + `OwnedManifest` JSON (`"owned_files"` key) + writing-version stamp (`"version"` key, written via `stampVersion()` on every `saveToActiveProject()`); project-relative path resolution. A `projectconfig` hook (`BeginLoadProjectState(isUndo)`) triggers a deferred session reload on undo/redo so Ctrl-Z/redo visibly restores book/view/tail/manifest in-session. Hosts `ReaSamplerSession::pruneDryRun()` (read-only orphan enumeration via M4 project-relative resolution) and `pruneOrphanSet()` (full-set query for the R3 delete path); supplies `referencedPaths()` + `owned().paths()` to the `prune_reconcile` pure core. - `view` — Design View shell: reads the folder tree via `view_tree`, snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline) and derived visibility on parents; 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 used by both the view shell and the actions layer. - `provenance_shell` — FX-chain identity queries via `TrackFX_*` / `TakeFX_*` APIs; collects source-item paths and parent-detection inputs to feed the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture when every resolving source item maps by exact normalized path (case-folded on Windows) to exactly one bank sample; ambiguous/mixed cases record nothing conservatively. - `drag_out_win` — M11 OS drag-out shell: Windows OLE `DoDragDrop` / `CF_HDROP`, copy-only structurally (`DROPEFFECT_MOVE` not offered, no source-deletion path); macOS/Linux via `SWELL_InitiateDragDropOfFileList` (copy-semantics caveat documented — SWELL does not expose a drop-effect query). Driven by the `drag_out` pure module. - `actions` — registers the capture/placement/slot action family, the Design View action family (toggle active mode, activate Arrange/Design, tag/untag selected tracks, show-both), the multi-bank action family (create/rename/reorder/delete bank, evacuate, activate, move/copy/remove selected samples), and the Phase R prune action (`BANK_PRUNE_FOLDER` — dry-run-first, confirm-with-manifest, then `pruneDeletePlan`-guarded deletion; **the ONLY file-deletion authority in the system**); routes each to the modules above via the `command_id`/`gaccel`/`hookcommand` contract. 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 writes no ext state and opens no undo point (file deletion is not REAPER-undoable). ## 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): 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 `'-'`. ## 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.