Files
reasampler/CLAUDE.md
T
daniel b09f375490 docs: fix swell_resgen.php path and repair citations broken by the CONTEXT split
mac_resgen.php does not exist. 34 CONTEXT.md citations retargeted to
CONTEXT-ARCHIVE.md; 6 self-inverted refs in the archive corrected.
2026-07-27 21:32:22 -04:00

18 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

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, src/vst/, second CMake target reasampler_vst, gated on the vendored vendor/vst3sdk submodule slice). The pure-testable-core / REAPER-facing-shell discipline is preserved throughout. CONTEXT.md is the authoritative spec — settled decisions, invariants, guardrails, and not-yet-built specs; it is large, so locate the relevant phase section by grepping its headings and read only that section with an offset rather than reading it whole. Build detail for landed phases lives in CONTEXT-ARCHIVE.md. 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 three 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

  • 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

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).

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_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 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

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; 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.
  • view_tree — pure I_FOLDERDEPTH→FolderTree helper for the Design View shell.
  • bank_grid — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
  • tab_strip — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
  • mode_switch — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
  • bank_book — multi-bank registry: an ordered set of banks each wrapping a BankIndex. Pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model. Owns create/rename/reorder/delete of named banks, active-bank id, index-only move/copy/remove of a sample between banks, and JSON round-trip.
  • owned_manifest — the set of project-relative files the capture path itself created, persisted under the "owned_files" ext-state key, so the prune path can distinguish the bank system's own orphans from hand-dropped files.
  • app_version — REAPER-free version/channel identity: CMake-sourced semver constant, ext-state stamp value, and the full set of channel-derived identity accessors. All channel strings derive from one REASAMPLER_CHANNEL_IS_BETA bit; no scattered #ifdefs in the shells.
  • wav_trim — 32-bit-float WAV parse + header-aware truncate plan for the realtime tail's PCM decay-scan trim.
  • provenance — capture-recipe fingerprint: build/encode/compare a rsprov1 fingerprint of scope, range, tail, rate/channels, track GUIDs, and FX-chain identity. A thin reproducibility fingerprint — NOT a serialized chain to restore.
  • prune_reconcile — pure prune core: pruneOrphans(present, referenced, owned) computes (owned ∩ present) referenced; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists.
  • prune_button — pure layout/hit-test for the bank_panel footer Prune button.
  • batch_capture — pure batch-capture planner: maps source ranges to capture units and aggregates results.
  • action_buttons — pure action-button strip layout/hit-test.
  • drag_out — pure OS drag-out module: gesture-boundary decision and path-list assembly. The InstrumentDrop gesture signals that the shell should execute an instrument-drop rather than a file-copy drag.
  • theme — pure palette module: role→color mapping, REAPER-grey neutral ladder + three-accent pastel system, WCAG contrast-floor helpers.
  • component_geometry — pure button/slider/list-row geometry + hover hit-test helpers.
  • action_bar — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
  • footer_bar — pure footer layout/hit-test: [Arrange|Design] mode-toggle geometry, Tail button, and Prune placement.
  • overflow_menu — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
  • mode_enable — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
  • tooltip — pure tooltip placement + prefix-strip: strips the ReaSampler: display prefix from the registered action phrase; width clamped to the client rect.
  • card_drag — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace.
  • card_meta — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped.
  • instrument_drop — pure FX-button drop blob builder: constructs the base64-encoded vst_chunk blob needed to inject a pre-configured reasampler_9000 instrument. All-or-nothing contract — caller rolls back via TrackFX_Delete on any failure.
  • assignment_request — pure ingest-assign wire: typed request record carrying the drop payload from the ingest shell through to the VST3 bridge.

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 with three-zone layout: top toolbar (Capture → Maintenance → Placement via action_bar, short labels, More (⋯) overflow menu via overflow_menu), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer ([Arrange|Design] toggle, Tail button, Prune via footer_bar). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via accent/tertiary purple border. Draws through the L1 kit by palette role; OS drag-out via drag_out + drag_out_win.
  • persist — project ext state (SetProjExtState/GetProjExtState, namespace "reasampler") ↔ BankBook JSON, ViewModeModel JSON, TailSetting JSON, OwnedManifest JSON, and writing-version stamp. A projectconfig hook triggers a deferred session reload on undo/redo. Hosts the prune dry-run and full-set orphan queries; supplies referencedPaths() + owned().paths() to the prune_reconcile pure core.
  • view — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (B_SHOWINTCP/B_SHOWINMIXER/B_MAINSEND/I_FXEN + per-FX offline), 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.
  • provenance_shell — FX-chain identity queries via TrackFX_*/TakeFX_* APIs; feeds the pure provenance fingerprint builder. Stamps Sample.provenance on capture; ambiguous/mixed cases record nothing conservatively.
  • drag_out_win — OS drag-out shell: Windows OLE DoDragDrop/CF_HDROP, copy-only (DROPEFFECT_MOVE not offered); macOS/Linux via SWELL_InitiateDragDropOfFileList.
  • ingest — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the assignment_request ext-state wire. ingest NEVER inserts a timeline item.
  • instrument_drop_win — FX-button drop shell: resolves a screen point to a track + TCP FX-button hotspot, then adds a ReaSampler 9000 instance and injects state via TrackFX_SetNamedConfigParm "vst_chunk". Exposes loadInstrumentOntoTrack (inner half, no own undo block) and performInstrumentDrop (wraps in its own undo block). Never captures, never writes the bank, never inserts a timeline item.
  • draw_kit — shared LICE draw shell: fillSurface, drawButton/drawSlider/drawListRow/drawWaveform, cached-font text(), full interaction-state model, double-buffer preserved. Consumes theme + component_geometry.
  • actions — registers the capture/placement/slot, Design View, multi-bank, and prune action families; routes each 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 (BANK_PRUNE_FOLDER) is the ONLY file-deletion authority in the system; it opens no undo point (file deletion is not REAPER-undoable).

