From 0e9ef05c75556e2c2e926b6a0b2e7bec1820932c Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 26 Jul 2026 23:37:53 -0400 Subject: [PATCH] =?UTF-8?q?docs(product):=20spec=20S18=20=E2=80=94=20VST3?= =?UTF-8?q?=20channel=20isolation=20(beta=20ReaSampler=209000=20pairs=20wi?= =?UTF-8?q?th=20beta=20extension=20only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 57 ++++++++++++++++++++ PLAN.md | 98 +++++++++++++++++++++++++++++++++++ docs/product/midi-playback.md | 45 ++++++++++++++++ 3 files changed, 200 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index d8c23f4..900a191 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 diff --git a/PLAN.md b/PLAN.md index 1deb3f1..01882ab 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/docs/product/midi-playback.md b/docs/product/midi-playback.md index 88b3a92..ddc1ddc 100644 --- a/docs/product/midi-playback.md +++ b/docs/product/midi-playback.md @@ -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