26 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
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. A post-S-VIEW DAW-fix pass has landed (all 52 suite tests green): envelope nodes fully editable in both modes (every Gate stage A/H/D/S/R + Trigger zero-fade-out node, param-domain schematic scaling, 8 px min node separation, all nodes clamped in-canvas); gap-free per-column waveform render (columnMinMax homed in peaks, waveformColumnCount in component_geometry, shared via drawWaveform); param_slider Knob primitive (7→5 o'clock arc, needle, vertical-drag); zone-bleed fix 3a (reconcileSingleCaptureZones in sample_map). The voice-system redesign is also landed: sampler_core gains user-parameterized voice count (1–32, default 16), VoiceMode Poly/Mono (last-note held-note stack, MonoTrigger retrigger/legato toggle), an isolated PreviewCard (dedicated preview voice outside the MIDI pool — never steals from/into it; unity-Preserve zero-latency bypass scoped to it), and two-tier panic (CC 123 = release, CC 120 = immediate hard-stop incl. Trigger one-shots); processor sums the preview card alongside the engine + drain, retireIdleDrain() retires fully-idle drain snapshots, and voice-param edits rebuild from the already-decoded PCM (no bank re-read/WAV re-decode) via the drain-slot swap; ComponentState envelope bumped v6→v7 (voiceCount/voiceMode/monoTrigger bytes; pre-v7 blobs lift to 16/Poly/Retrigger). FB1 Sample-view recomposition (r11) has also landed (suite 55/55 green): all linear sliders replaced by radial knobs in a fenced knob deck (groups: AMP ENVELOPE / PITCH / PITCH ENV / VOICE / MASTER); mode toggles are compact in the caption row, not full-width; the hero waveform runs full-width (elastic band, 840×620 default preserved); the inline velocity-curve box is replaced by a 28×28 curve preview button → centered popup with right-click node delete; voice-band controls (count / Poly-Mono / Retrig-Legato) are placed in the VOICE deck group; a post-mixer per-sample-ramped master gain (−∞…+24 dB, no zipper) is placed in the MASTER deck group, persisted as masterGainLinear — ComponentState envelope bumped v7→v8 (pre-v8 blobs lift to unity gain). Three new pure src/vst/ modules landed: knob_deck (group-box + caption-row + knob-cell geometry, deterministic wrap, hit-test), curve_popup (sheet/close/box geometry + outside-sheet dismissal test), master_gain (dB↔linear taper math, −∞…+24 dB). FB2 Zone-panel parity (r11, 2026-07-28) has also landed (suite 55/55 green): the Zone param panel now uses the same knob deck + curve-preview-button/popup grammar as the Sample face — one control grammar across both surfaces of the one per-zone storage site; Zone-authoring affordances (+Add Zone / Delete, the piano-key strip, Low/High/Root legend) are preserved; VOICE and MASTER groups remain Sample-only (per-instance). param_slider's linear slider rows are retired on the Zone panel (the FA4 Knob primitive is now the only live consumer of that half of param_slider). This completes the r11 editor recomposition (Wave B / Phase S editor redesign). A GA post-launch DAW-fix pass has also landed (suite 55/55 green): pitch_shift rewritten from dual-tap OLA (anti-phase cancellation → spectral garbage on repitched notes) to correlation-aligned SOLA splices with a ratio-scaled raised-cosine fade (clean pitch shift past +24 st); Voice::start applies a bounded blend (out*(1-w) + ref*w, w decaying from 1.0) at takeover boundaries — mono retrig/fallback, poly at-cap steal, and preview re-trigger — superseding the earlier (1-amp) envelope-complement gate that zeroed the compensation on Trigger/zero-attack restarts; the output bus is now permanently stereo (ChannelMode is decode-only; the dynamic mono↔stereo bus renegotiation is deleted) with channel mode auto-defaulting from the loaded capture via new ComponentState v9 (channelModeExplicit flag) + a pure channelModeFor helper; SetCapture moved to drag-arm in bank_panel so a first straight-out drag arms correctly; and the Design/Arrange mode-toggle action now calls bankPanelInvalidate() so the panel footer reflects the new mode without requiring a button click.
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.sdkThe VST3 target (
reasampler_vst) is gated onEXISTS .../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_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 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.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— pureI_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 aBankIndex. 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 oneREASAMPLER_CHANNEL_IS_BETAbit; 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 arsprov1fingerprint 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 thebank_panelfooter 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. TheInstrumentDropgesture 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 theReaSampler: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-drop payload builder: constructs a Steinberg-format.vstpresetimage (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies viaTrackFX_SetPreset; owns theinfoNamesFxHotspotprefix classifier forGetThingFromPointtokens. All-or-nothing contract — caller rolls back viaTrackFX_Deleteon any failure.assignment_request— pure ingest-assign wire: typed request record carrying the drop payload from theingestshell through to the VST3 bridge.
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 with three-zone layout: top toolbar (Capture → Maintenance → Placement viaaction_bar, short labels, More (⋯) overflow menu viaoverflow_menu), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer ([Arrange|Design]toggle, Tail button, Prune viafooter_bar). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection viaaccent/tertiarypurple border. Draws through the L1 kit by palette role; OS drag-out viadrag_out+drag_out_win.persist— project ext state (SetProjExtState/GetProjExtState, namespace"reasampler") ↔BankBookJSON,ViewModeModelJSON,TailSettingJSON,OwnedManifestJSON, and writing-version stamp. Aprojectconfighook triggers a deferred session reload on undo/redo. Hosts the prune dry-run and full-set orphan queries; suppliesreferencedPaths()+owned().paths()to theprune_reconcilepure 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 orB_MUTE/I_SOLO.track_guid— sharedMediaTrack*→ canonical GUID-string formatter; single source of truth for membership keys.provenance_shell— FX-chain identity queries viaTrackFX_*/TakeFX_*APIs; feeds the pureprovenancefingerprint builder. StampsSample.provenanceon capture; ambiguous/mixed cases record nothing conservatively.drag_out_win— OS drag-out shell: Windows OLEDoDragDrop/CF_HDROP, copy-only (DROPEFFECT_MOVEnot offered); macOS/Linux viaSWELL_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 theassignment_requestext-state wire. ingest NEVER inserts a timeline item.instrument_drop_win— FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient.vstpreset+TrackFX_SetPreset(the formerTrackFX_SetNamedConfigParm"vst_chunk" write was silently unappliable for VST3). ExposesloadInstrumentOntoTrack(inner half, no own undo block) andperformInstrumentDrop(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-fonttext(), full interaction-state model, double-buffer preserved. Consumestheme+component_geometry.actions— registers the capture/placement/slot, Design View, multi-bank, and prune action families; routes each 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 (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, user-parameterized voice count (1–32, default 16),VoiceModePoly/Mono (last-note held-note stack,MonoTriggerRetrigger/Legato toggle), isolatedPreviewCard(dedicated preview voice outside the MIDI pool — never steals from/into it; unity-Preserve zero-latency bypass scoped to it), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots); per-zoneZonePlayParams(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 insrc/(Daniel's standing ruling, load-bearing). JSON round-trip.pitch_shift— hand-rolled correlation-aligned SOLA (splice-overlap-add) pitch shifter for the Preserve playback mode: one active read tap chases the write head at the shift ratio; each splice jump is refined by a cross-correlation search so the new read point is waveform-aligned, then old and new taps are crossfaded (raised-cosine, amplitude-complementary). Replaces the prior dual-tap OLA whose fixed half-window tap offset caused anti-phase cancellation on many source frequencies. GA2: ring buffer primed with the actual upcoming source at note-on (was zero-filled) → gap-free frame-0 onset, ~25 ms Preserve onset latency eliminated (Preserve now speaks on frame 0, matching Varispeed), and real-content-bounded tail (last-window tail-truncation gone). No third-party dependencies; RT-discipline: no allocation inprocess().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 theGetProjExtStateint return against its filled buffer.editor_geometry— VST3 editor layout: defines the sharedRecttype +contains()hit-test; providesEditorLayoutandlayoutEditor(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 overcapture_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, threadingstartFramecorrectly through pack and unpack directions.velocity_curve— pure velocity→amp transfer curve:VelocityCurveevaluated by a Fritsch–Carlson 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 fixedvelocity/127path — a deliberate non-back-compat behavior change (Daniel-approved).embed_strip— compact single-row control layout for embed mode in the track FX chain.knob_deck— pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap,DeckLayout/DeckHit. Mirror ofaction_bar/param_slider; no LICE or REAPER types.curve_popup— pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror ofoverflow_menu; no LICE or REAPER types.master_gain— pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift.reasampler_uid.h— SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs,REASAMPLER_PROC_UID_*/REASAMPLER_PROC_UID_BETA_*) and theREASAMPLER_ACTIVE_UID_*channel-selector macros. Split out ofreasampler_vst.hso the pure extension side (instrument_drop) can derive the.vstpresetclass-ID hex string without pulling in the VST3 SDK. Bothreasampler_vst.h(runtimeFUID) andinstrument_drop(preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge.
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— VST3SingleComponentEffectshell: declares event-input bus + permanently stereo output (GA fix: dynamic mono↔stereo bus renegotiation deleted;ChannelModeis now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-threadreloadFromBank+ atomic pointer swap soprocess()does no allocation, no file I/O, no bridge calls. Sums thePreviewCardalongside the engine + drain inprocess();retireIdleDrain()retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (setVoiceCount/setVoiceMode/setMonoTrigger) rebuild the engine from the already-decoded keymap via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. FB1: applies the post-mixermasterGainLinear(fromComponentStatev8) as a per-sample ramp over the summed output — no zipper noise. GA v9:channelModeExplicit_flag persisted;channelModeFor()auto-defaults the mode from the loaded capture's channel count when the flag is not set.reasampler_editor— VST3IPlugViewLICE 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— implementsIReaperUIEmbedInterfaceso the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout/hit-test toembed_strip.vst_entry— VST3 entry point:GetPluginFactoryexport, class registration, channel-forked class UIDs.
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'-'.
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 and are cited by section from CONTEXT.md and PLAN.md; 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.
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.