VST3 instrument (src/vst/) — pure core:

  • sampler_core — polyphonic voice engine with bounded stealing, per-zone ZonePlayParams (Gate/Trigger, AHDSR, pitch engine Varispeed/Preserve, AD pitch mod envelope), repitch/interpolation with loop-point-aware sustain.
  • sample_map — zone payload: zones keyed by note range. Wall-clock times stored as rate-free SECONDS, resolved against the live project rate — NO hardcoded sample rates in src/ (Daniel's standing ruling, load-bearing). JSON round-trip.
  • pitch_shift — hand-rolled overlap-add pitch shifter for the Preserve playback mode; no third-party dependencies.
  • bank_sync — generation change-detection + assignment-request consume: owns the yes/no decision logic so the rules are provable without a host. The processor shell owns cadence and side effects.
  • bridge_marshal — pure marshalling helper for the REAPER VST-host bridge read: interprets the GetProjExtState int return against its filled buffer.
  • editor_geometry — VST3 editor layout: defines the shared Rect type + contains() hit-test; provides EditorLayout and layoutEditor(w,h).
  • keyboard_strip — piano-keyboard strip: MIDI-note→key rect mapping, black/white key layout, hit-test, zone highlight overlay geometry.
  • waveform_view — waveform/marker geometry: maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap.
  • capture_browser — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing.
  • browser_scroll — scroll + type-to-filter layered over capture_browser: vertical scroll offset, scrollbar thumb, thumb-drag mapping, and name-substring search.
  • note_entry — parses a raw string into a clamped MIDI note [0,127]; accepts plain decimal integers or note names (C4==60, DAW convention).
  • param_slider — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
  • trigger_seam — pure Trigger frames↔fraction converter: owns the shared formula for converting between engine source-frame fade counts and the overlay's fractional representation, threading startFrame correctly through pack and unpack directions.
  • velocity_curve — pure velocity→amp transfer curve: VelocityCurve evaluated by a FritschCarlson monotone cubic Hermite spline (no overshoot outside [0,1]). eval(velocity) called once per note-on. flat() default (y=1, every velocity→unity) replaces the prior fixed velocity/127 path — a deliberate non-back-compat behavior change (Daniel-approved).
  • embed_strip — compact single-row control layout for embed mode in the track FX chain.

VST3 instrument (src/vst/) — shells:

  • reaper_bridge — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. Never writes to the extension's bank — this is a load-bearing invariant; no mutation path exists in this module.
  • reasampler_processor — VST3 SingleComponentEffect shell: declares event-input bus + stereo output, marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread reloadFromBank + atomic pointer swap so process() does no allocation, no file I/O, no bridge calls.
  • reasampler_editor — VST3 IPlugView LICE editor shell: hosts a LICE-drawn child window; default face is the capture browser, then single-capture setup, with opt-in zones panel. Drop-onto-editor ingest is NOT shipped (deferred).
  • reasampler_embed — implements IReaperUIEmbedInterface so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout/hit-test to embed_strip.
  • vst_entry — VST3 entry point: GetPluginFactory export, class registration, channel-forked class UIDs.

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.