Files
reasampler/CLAUDE.md
T

17 KiB
Raw Blame History

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. M0M8 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 (B1B5, B-cap), Phase D1/D2 Design View (D1D5, D2-W1W3-B), Phase V versioning/beta-channel (V1/V3, V4), M10 provenance + re-capture from source, Phase R Reclaim (R1R3: prune-reconcile core, dry-run shell, guarded deletion + action + panel button), M11 in full (batch capture, action-button strip + keybinding labels, conform-on-insert verified-extant, native OS drag-out), and Phase L wave 1 L1 (shared LICE drawing kit: theme/palette module, component_geometry geometry helpers, draw_kit shell, GDI DrawText retired in bank_panel) 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-sdksdk/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.
theme_tests executable Pure unit tests for theme — no REAPER, no DAW.
component_geometry_tests executable Pure unit tests for component_geometry — 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_fileversion_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_modelSample 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_fileversion_generated.h; no scattered #ifdefs 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.
  • theme — Phase L pure palette module (L1): role→color mapping via one constants block (Direction B Neon Console + Direction C spectral, DS-2); WCAG contrast-floor helpers; interaction-state color model; spectral ramp. No LICE or REAPER types.
  • component_geometry — Phase L pure component geometry/hit-test helpers (L1): button/slider/list-row geometry + hover hit-test. No LICE or REAPER types. Mirror of mode_switch/bank_grid.

REAPER-facing shells:

  • captureICaptureBackend 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.
  • draw_kit — Phase L shared LICE draw shell (L1): fillSurface (micro-gradient + inner highlight/shadow), drawButton/drawSlider/drawListRow/drawWaveform, cached-font text() over four LICE_CachedFonts (kit-owned lifecycle), full interaction-state model, double-buffer preserved. Consumes theme + component_geometry. First consumer: bank_panel (GDI DrawText path retired in L1).
  • 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.