Files
reasampler/src/shell/instrument/CLAUDE.md
T
daniel 2e09776342 fix: stroke arcs and splines analytically — opaque core, angle-independent weight
LICE_Arc never reaches opacity and ThickFLine's width is minor-axis. One
distance-to-polyline coverage mask, blended once, replaces both.
2026-08-01 13:18:21 -04:00

14 KiB

src/shell/instrument — ReaSampler 9000 VST3 shells

Scope

The REAPER/VST3-facing shells for the ReaSampler 9000 instrument: the read-only bank bridge, the processor, the editor, the embed strip, and the VST3 entry point — plus two small identity/helper headers this directory owns outright (reasampler_vst.h, editor_internal.h).

The pure engine/geometry core this shell wraps (sampler_core, pitch_shift, sample_map, component_state_io, play_params.h, editor_geometry, sample_bands, sample_chrome, keyboard_strip, waveform_view, capture_browser, browser_scroll, param_slider, trigger_seam, velocity_curve, embed_strip, knob_deck, deck_groups, deck_values, curve_popup, spline_edit, master_gain, reasampler_uid.h) lives in core/instrument/* and core/wire and is documented there — this directory consumes it but does not own it.

Invariants

The build shape (D-A, settled 2026-07-26 — bare Steinberg VST3 SDK + LICE editor). Bare Steinberg VST3 SDK, no JUCE, with the editor drawn in the same LICE/SWELL stack bank_panel already uses. SingleComponentEffect (the SDK's combined processor+controller base) plus the SDK's factory macros is the audio-processing scaffolding. Drawing the editor in a VST3 IPlugView that hosts a LICE surface reuses the bank_panel docking muscle, keeps the look house-consistent, and avoids JUCE's AGPL-or-pay license posture. The IPlugView↔LICE bridge (window lifecycle, sizing, event routing from the host into the draw/hit-test loop) is the same class of work as docking bank_panel, not a new competence.

Embedded TCP/MCP UI (D-D) — reasampler_embed. A REAPER-hosted VST3 can draw its own UI inline in the track/mixer control panel via reaper_plugin_fx_embed.h (the plugin implements IReaperUIEmbedInterface — the same Cockos surface REAPER's own embedded FX use). Because this uses the same LICE-class drawing as the main editor path, it composes naturally with the bare-SDK-plus-LICE build. Must-verify: the IReaperUIEmbedInterface contract and embed message/lifecycle against vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h.

Channel mode (D-E) — current reality. An earlier design (D-E, settled 2026-07-26) specified a per-instance mono/stereo toggle negotiating the REAPER audio bus via setBusArrangements/getBusArrangement, with mono-source+stereo-mode → dual-mono and stereo-source+mono-mode → downmix as the cross-mode policy. This was superseded by the GA post-launch DAW-fix pass: the output bus is now permanently stereo, ChannelMode is decode-only (the dynamic mono↔stereo bus renegotiation from the earlier design was deleted), and channel mode auto-defaults from the loaded capture via ComponentState v9's channelModeExplicit flag + the pure channelModeFor helper. Root CLAUDE.md is authoritative for this behavior — do not reintroduce per-instance bus renegotiation.

VST3 channel identity — the UID pair + the pairing surface (S18). A beta-built VST pairs with the beta extension only, a stable VST with stable only, both installable side-by-side in one REAPER — one channel per binary; all channel identity derives from the ONE REASAMPLER_CHANNEL_IS_BETA bit via the pure app_version module (no scattered #ifdefs in the VST shell, except the one described below).

  • The UID-pair invariant is a permanent commitment. The VST3 class UID is the plugin's identity — a saved REAPER project records it and rebinds a saved instance by it. BOTH channel UIDs (reasampler_uid.h's stable + beta pairs) are frozen forever once shipped; the channel bit selects which one is compiled into this binary (one DEF_CLASS2, one class per binary — never both classes in one binary). The UID selection is the ONLY channel #ifdef in the VST shell, because an INLINE_UID needs literal brace-init tokens and cannot route through app_version's runtime string accessors.
  • Binary + display identity are channel-derived, sourced from app_version's VST-name accessors — never a literal in reasampler_vst.h/vst_entry.cpp.
  • The complete pairing surface is structural, not per-key. Plugin identity (UID + filename + display) is channel-forked, and all wire keys live under the channel-derived ext-state namespace — the two together make pairing complete: no per-key or per-seam isolation work is ever needed for a new wire key.
  • Verify all identity/factory wiring against the vendored Steinberg SDK (DEF_CLASS2 / INLINE_UID / FUID from pluginfactory.h + funknown.h).

The three commit tiers (Θ-W3). An edit reaches the audio by exactly one of three routes, and which route a control takes is decided once, by the pure isLiveDeckParam / liveCommitFor pair (core/instrument/ui/deck_groups) that the editor's dragCommitsLive only maps onto — see core/instrument/CLAUDE.md's "Live parameter delivery" for the rule and its rationale.

  1. Full reloadreloadInstrument: bridge read, WAV re-decode, fresh engine, snapshot swap.
  2. Engine rebuildrebuildVoiceEngine: same drain-slot swap around the already-decoded SampleData. Voice count / mode / mono trigger.
  3. LivepublishLiveParams (and masterGain_, the original of the shape): a lock-free publish the audio thread observes at block boundaries. No rebuild, no snapshot, no disk.

The editor's commitLive is the tier-3 peer of commitAndReload; why it still writes the parameter set is recorded at its declaration in reasampler_editor.h, and why liveParams_ is declared ahead of the instrument slots at that member in reasampler_processor.h.

Non-goals / guardrails.

  • The instrument never captures and never inserts into the arrange. Playback is a read-only act over the bank. Any instrument path that captures, places a timeline item, or writes back into the bank is a bug.
  • The instrument never ingests. Capture, import, and drop-ingest are extension acts; the instrument only reads and plays. A drop onto the editor window (if ever shipped) is relayed to the extension as an ingest request — the instrument never writes the bank itself.
  • No cross-platform / multi-format. Windows-only, VST3-only, REAPER-only (D5). Do not add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target.
  • The pure core stays REAPER-free and VST3-free — the voice engine / envelope / repitch module takes no VST3 or REAPER type at its boundary; the shell marshals. Any VST3 or REAPER type leaking into core/instrument is a bug.
  • Verify Steinberg SDK, bridge, embed, and LICE-view surfaces against the vendored headers before use.

Modules

  • 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. pS-usage: gains writeUsageExtState (prefix-guarded — accepts only rsusage_-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant.
  • reasampler_processor (shell/instrument/: reasampler_processor.cpp lifecycle + process(), processor_state.cpp component-state I/O + UI-thread parameter accessors, processor_reload.cpp the off-audio-thread reloadInstrument/publish family — Q-W2v, T4-12 split; process() and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 SingleComponentEffect shell: declares event-input bus + permanently stereo output (GA fix: dynamic mono↔stereo bus renegotiation deleted; ChannelMode is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread reloadInstrument + atomic pointer swap so process() does no allocation, no file I/O, no bridge calls. The instance state is {loaded capture id, one InstrumentParams}, and reloadInstrument resolves + decodes exactly that one capture into the SampleData the engine plays. Self-contained playback (pS): ComponentState v10 adds a SampleRefs table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); reloadInstrument decodes directly from SampleRefs, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. retireIdleDrain() retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (setVoiceCount/setVoiceMode/setMonoTrigger) rebuild the engine from the already-decoded SampleData via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. FB1: applies the post-mixer masterGainLinear (from ComponentState v8) 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. pS: ComponentState bumped v9→v10 (SampleRefs table); pre-v10 blobs lift to empty refs and re-save self-contained. pS-usage: publishes instance usage (held SampleRefs paths) to rsusage_<instanceGuid> at the tail of reloadInstrument (off audio thread) via reaper_bridge::writeUsageExtState; ComponentState bumped v10→v11 (instanceGuid field); pre-v11 blobs mint guid on first publish.
  • reasampler_editor — VST3 IPlugView LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure sample_bands allocator: editor_session (session/bridge state, caches, commit-and-reload), editor_controls (the ONE faceLayout band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure core/instrument/ui/deck_values module this only adapts int ids onto), editor_models (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — editor_paint/editor_input (dispatch + drag router + hover dispatch), _chrome, _waveform, _deck — plus the two band-independent surfaces (_browse for the modal picker, _curve for the velocity-curve popup) and editor_platform (IPlugView/Win32 window plumbing). Shared internals in editor_internal.h, no TU of its own. 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 to embed_strip. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
  • editor_stroke — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure core/ui/stroke_aa and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through strokeArcAA / strokePolylineAA / strokeLineAA. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch shell/panel/draw_kit: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
  • vst_entry — VST3 entry point: GetPluginFactory export, class registration, channel-forked class UIDs.
  • editor_internal.h — INTERNAL shared helpers for the reasampler_editor TU family, included only by the editor's own shell TUs (editor_session / editor_controls / editor_paint_* / editor_input_* / editor_platform), never a public seam: the Rect↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure deck_groups module's, not this file's. The piano-strip and root-key draws live in editor_paint_chrome, their only consumer, not here.
  • reasampler_vst.h — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected Steinberg::FUID, built from the FOREVER-FROZEN macros in core/wire/reasampler_uid.h), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. (Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading src/shell/instrument/reasampler_vst.h directly.)

Gotchas

  • editor_internal.h is include-only — it has no TU of its own and must never become a public seam; only the reasampler_editor band-axis TUs include it.
  • The editor window class carries CS_DBLCLKS, which REPLACES the second button-down of a double-click with WM_?BUTTONDBLCLK. Every surface that counts two downs — Browse's load accelerator, the spline surfaces' right-click delete — survives only because both DBLCLK handlers fall through to the ordinary down handler. Adding a new double-click consumer means preserving that fall-through, not bypassing it.
  • The two VST3 class UIDs (core/wire/reasampler_uid.h, consumed via reasampler_vst.h) are FOREVER-FROZEN — never regenerate an already-shipped UID.
  • The UID selection #ifdef in reasampler_vst.h is the one deliberate exception to "channel identity derives from app_version accessors, no scattered #ifdefs" — INLINE_UID needs literal brace-init tokens, so it can't route through a runtime string accessor.