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 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. 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 ## Non-goals / guardrails
- **The instrument never captures and never inserts into the arrange.** Playback is a - **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 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. 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) ## Phase S — held and optional-forever (noted, not specified)
- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun), - **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 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; 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. 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 ## Where this landed