Per-zone play params on SampleData; hand-rolled pure pitch_shift OLA for Preserve (WDL drags windows.h); zone-payload v3 tail; RT-safe pre-warmed shifters + Preserve voice cap.
25 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
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), M11 in full (batch capture, action-button strip + keybinding labels, conform-on-insert verified-extant, native OS drag-out), Phase L L1 (shared LICE drawing kit: theme/palette module, component_geometry geometry helpers, draw_kit shell, GDI DrawText retired in bank_panel), Phase L L2 (dock-panel layout redesign: action_bar pure task-grouped layout module, full M11-aware button inventory placed by task cluster, bank_panel redesigned through the L1 kit), Phase L L4 (dock-panel button layout enhancement: three-zone layout — top capture/placement/maintenance toolbar, bottom Design-View tagging/switching toolbar, footer narrow-toggle + Tail-button + Prune; footer_bar pure module + footer_bar_tests; ActionCluster::Tagging/Switching in action_bar), Phase L L5 (dock-panel button refinements: top-bar overflow menu for Batch Items/Batch Razor/Capture RT via overflow_menu pure module; custom LICE-kit hover-delay tooltips via tooltip pure module; four opposite-mode Item/Track tag buttons + Show Both on the bottom toolbar; Toggle + Activate-Arrange/Design buttons removed; grouping spacing widened — mode_enable pure opposite-mode enablement predicate), Phase L L6 (toolbar polish: single-row short-label button faces — keybinding micro sub-row removed from ActionBarSlot, keybinding now shown in hover tooltip as "phrase — binding"; Cancel RT moved from visible bar into the overflow menu — More (⋯) now lists Batch Items / Batch Razor / Capture RT / Cancel RT; visible top-bar cluster order Capture → Maintenance → Placement; toolbar height 40→28 px), and Phase L L7 (capture ordering, card metadata, and selection styling: per-bank SlotMap (id→slot) in bank_book — gap-preserving persisted display order, reorderSample/replaceSample/orderedSampleIds/reconcileSlots mutators; captureTimeSigNum/captureTimeSigDenom stamped on Sample at capture via TimeMap_GetTimeSigAtTime; card_drag pure module — gesture precedence + SWELL stock cursor cues; card_meta pure module — bars.beats.subdivisions + s.ms formatters; bank_panel sparse-grid rendering with gap cells, drop dispatch + cursor cues, metadata overlay, tertiary-border selection) 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 headersvendor/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. |
action_bar_tests |
executable | Pure unit tests for action_bar — no REAPER, no DAW. |
footer_bar_tests |
executable | Pure unit tests for footer_bar — no REAPER, no DAW. |
overflow_menu_tests |
executable | Pure unit tests for overflow_menu — no REAPER, no DAW. |
mode_enable_tests |
executable | Pure unit tests for mode_enable — no REAPER, no DAW. |
tooltip_tests |
executable | Pure unit tests for tooltip — no REAPER, no DAW. |
card_drag_tests |
executable | Pure unit tests for card_drag — no REAPER, no DAW. |
card_meta_tests |
executable | Pure unit tests for card_meta — no REAPER, no DAW. |
pitch_shift_tests |
executable | Pure unit tests for pitch_shift (S16 Preserve engine) — 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—Samplemetadata struct +BankIndex(add/remove/query/tier/dedup-by-hash + JSON round-trip). Test it hard — it is the heart.SamplecarriescaptureTimeSigNum/captureTimeSigDenom(L7: stamped at capture viaTimeMap_GetTimeSigAtTime, refreshed on re-capture).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 ofbank_modelfor the Design View phase.view_tree— pureI_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 ofbank_grid.bank_book— multi-bank registry (Phase B): an ordered set of banks (pool seeded as bank-zero + named banks), each wrapping aBankIndex. 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,hashReferencedElsewherecross-bank reference query, JSON round-trip + legacy-bank_index→pool migration. WrapsBankIndex(bank_model untouched; nobankIdonSample). L7 adds: per-Bankgap-preservingSlotMap(id→slot) for deterministic display order — persisted inside the existing"banks"blob;reorderSample(insert-before-shift; same-slot = no-op),replaceSample(occupant index-removal via standard remove path + pool-guard inheritance; no-op on reject),orderedSampleIds,reconcileSlots(pre-L7 migration seeds dense insertion order on load).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 frombank_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 oneREASAMPLER_CHANNEL_IS_BETAbit threaded in viaconfigure_file→version_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 onpeaksfor theAudioSamplefloat alias.provenance— capture-recipe fingerprint (M10): build/encode/compare arsprov1length-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. DrivesBankIndex::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);buildPruneReporttallies count/bytes/display-capped file list;pruneDeletePlanproduces 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 tobank_book.prune_button— Phase R pure layout/hit-test for thebank_panelfooter Prune button:computePruneButton(right-anchored, suppressed gracefully when footer is too narrow) +hitTestPruneButton. Mirror ofmode_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 sharedcaptureAndIndexOnehelper, 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,ActionButtonRectstruct. Mirror ofmode_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 (DS-2 revised — REAPER-grey neutral ladder:bg/base #2b2b2b/bg/panel #333333/bg/cell #3a3a3a/line/hairline #4a4a4a/text/primary #dcdcdc/text/dim #a8a8a8; three-accent pastel system:accent/primarypastel lime#B0E098= live/active/selected,accent/secondarypastel teal#84D6D0+accent/tertiarypastel purple#C2AAE8= categorical distinctions,accent/hotlighter lime = hover/drag feedback);Roleenum carries the three accent roles;roleColor/roleColorStateupdated (Active/Dragging/Focus → primary accent);spectralColoris a pastel three-stop sweep anchored on the three accents (lime → teal → purple); WCAG contrast-floor helpers; full interaction-state color model. 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 ofmode_switch/bank_grid.action_bar— Phase L pure task-grouped action-bar layout/hit-test (L2 + L4): clusters (Capture / Placement / Maintenance / Tagging / Switching), per-button label rects, whole-trailing-button overflow, point→index hit-test.ActionCluster::TaggingandActionCluster::Switchingadded in L4 for the bottom Design-View toolbar. No LICE or REAPER types. Mirror ofmode_switch/bank_grid/action_buttons.footer_bar— Phase L pure footer layout/hit-test (L4): narrow[Arrange|Design]mode-toggle geometry + per-mode count label, Tail button, and Prune placement in the panel footer strip. No LICE or REAPER types. Mirror ofmode_switch/prune_button.overflow_menu— Phase L pure overflow-menu-button geometry/reserve/hit-test (L5): right-anchored More (⋯) button rect computation and hit-test for the top-toolbar overflow. No LICE or REAPER types. Mirror ofprune_button.mode_enable— Phase L pure opposite-mode enablement predicate (L5): given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons on the bottom toolbar. No LICE or REAPER types.tooltip— Phase L pure tooltip placement + prefix-strip (L5): custom LICE-kit hover-delay tooltip geometry and text derivation — strips theReaSampler:display prefix from the registered action phrase; tooltip box width clamped to the client rect. No LICE or REAPER types.card_drag— Phase L pure drag-gesture precedence + slot hit-test (L7): leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace. Returns the resolved gesture so the shell can map it to a SWELL stock cursor cue. SparsecomputeSlotRects/hitTestSlot. No LICE or REAPER types. Mirror ofdrag_out/prune_button.card_meta— Phase L pure card-metadata formatters (L7): bars.beats.subdivisions fromlengthSeconds+captureTempo+ capture-time-signature stamp; seconds.milliseconds rounded. Both blank when the sample is unstamped. No LICE or REAPER types.
REAPER-facing shells:
capture—ICaptureBackendinterface;OfflineRenderBackend(deterministic default) andRealtimeRecordBackend. Input:CaptureRequest. Output: finished file + populatedSamplehanded tobank_model.insert— placement viaInsertMedia; 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; three-zone layout (L4/L5/L6): top toolbar = visible bar in cluster order Capture (Capture Item, Capture Track) → Maintenance (Re-capture) → Placement (Insert, Insert Conform) viaaction_bar, each button showing a single-row short label, firing viaNamedCommandLookup+Main_OnCommand; a right-anchored More (⋯) overflow menu button (overflow_menu) opens aTrackPopupMenulisting Batch Items / Batch Razor / Capture RT / Cancel RT; hover tooltips (tooltip) show the full action name with theReaSampler:prefix stripped plus the live keybinding ("phrase — binding" when bound, bare phrase when unbound), sourced from the registered action phrase +kbd_getTextFromCmd; toolbar height 28 px; bottom toolbar = four opposite-mode tag buttons (Item: Arrange / Item: Design / Track: Arrange / Track: Design — live only for the opposite of the active mode,Disabledotherwise viamode_enable) + set-apart Show Both (VIEW_SHOW_BOTH); Toggle and Activate-Arrange/Activate-Design buttons removed (actions stay registered); groupingclusterGap24 /buttonGap4; footer = narrow[Arrange|Design]toggle · per-mode count · Tail button (cycles tail on click, proper kit button states) · set-apartwarnPrune (rightmost) viafooter_bar; entire panel draws through the L1 kit by palette role with hover on interactive elements, singleKitColor→LICE_pixelboundary viadraw_kit; OS drag-out hook (M11) initiating an OS-level copy drag when the pointer leaves the panel client rect, viadrag_out+drag_out_win. L7 adds: grid renders in sparse slot order (decorative empty-gap cells for unoccupied slots; every cell↔sample consumer remapped to id-based occupied-ordinal space — selection, keyboard nav with gap-skip arrows, audition, multi-select, delete/re-capture resolution, drags); drop dispatch (reorder / Alt-replace / move-copy) with one-Ctrl-Z undo, per-slot drop highlight (accent/hot, doubled outline for replace), SWELL stock cursor cues viaSetCursorper thecard_dragresolved gesture; metadata overlay (bars.beats bottom-left Micro, s.ms bottom-right ValueMono,TextDim, decorative); selection = normal cell +accent/tertiarypurple border, inversion removed, focus ring distinct.persist— project ext state (SetProjExtState/GetProjExtState, namespace"reasampler") ↔BankBookJSON ("banks"key — L7: per-BankSlotMaprides inside this blob; a pre-L7 blob with no slot data migrates viareconcileSlots()to dense insertion order on load) +ViewModeModelJSON ("view_state"key) +TailSettingJSON ("tail_setting"key) +OwnedManifestJSON ("owned_files"key) + writing-version stamp ("version"key, written viastampVersion()on everysaveToActiveProject()); project-relative path resolution. Aprojectconfighook (BeginLoadProjectState(isUndo)) triggers a deferred session reload on undo/redo so Ctrl-Z/redo visibly restores book/view/tail/manifest in-session. HostsReaSamplerSession::pruneDryRun()(read-only orphan enumeration via M4 project-relative resolution) andpruneOrphanSet()(full-set query for the R3 delete path); suppliesreferencedPaths()+owned().paths()to theprune_reconcilepure core.view— Design View shell: reads the folder tree viaview_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 orB_MUTE/I_SOLO.track_guid— sharedMediaTrack*→ 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 viaTrackFX_*/TakeFX_*APIs; collects source-item paths and parent-detection inputs to feed the pureprovenancefingerprint builder. StampsSample.provenanceon 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 OLEDoDragDrop/CF_HDROP, copy-only structurally (DROPEFFECT_MOVEnot offered, no source-deletion path); macOS/Linux viaSWELL_InitiateDragDropOfFileList(copy-semantics caveat documented — SWELL does not expose a drop-effect query). Driven by thedrag_outpure module.draw_kit— Phase L shared LICE draw shell (L1):fillSurface(micro-gradient + inner highlight/shadow),drawButton/drawSlider/drawListRow/drawWaveform, cached-fonttext()over fourLICE_CachedFonts (kit-owned lifecycle), full interaction-state model, double-buffer preserved. Consumestheme+component_geometry. First consumer:bank_panel(GDIDrawTextpath 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, thenpruneDeletePlan-guarded deletion; the ONLY file-deletion authority in the system); routes each to the modules above via thecommand_id/gaccel/hookcommandcontract. 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 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):
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'-'.
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.