LICE_Arc never reaches opacity and ThickFLine's width is minor-axis. One distance-to-polyline coverage mask, blended once, replaces both.
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 (oneDEF_CLASS2, one class per binary — never both classes in one binary). The UID selection is the ONLY channel#ifdefin the VST shell, because anINLINE_UIDneeds literal brace-init tokens and cannot route throughapp_version's runtime string accessors. - Binary + display identity are channel-derived, sourced from
app_version's VST-name accessors — never a literal inreasampler_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/FUIDfrompluginfactory.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.
- Full reload —
reloadInstrument: bridge read, WAV re-decode, fresh engine, snapshot swap. - Engine rebuild —
rebuildVoiceEngine: same drain-slot swap around the already-decodedSampleData. Voice count / mode / mono trigger. - Live —
publishLiveParams(andmasterGain_, 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/instrumentis 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: gainswriteUsageExtState(prefix-guarded — accepts onlyrsusage_-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant.reasampler_processor(shell/instrument/:reasampler_processor.cpplifecycle +process(),processor_state.cppcomponent-state I/O + UI-thread parameter accessors,processor_reload.cppthe off-audio-threadreloadInstrument/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) — 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-threadreloadInstrument+ atomic pointer swap soprocess()does no allocation, no file I/O, no bridge calls. The instance state is{loaded capture id, one InstrumentParams}, andreloadInstrumentresolves + decodes exactly that one capture into theSampleDatathe engine plays. Self-contained playback (pS):ComponentStatev10 adds aSampleRefstable — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName);reloadInstrumentdecodes directly fromSampleRefs, 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-decodedSampleDatavia 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. pS:ComponentStatebumped v9→v10 (SampleRefstable); pre-v10 blobs lift to empty refs and re-save self-contained. pS-usage: publishes instance usage (heldSampleRefspaths) torsusage_<instanceGuid>at the tail ofreloadInstrument(off audio thread) viareaper_bridge::writeUsageExtState;ComponentStatebumped v10→v11 (instanceGuidfield); pre-v11 blobs mint guid on first publish.reasampler_editor— VST3IPlugViewLICE 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 puresample_bandsallocator:editor_session(session/bridge state, caches, commit-and-reload),editor_controls(the ONEfaceLayoutband 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 purecore/instrument/ui/deck_valuesmodule 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 (_browsefor the modal picker,_curvefor the velocity-curve popup) andeditor_platform(IPlugView/Win32 window plumbing). Shared internals ineditor_internal.h, no TU of its own. 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 toembed_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 purecore/ui/stroke_aaand 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 throughstrokeArcAA/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 touchshell/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:GetPluginFactoryexport, class registration, channel-forked class UIDs.editor_internal.h— INTERNAL shared helpers for thereasampler_editorTU family, included only by the editor's own shell TUs (editor_session/editor_controls/editor_paint_*/editor_input_*/editor_platform), never a public seam: theRect↔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 puredeck_groupsmodule's, not this file's. The piano-strip and root-key draws live ineditor_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-selectedSteinberg::FUID, built from the FOREVER-FROZEN macros incore/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 readingsrc/shell/instrument/reasampler_vst.hdirectly.)
Gotchas
editor_internal.his include-only — it has no TU of its own and must never become a public seam; only thereasampler_editorband-axis TUs include it.- The editor window class carries
CS_DBLCLKS, which REPLACES the second button-down of a double-click withWM_?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 viareasampler_vst.h) are FOREVER-FROZEN — never regenerate an already-shipped UID. - The UID selection
#ifdefinreasampler_vst.his the one deliberate exception to "channel identity derives fromapp_versionaccessors, no scattered#ifdefs" —INLINE_UIDneeds literal brace-init tokens, so it can't route through a runtime string accessor.