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
+57
View File
@@ -2001,6 +2001,63 @@ a multi-capture drag over the FX button is either rejected or loads the first).
must construct exactly ReaSampler 9000's state bytes — the cross-artifact contract still
being defined in Phase S. Blocks the drop half until the blob format is agreed.
## VST3 channel identity — the UID pair + the pairing surface (S18; extends Phase V V4)
**Decided (Daniel, 2026-07-26):** the beta/stable channel split Phase V V4 gave the
*extension* extends to the **ReaSampler 9000 VST3 instrument** — a beta-built VST pairs with
the beta extension only, a stable VST with stable only, both installable side-by-side in one
REAPER. This is the instrument-side companion to V4 and mirrors its philosophy exactly:
**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.**
**What is already isolated (structural, not added by S18).** The wire/data pairing is
already done and needs no per-key work: `ext_keys.h`'s `kProjExtNamespace()` delegates to
`app_version::extStateNamespace()`, so a beta-compiled VST's bridge reads `"reasampler_beta"`.
Every wire key — `banks`, `assign_request`, S9's bank-generation key (in-flight), S17's
component-state contract, and **any future key** — is a plain constant *under* that
namespace, so channel data-isolation is **structural: no per-key opt-in, and a future key
that forgets to isolate is impossible by construction** (it keys off the namespace accessor,
not a raw literal). What S18 adds is only the missing *plugin identity* layer.
- **The UID-pair invariant (the permanent commitment).** The VST3 class UID is the plugin's
identity — a saved REAPER project records it and rebinds a saved instance by it. Today
`reasampler_vst.h` holds **one** forever-locked UID (`kReaSamplerProcessorUID`,
`REASAMPLER_PROC_UID_1..4`, S-NAME-1). A beta VST with the **same** UID cannot coexist with
stable in one install (same UID = identity collision / arbitrary rebind). So beta needs its
**own** forever-stable UID: a second constant, minted once, locked exactly as the first.
**Invariant: BOTH UIDs are frozen forever once shipped; the channel bit selects which is
compiled into this binary** (one `DEF_CLASS2`, one class per binary — not both classes in
one binary; that mirrors V4's fully-isolated-binary philosophy and keeps a beta build from
ever presenting the stable identity). Saved-project isolation follows directly: a project
saved with beta instances rebinds only to the beta VST; a stable-saved instance opened
where only the beta extension has banks resolves the stable UID and shows a clean empty
"pick a capture" state (S10 policy), not an error.
- **Binary + display identity, channel-derived.** Mirror the extension's `OUTPUT_NAME` fork
(`reaper_reasampler` / `reaper_reasampler_beta`): the VST3 module's on-disk name forks
`reasampler_9000` / `reasampler_9000_beta`, its factory display name "ReaSampler 9000" /
"ReaSampler 9000 beta", and its editor title band + S6 embed-strip label are channel-aware
— **all sourced from `app_version` channel accessors (a VST-name accessor beside
`binaryName()`/`dockTitle()`), never a literal in `reasampler_vst.h`/`vst_entry.cpp`.** The
factory version string carries the `-beta` render where V4's `appVersion()` already does;
vendor/url/email stay shared unless V4 qualified the equivalent (V4 kept the lane-name
prefix shared — shared-where-V4-shares is the default).
- **The complete pairing surface (the guarantee to state, not new code).** A channel's VST
talks to that channel's extension **only**, because (1) plugin identity — UID + filename +
display — is channel-forked (above), and (2) **all** wire keys live under the
channel-derived `kProjExtNamespace()`. The two together make pairing complete and
structural: identity keeps the *plugins* distinct; the namespace keeps the *data* distinct.
No per-key or per-seam isolation work is ever needed — S8's assignment key, S9's generation
key, and S17's blob-injection key all inherit it. **Verify all identity/factory wiring
against the vendored Steinberg SDK** (`DEF_CLASS2` / `INLINE_UID` / `FUID` from
`pluginfactory.h` + `funknown.h`); the pure `app_version` name accessors are CTest-tested.
- **Fork S18-F1 (flagged — Daniel's call): mint the beta UID now vs. at first beta release.**
Lean **mint now** — mirrors the stable UID (minted at the S1 spike, locked long before
ship), removes a "remember to mint before shipping beta" landmine, zero cost for an
unused-until-beta constant. The alternative (a locked-once placeholder replaced before the
first beta VST ships) defers the commitment but adds a release-gate step. Flagged only
because the UID is a forever commitment.
## Non-goals / guardrails
- **The instrument never captures and never inserts into the arrange.** Playback is a
+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
+45
View File
@@ -918,6 +918,51 @@ This reshapes S16 and **flips the r7 WDL verdict** on `WDL_SimplePitchShifter`.
with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 interaction note;
CONTEXT.md §Pitch engine modes — Varispeed vs Preserve + the corrected WDL surface finding.
### Addendum — VST channel isolation (Daniel, 2026-07-26)
**Daniel's directive (2026-07-26, settled):** *"support the beta/stable channels for the VST
as well. The VST in beta should talk to the beta extension only."* This extends Phase V's V4
beta/stable split — which fully isolated the **extension** per channel — to the **ReaSampler
9000 VST3 instrument**. Spec'd as **S18**, an immediate Phase S wave, dispatchable in parallel
with or right after the in-flight waves (S9 ext_keys, S15/S16 processor/editor) — it touches
`vst_entry.cpp` / `reasampler_vst.h` / the CMake VST3 block, mostly disjoint from those.
**Established honestly from the tree — what already works vs. what's missing:**
- **Already isolated (the V4↔S4 reconcile did this): data pairing.** A beta-built VST already
*reads* the beta namespace — `ext_keys.h`'s `kProjExtNamespace()` delegates to
`app_version::extStateNamespace()`, and every wire key (`banks`, `assign_request`, S9's
generation key, S17's blob key, any future key) is a plain constant *under* that namespace.
Channel data-isolation is therefore **structural, not per-key** — new keys inherit it for
free. No S18 work here.
- **Missing: the VST's *plugin identity*.** Its class UID, binary filename, and display
strings are single-valued (same for both channels), so two installed channels would collide
on UID and filename. S18 closes exactly this.
**The shape of S18 (mirrors V4's philosophy — one channel per binary, one bit drives it):**
1. **A UID pair.** The stable class UID is locked forever (S-NAME-1). Beta needs its own
forever-stable UID (a second constant, minted once, locked identically). Both frozen
forever; the channel bit selects which is compiled in. **One class per binary, not both**
— the V4 fully-isolated-binary philosophy, so a beta build never presents the stable
identity. Saved-project isolation follows: a beta-saved instance rebinds only to the beta
VST. *Fork S18-F1 (Daniel's call):* mint the beta UID **now** (lean — mirrors the stable
UID minted at the S1 spike, removes a pre-ship landmine, zero cost unused) vs. defer to
first beta release behind a locked-once placeholder.
2. **Channel-derived binary + display identity.** `reasampler_9000` / `reasampler_9000_beta`
filename (mirror the extension's `OUTPUT_NAME` fork); "ReaSampler 9000" / "ReaSampler 9000
beta" display; editor title + embed label channel-aware — all from the ONE bit via
`app_version` accessors, no scattered `#ifdef`s (the V4 invariant).
3. **The pairing guarantee, stated as an invariant.** A channel's VST talks to that channel's
extension only, because identity keeps the plugins distinct and the channel-derived
namespace keeps the data distinct. **Structural, not per-key** — S8/S9/S17's cross-artifact
keys all inherit it; a future key that forgets to isolate is impossible by construction.
4. **DAW-verify contract.** Both channels installed side-by-side; each browser sees only its
channel's banks; stable-project + beta-VST = clean empty (not error); the S-NAME-1
rename/rebind test extends to the beta UID.
**Where the spec lives:** PLAN.md §S18; CONTEXT.md §VST3 channel identity — the UID pair + the
pairing surface. The pairing surface's data half is already load-bearing V4 machinery; S18
adds only the identity fork on top.
---
## Where this landed