# 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`, `curve_popup`, `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 `#ifdef`s 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 reload** — `reloadInstrument`: bridge read, WAV re-decode, fresh engine, snapshot swap.
2. **Engine rebuild** — `rebuildVoiceEngine`: same drain-slot swap around the already-decoded
   `SampleData`. Voice count / mode / mono trigger.
3. **Live** — `publishLiveParams` (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` (parameter plumbing + the ONE `faceLayout` band resolve every paint and hit-test path shares), 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).
- `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 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 `#ifdef`s" —
  `INLINE_UID` needs literal brace-init tokens, so it can't route through a runtime
  string accessor.
