docs(product): spec S18 — VST3 channel isolation (beta ReaSampler 9000 pairs with beta extension only)

This commit is contained in:
2026-07-26 23:37:53 -04:00
parent be67c93b39
commit 0e9ef05c75
3 changed files with 200 additions and 0 deletions
+98
View File
@@ -1027,6 +1027,104 @@ artifact, not extension-only.
single- and multi-capture payloads; FX-button hit resolution (pure geometry where it
can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged.
## S18 — VST3 channel isolation: a beta ReaSampler 9000 that pairs with the beta extension only
**Goal:** Extend Phase V's beta/stable channel split (V4 — the *extension* is fully
isolated per channel) to the **ReaSampler 9000 VST3 instrument**, so a beta-built VST is a
distinct plugin that pairs only with the beta extension, and a stable VST pairs only with
stable — installable side-by-side in one REAPER with no collision. **What already works
(established from the tree, not re-derived): data pairing is done.** `ext_keys.h`'s
namespace is channel-derived (`kProjExtNamespace()` → `app_version::extStateNamespace()`),
so a beta-compiled VST already *reads* `"reasampler_beta"`; every wire key (`banks`,
`assign_request`, S9's future generation key, any future key) is a plain constant *under*
that namespace, so channel data-isolation is **structural, not per-key** — new keys inherit
it automatically. **What is missing is the VST's *plugin identity*:** its class UID, binary
filename, and display strings are single-valued today (same for both channels → a
UID/filename collision if both are installed). This wave closes that. Mirrors V4's
philosophy: **one channel per binary; all identity derives from the ONE
`REASAMPLER_CHANNEL_IS_BETA` bit via `app_version`, no scattered `#ifdef`s.** CONTEXT.md
§Phase S (VST3 channel identity — the UID-pair invariant + the pairing surface). Product
framing: `docs/product/midi-playback.md` §Addendum — VST channel isolation.
**Consistent with V4 (make the invariant explicit):** V4 already committed the extension to
*two* forever-stable command-id families and *two* forever-stable ext-state namespaces. This
wave commits the *instrument* to the parallel permanent cost: **two forever-stable VST3 class
UIDs** — the existing stable UID (S-NAME-1, locked) and a second beta UID (minted once,
locked forever the same way). Both are frozen forever; the channel bit selects which is
*compiled into this binary*. **One class per binary, not both in one binary** — this mirrors
V4's fully-isolated-binary philosophy (a channel build carries only its own identity), keeps
the factory registration a single `DEF_CLASS2`, and means a beta-installed VST cannot present
the stable identity.
**Verify (in DAW):** the stable VST3 (`reasampler_9000.vst3`, existing UID) and a beta VST3
(`reasampler_9000_beta.vst3`, the new UID) install side-by-side in one REAPER and both appear
in the FX browser as distinct plugins ("ReaSampler 9000" and "ReaSampler 9000 beta"); a beta
instance reads only the beta extension's banks (its browser shows the beta banks, never
stable's) and a stable instance only stable's; a project saved with a **beta** instance
reopens rebinding to the beta VST (not the stable one) and restores its state; a project
saved with a **stable** instance opened where only the beta extension has banks shows a clean
empty "pick a capture" state (S10 policy), **not** an error; the S-NAME-1 save-rename-reopen
compat test extends to the beta UID (a beta instance rebinds by its own UID). **Nothing plays
differently** — this is an identity/pairing wave, no engine change.
**Depends on:** V4 (the `app_version` channel-identity single-source this extends), S1 (the
VST3 factory + `reasampler_vst.h` identity constants + the CMake second target it edits).
Consumes the S4 live-state seam only to *demonstrate* pairing; adds no new seam. **Disjoint
from the in-flight waves:** touches `vst_entry.cpp`, `reasampler_vst.h`, and the CMake VST3
block — S9 (ext_keys additions) and S15/S16 (processor/editor) are elsewhere. **ext_keys
coordination note:** S9's new generation key is a plain constant under the channel-derived
namespace, so it inherits this isolation with no S18 change; no coordination edit is needed —
only the shared awareness that all wire keys must live under `kProjExtNamespace()` (already
the rule). Dispatchable in parallel with, or immediately after, the in-flight waves.
- [ ] **Beta VST3 class UID (the permanent commitment).** Mint a second FOREVER-STABLE
class UID in `reasampler_vst.h` alongside the existing `kReaSamplerProcessorUID`
(`REASAMPLER_PROC_UID_1..4`) — a distinct forever-frozen constant (e.g.
`REASAMPLER_PROC_UID_BETA_1..4` + `kReaSamplerProcessorUIDBeta`). The channel bit selects
which UID the factory registers (`DEF_CLASS2`'s `INLINE_UID` + the runtime `FUID`) —
compile-time, one class per binary. **Both UIDs are frozen forever once shipped** — the
same lock the stable UID carries (a saved project records the UID and rebinds by it; a
changed UID orphans every saved beta instance). Record the invariant in the header's
UID-lock comment. **Fork S18-F1 (flagged — Daniel's call):** whether the beta UID is
minted *now* (locked from this wave) or deferred to *first beta release* of the VST
(leaving a compile-time placeholder that must be replaced-once-and-locked before any beta
VST ships). Lean: **mint now** — mirrors how the stable UID was minted at the S1 spike
(locked long before ship), removes a "remember to mint before shipping" landmine, and the
cost of an unused-until-beta constant is zero. Flagged because it is a forever commitment.
- [ ] **Channel-derived binary + display identity (no scattered `#ifdef`s — the V4
invariant).** Route all VST identity through `app_version` accessors, mirroring how the
extension's `OUTPUT_NAME` already derives from `REASAMPLER_OUTPUT_NAME`. (a) **Binary
name:** the CMake VST3 target's `OUTPUT_NAME` forks by channel — `reasampler_9000`
(stable) / `reasampler_9000_beta` (beta) — driven by the same `REASAMPLER_CHANNEL` config
the extension target reads (add a `REASAMPLER_VST_OUTPUT_NAME` beside
`REASAMPLER_OUTPUT_NAME`, or reuse the one channel bit; **check what the extension does and
mirror it exactly**). (b) **Display name:** the factory `DEF_CLASS2` plug-in display string
is "ReaSampler 9000" / "ReaSampler 9000 beta" — sourced from an `app_version` VST-name
accessor (a new channel-derived accessor beside `binaryName()`/`dockTitle()`, e.g.
`vstPluginName()`), not a literal in `reasampler_vst.h`/`vst_entry.cpp`. (c) **Editor title
band + S6 embed-strip label** are channel-aware from the same accessor. All fan out from the
ONE channel bit — no per-file `#ifdef`.
- [ ] **Factory vendor/version strings channel-aware where V4 does the equivalent.** The
`BEGIN_FACTORY` vendor/url/email and the `PClassInfo2` version string
(`REASAMPLER_VST_VERSION`, currently a fixed `"0.1.0.0"`) align with the channel where the
extension's V4 equivalents do — the version display carries the `-beta` render
(`appVersion()` already yields `"0.9.01-beta"` on beta); the vendor strings stay shared
unless V4 qualified them (**check V4's treatment and match it** — V4 kept the lane-name
prefix shared, so shared-where-V4-shares is the default). No new forever-stable string
beyond the UID and the id-family V4 already owns.
- [ ] **Pairing-surface invariant recorded (no new code — a documented guarantee).** Write
the complete pairing surface as an invariant in CONTEXT.md: a channel's VST talks to that
channel's extension **only**, because (1) plugin identity (UID + filename + display) is
channel-forked here, and (2) **all** wire keys — `banks`, `assign_request`, S9's
generation key, and any future key — live under the channel-derived `kProjExtNamespace()`,
so the beta VST's bridge reads only `"reasampler_beta"`. The invariant to write: **channel
isolation is structural — no per-key opt-in — so a future wire key that forgets to isolate
is impossible by construction** (it keys under the namespace accessor, not a raw literal).
This is the guard that S9/S8/S17's cross-artifact keys inherit isolation for free.
- [ ] **DAW-verify contract (the acceptance gate, no unit test — identity is a shell fact).**
Both channels installed side-by-side: each browser sees only its channel's banks; a
stable-project + beta-VST opens clean-empty (not error); the S-NAME-1 rename/rebind compat
test extended to the beta UID (save a beta instance, confirm it rebinds by the beta UID on
reopen). The pure `app_version` accessors (binary/display name per channel) are CTest-green
in `app_version_tests` (extend the existing V4 channel-string assertions to the new VST
accessors); the UID selection + factory wiring are DAW-verified (no host-free test path).
## Phase S — held and optional-forever (noted, not specified)
- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun),
full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once