From 37cb290cbce5ae3d627dffe8b06e3bf989b3ecb4 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 26 Jul 2026 23:09:28 -0400 Subject: [PATCH] =?UTF-8?q?docs(product):=20S17=20load-capture=20seam=20?= =?UTF-8?q?=E2=80=94=20settle=20on=20(B)=20component-state=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold the decision into the existing phase-s S17 spec: (B) via TrackFX_SetNamedConfigParm vst_chunk; (A) ext-state handshake rejected. Flag the shared component-state blob format as an extension<->instrument coordination dependency. --- CONTEXT.md | 75 +++++++++++++++++++++++++++++++----------------------- PLAN.md | 44 +++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 890fe52..d8c23f4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1913,27 +1913,40 @@ reads bank index + mapping from `"reasampler"` ext-state via the bridge — §Th but **no entry point for an external actor to say "this fresh instance should play *this specific* capture."** Reading the bank is not the same as being *pointed at one sample*. This wave is the reason to add that seam, and the seam lands **inside the instrument** -(the `phase-s` artifact), not the extension. Two candidate mechanisms — **the choice is a -Phase S open question:** -- **(A) Fresh-instance ext-state handshake.** The extension writes a small "pending load" - hint into `"reasampler"` ext-state keyed to the target track/FX (e.g. the capture id + - a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via the same - bridge it already uses, claims + clears the hint, and self-selects that capture. **Pro:** - reuses the existing bridge; no new VST3 surface. **Con:** a cross-process handshake with - a claim/clear race to get right; "which instance claims which hint" needs a stable key. -- **(B) VST3 `setState` / preset injection.** The extension builds the instance's - component state (the same blob `getState`/`setState` round-trips) with the capture - pre-selected and injects it right after `TrackFX_AddByName`. **Pro:** deterministic, no - shared-state race, uses the instrument's own persistence format. **Con:** the extension - must know and construct the instrument's state blob format — a tighter cross-artifact - coupling to a format that is itself still being built in Phase S; verify whether the - extension can set an added FX's state through the REAPER API (candidate: - `TrackFX_SetNamedConfigParm` / a state-set path — **not yet confirmed against - `reaper_plugin_functions.h`**). +(the `phase-s` artifact), not the extension. - Lean: **(A)** keeps the artifacts loosely coupled through the one ext-state seam they - already share and avoids the extension hard-coding the instrument's state format — but - it inherits the claim/clear race. Daniel to decide (open question below). +**Mechanism (SETTLED — (B) VST3 component-state injection).** Right after +`TrackFX_AddByName` returns the new FX index, the extension writes the instance's component +state directly — the same blob the instrument's `getChunk`/`setChunk` round-trips — with +the target capture pre-selected. Deterministic, no shared-state race, no cross-process +handshake; it uses the instrument's own persistence format. The state-set path is +`TrackFX_SetNamedConfigParm` — **verified present** in `reaper_plugin_functions.h`: `bool +TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char* +value)`, and the header documents the write-parms `vst_chunk` / `vst_chunk_program` as the +base64-encoded VST-specific chunk. So the injection call is +`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", )`. + +**Load-bearing caveat — `vst_chunk` is the plugin's own serialized chunk.** `vst_chunk` is +ReaSampler 9000's **own** base64-encoded serialized state (its `getChunk`/`setChunk` +FXP/FXB-style blob), **not** a raw VST3 `IComponent::setState` stream that REAPER +re-marshals into the plugin. The extension therefore has to construct **exactly the +instrument's own state-blob bytes** with the capture pre-selected — REAPER does not +translate a neutral state representation on its behalf. This makes the **component-state +blob format a shared cross-artifact contract** — one that is **still being defined in Phase +S** — and a **coordination dependency between the extension and the instrument:** both must +agree on the exact byte layout that ReaSampler 9000's `setChunk` accepts before either half +is final. The load-capture seam and the component-state persistence work (§Where the toggle +lives / component-state version bumps) share this one blob format. + +**Rejected alternative — (A) fresh-instance ext-state handshake.** The extension writes a +small "pending load" hint into `"reasampler"` ext-state keyed to the target track/FX (a +capture id + a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via +the bridge it already uses, claims + clears the hint, and self-selects that capture. It +would keep the artifacts loosely coupled through the one ext-state seam they already share +and avoid the extension hard-coding the instrument's state format — but it **loses on the +claim/clear race:** "which instance claims which hint" needs a stable key and a +cross-process handshake to get right, and (B) sidesteps that entirely by writing the state +directly and deterministically. **Coexistence with the OS drag-out (disambiguation contract).** The two OS-vs-internal modes are disambiguated **by pointer location, not a mode toggle** — the user never picks @@ -1962,20 +1975,12 @@ a multi-capture drag over the FX button is either rejected or loads the first). predicate; the M11 `drag_out` unit tests must not regress. **Open questions (Daniel / Phase S team to decide).** -- **Load-capture seam mechanism (A vs. B above).** The single load-bearing Phase S design - choice this wave forces. Lean is (A) for loose coupling; needs Daniel's call. - **Multi-capture drag over an FX button** — reject (only single-capture drags arm `InstrumentDrop`), or load the first / a keymap of all? Tier-0 leans reject-or-first; a multi-capture keymap load is a Tier-1 stretch. - **FX-button hotspot vs. whole TCP.** Does the drop zone have to be the FX button specifically, or is dropping anywhere on the target track's TCP enough (simpler hit resolution, arguably clearer target)? Depends on what the SDK exposes (see must-verify). -- **Numbering vs. the `phase-s` worktree (reconciled on merge to dev).** Authored on dev - as a provisional S7; renumbered to **S17** on merge, since the worktree's authoritative - Phase S set (S7 stereo, S8 ingest, S9 change-detection, S10–S16) took the lower labels. - Drop-and-load stays a distinct integration gesture (drop onto a track's FX button) — - a sibling of but not the same as S8 (ingest into the bank) and S13 (drop-to-load inside - the editor). See PLAN.md §S17. **Must-verify before build (drop-and-load).** - `TrackFX_AddByName` — **verified present** (`reaper_plugin_functions.h`): signature and @@ -1985,10 +1990,16 @@ a multi-capture drag over the FX button is either rejected or loads the first). `reaper_plugin_functions.h`); whether the FX button specifically is addressable vs. the TCP as a whole is an open verification that also decides the "hotspot vs. whole TCP" question. -- **Instance state injection (only if seam mechanism (B) is chosen)** — whether the - extension can set a just-added FX's state via the REAPER API - (`TrackFX_SetNamedConfigParm` or similar) — **not yet confirmed against - `reaper_plugin_functions.h`.** Moot if (A) is chosen. +- **Instance state injection (seam mechanism (B) — SETTLED, load-bearing prerequisite)** — + **verified present** in `reaper_plugin_functions.h`: `bool + TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char* + value)`, with the header documenting `vst_chunk` / `vst_chunk_program` as the + base64-encoded VST-specific chunk write-parms. The injection call is + `TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", )`. The remaining + prerequisite is **not** the API but the **shared component-state blob format**: `vst_chunk` + carries the instrument's *own* serialized chunk (its `setChunk` input), so the extension + 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. ## Non-goals / guardrails diff --git a/PLAN.md b/PLAN.md index eff680a..1deb3f1 100644 --- a/PLAN.md +++ b/PLAN.md @@ -956,9 +956,25 @@ The current Phase S spec gives the instrument a *live-state read* seam (it reads index + mapping from `"reasampler"` ext-state via the bridge) but **no entry point for an external actor to say "instantiate playing *this specific* capture."** This wave is the reason to add that seam. It is a Phase S dependency, not extension-side, and must land in -the instrument before drop-and-load's drop half can work end-to-end. See CONTEXT.md -§Phase S for the two candidate seam mechanisms (fresh-instance ext-state handshake vs. -VST3 `setState` preset injection) and the open question on which is chosen. +the instrument before drop-and-load's drop half can work end-to-end. **Mechanism (SETTLED +— (B) VST3 component-state injection):** after `TrackFX_AddByName`, the extension writes the +new instance's component state directly via `TrackFX_SetNamedConfigParm(track, fx, +"vst_chunk", )` — **verified present** in `reaper_plugin_functions.h` (`bool +TrackFX_SetNamedConfigParm(MediaTrack*, int fx, const char* parmname, const char* value)`; +the header documents the `vst_chunk`/`vst_chunk_program` parms as the base64-encoded +VST-specific chunk write-path). Deterministic, no shared-state race, no cross-process +handshake. **Load-bearing caveat:** `vst_chunk` is the plugin's **own** base64-encoded +serialized chunk (its `getChunk`/`setChunk` FXP/FXB-style blob), **not** a raw VST3 +`IComponent::setState` stream REAPER re-marshals — so the extension must construct exactly +ReaSampler 9000's own state-blob bytes with the target capture pre-selected. That makes the +**component-state blob format a shared cross-artifact contract** — still being defined in +Phase S — and a coordination dependency between the extension and the instrument: both must +agree on the exact byte layout the instrument's `setChunk` accepts. Rejected alternative — +**(A) fresh-instance ext-state handshake** (extension writes a "pending load" hint into +`"reasampler"` ext-state, the fresh instance claims + clears it on init): loosely coupled +through the existing bridge, but loses on the claim/clear race — "which instance claims +which hint" needs a stable key and a cross-process handshake to get right. See CONTEXT.md +§Phase S (drop-and-load) for the full seam decision. **Coexistence with the M11 OS drag-out (disambiguation, load-bearing):** the two drag modes are disambiguated by **where the pointer goes**, not by a mode toggle. Inside the panel client rect → `Internal` (unchanged). Left the panel but still over REAPER's own @@ -976,7 +992,9 @@ still works unchanged; no media item is ever inserted into the arrange; the inst holds no private copy (it reads the one authoritative bank). **Depends on:** M11 (`drag_out` gesture machinery — the mode it extends); **Phase S S4** (a loadable, playing ReaSampler 9000 instance must exist) **AND the new load-capture seam -added inside ReaSampler 9000**. Composes with — but is distinct from — **S8** (ingest +added inside ReaSampler 9000 via (B) component-state injection — the shared component-state +blob contract must be defined so the extension can construct it and the instrument's +`setChunk` accept it**. Composes with — but is distinct from — **S8** (ingest through the bank: capture/import/drop *into* the bank) and **S13** (drop-to-load *inside* the editor). S17 is the third integration gesture: drop *onto a track's FX button* to instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S @@ -998,21 +1016,17 @@ artifact, not extension-only. capture. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline). - [ ] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry - point that lets the just-added instance be told which capture to play (mechanism chosen - per the CONTEXT.md open question). This is the cross-artifact half; it lands in the - instrument, not the extension. + point that lets the just-added instance be told which capture to play via **(B) + component-state injection**: the instrument's `getChunk`/`setChunk` must round-trip a + component-state blob whose byte layout is the **shared cross-artifact contract** (defined + in Phase S) that the extension constructs with the target capture pre-selected and writes + via `TrackFX_SetNamedConfigParm(..., "vst_chunk", ...)`. This is the cross-artifact half; + it lands in the instrument, not the extension. **Coordination dependency:** the blob + format must be agreed between extension and instrument before either half is final. - [ ] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across 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. -> **Numbering reconciliation (resolved on merge to dev).** This wave was authored on -> **dev** as a provisional **S7** while dev's Phase S ran only S1–S6. On merge with the -> `phase-s` worktree, the worktree's authoritative numbering (S7 stereo, S8 ingest, S9 -> change-detection, S10–S16) took the lower labels, so drop-and-load was renumbered to -> **S17** — the next free label past the worktree's Phase S set. It stays a distinct -> integration gesture (drop onto a track's FX button), a sibling of but not the same as -> S8 (ingest into the bank) and S13 (drop-to-load inside the editor). - ## 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