docs(product): fold the REAPER VST-host bridge into MIDI-playback framing

A VST hosted in REAPER can call back into REAPER's own API (name-resolved
via audioMaster), so a native ReaSampler instrument reads live project
ext-state directly. Re-frames the seam, D6, and the D1/D2 calculus.
This commit is contained in:
2026-07-26 05:49:47 -04:00
parent cce9016279
commit 6659c2dbdf
+176 -33
View File
@@ -5,10 +5,35 @@ banks. This is a **discussion-shaping doc, not a build plan** — no phase, no P
points, no settled forks yet. It exists so Daniel and the-boss can react to an honest points, no settled forks yet. It exists so Daniel and the-boss can react to an honest
map of the option space before anything is scoped. map of the option space before anything is scoped.
Status: framed by product-designer (2026-07-26). Grounded in the vendored REAPER SDK Status: framed by product-designer (2026-07-26), **revised 2026-07-26** to correct a
headers (`vendor/reaper-sdk/sdk/reaper_plugin.h`, `reaper_plugin_functions.h`) — the material omission — the earlier draft missed REAPER's **VST-host bridge**: a VST/VST3
plugin-format claims below are checked against those headers, not asserted. The plugin *hosted inside REAPER* can call back into REAPER's own API from within the
genuine forks are flagged as **Daniel's to decide**; nothing here pre-decides them. plugin, resolving API function pointers by name over the host `audioMaster` callback.
That single fact changes the integration seam (§2), the "one product or two" question
(D6), and the JSFX-vs-native calculus (D1/D2). The corrections are made plainly
in-place, not appended as a footnote; where the prior draft was too pessimistic about
what a native instrument can reach, it is fixed and the correction is flagged. Grounded
in the vendored REAPER SDK headers (`vendor/reaper-sdk/sdk/reaper_plugin.h`,
`reaper_plugin_functions.h`, `video_processor.h`, `reaper_plugin_fx_embed.h`) and
REAPER's published VST-extensions SDK page (`reaper.fm/sdk/vst/vst_ext.php`) — the
plugin-format claims below are checked against those, not asserted. The genuine forks
are flagged as **Daniel's to decide**; nothing here pre-decides them.
> **The bridge, stated once, up front (the correction).** A hosted VST is not limited to
> scraping bank WAVs + JSON off disk. REAPER hands the plugin its host callback; calling
> `hostcb(&effect, 0xdeadbeef, 0xdeadf00d, 0, "FunctionName", 0.0)` resolves *any* REAPER
> API function by name — the same string-keyed API surface the extension uses (verified:
> `video_processor.h` line 44 imports `video_CreateVideoProcessor` exactly this way;
> `GetProjExtState`/`SetProjExtState`/`EnumProjExtState` are ordinary entries in that same
> string-keyed table, `reaper_plugin_functions.h` lines 8376/8796/9969). The plugin can
> also fetch its **host context** — the track/take/project it's instantiated in (sibling
> opcode `0xdeadf00e`, `video_processor.h` line 40; CLAP has `clap_get_reaper_context`,
> `reaper_plugin.h` line 142). **Consequence:** a native ReaSampler instrument can read
> the *same* `"reasampler"` project ext-state that `persist` writes — live, project-aware,
> following the active project — not a file it re-parses off disk. This is a
> REAPER-VST-specific capability (it exists because the plugin is hosted *in REAPER*), and
> it is the thumb on the scale the prior draft failed to weigh. The cost of leaning on it
> is stated honestly in D1/D2: it makes the native plugin **REAPER-coupled.**
--- ---
@@ -88,16 +113,25 @@ the single most important thing for Daniel to internalize before scoping anythin
**Option A — a real VSTi/instrument plugin (JUCE or bare VST3/CLAP SDK) that reads **Option A — a real VSTi/instrument plugin (JUCE or bare VST3/CLAP SDK) that reads
ReaSampler's banks.** A separate build artifact: a VST3 (and/or CLAP) sampler plugin ReaSampler's banks.** A separate build artifact: a VST3 (and/or CLAP) sampler plugin
that the user instantiates on an instrument track. It reads the bank JSON + WAV layout that the user instantiates on an instrument track. It maps samples across the keyboard
ReaSampler writes, maps samples across the keyboard, and plays them MIDI-triggered with and plays them MIDI-triggered with a real voice engine. This is the "sophisticated
a real voice engine. This is the "sophisticated sampler" answer. sampler" answer.
- *Gives you:* everything an instrument is — polyphony, velocity, envelopes, the works, - *Gives you:* everything an instrument is — polyphony, velocity, envelopes, the works,
fully integrated into REAPER's routing/render/record path like any VSTi. fully integrated into REAPER's routing/render/record path like any VSTi. **And,
because it's hosted in REAPER, it reaches ReaSampler's project state directly via the
VST-host bridge** — it doesn't merely parse bank JSON off disk, it can read the live
`"reasampler"` ext-state `persist` writes, know its own host project, and follow the
active project. The prior draft treated Option A as a divorced file-reader; that was
the omission. A native ReaSampler instrument can be **natively, tightly integrated with
the extension's project state**, not loosely coupled through a file format alone.
- *Costs:* a **second codebase in a second plugin format**, almost certainly a new - *Costs:* a **second codebase in a second plugin format**, almost certainly a new
dependency (JUCE is the pragmatic choice; bare VST3 SDK is more code, CLAP is leaner dependency (JUCE is the pragmatic choice; bare VST3 SDK is more code, CLAP is leaner
but younger). It is a real DSP/voice-engine build, not a weekend. Cross-platform DSP, but younger). It is a real DSP/voice-engine build, not a weekend. Cross-platform DSP,
its own build/release/signing story, its own UI toolkit. This is a **product-sized its own build/release/signing story, its own UI toolkit. This is a **product-sized
commitment**, not a feature. commitment**, not a feature. And leaning on the bridge for state means the instrument
is **REAPER-coupled** — it stops being a portable VST that runs in any host and becomes
a REAPER companion. That may be exactly right (see D6), but it is a real narrowing and
must be a conscious choice, not a side effect.
**Option B — a JSFX sampler.** JSFX is REAPER's built-in scriptable plugin format **Option B — a JSFX sampler.** JSFX is REAPER's built-in scriptable plugin format
(text `.jsfx` files, JIT-compiled by REAPER, hostable as an instrument). A JSFX (text `.jsfx` files, JIT-compiled by REAPER, hostable as an instrument). A JSFX
@@ -112,6 +146,17 @@ instrument *can* receive live MIDI and emit audio on a track. It can load sample
than in a native plugin. Reading ReaSampler's JSON index from JSFX is friction (JSFX than in a native plugin. Reading ReaSampler's JSON index from JSFX is friction (JSFX
is not a general-purpose file parser). Best fit for a **minimal-to-mid** sampler, a is not a general-purpose file parser). Best fit for a **minimal-to-mid** sampler, a
real ceiling for a **sophisticated** one. real ceiling for a **sophisticated** one.
- *What JSFX can't match, now that the bridge is on the table:* the VST-host bridge is a
**VST/VST3/CLAP capability** — it is how a *hosted plugin* resolves REAPER API pointers.
JSFX is not a hosted VST; it has REAPER integration of its own kind (`ext_noinit`,
`file_*` ops, the shared `gmem[]`, `slider`/parameter plumbing), but it does **not** get
the same string-keyed REAPER-API surface a native VST does, and in particular has no
clean, native path to read `persist`'s `"reasampler"` project ext-state the way a native
VST can. On the JSFX path, the seam is genuinely "read a file ReaSampler wrote next to
the bank" (hence the sidecar note in §2). This asymmetry is new information: the bridge
makes **native meaningfully more integratable** than the prior draft assumed — it's not
just "native is more powerful DSP," it's "native can be *part of ReaSampler's state
model*, JSFX stays a file-coupled consumer."
**Option C — the hybrid (recommended framing to explore first).** Keep the extension **Option C — the hybrid (recommended framing to explore first).** Keep the extension
as the sole owner of **capture + organization** (which is its whole existing identity as the sole owner of **capture + organization** (which is its whole existing identity
@@ -130,10 +175,28 @@ and **"how much sampler do we actually want."** Those are §3 and §4.
## 2. The integration seam ## 2. The integration seam
The obvious shared artifact is what ReaSampler already produces: **the bank folder The prior draft assumed one seam: **the bank folder (project-relative WAVs) + the
(project-relative WAVs) + the bank/index JSON.** The question is whether that JSON is bank/index JSON on disk**, with the playback engine as a file-reader. That's still the
*sufficient* for a playback engine, or whether playback needs mapping data the index seam for *sample audio* (WAVs are files; there's no getting a live PCM stream across the
doesn't carry today. bridge, nor would you want to). But for **everything that isn't the raw audio** — the
index, the mapping data, which project's bank is active — the bridge opens a **second,
richer seam** the prior draft missed:
- **File seam (audio + a portable snapshot).** WAVs on disk; index/mapping as JSON or a
sidecar. Tool-agnostic, host-agnostic, portable. Any instrument (JSFX or native, in
REAPER or any other host) can consume it. This is the *only* seam JSFX gets, and the
*fallback/export* seam for native.
- **Live-state seam (native-in-REAPER only).** A native VST instance reads the
`"reasampler"` project ext-state directly via the bridge (`GetProjExtState` /
`EnumProjExtState`, resolved by name over `hostcb`), and knows its own host project via
the context callback. It sees what `persist` last wrote, follows the active project, and
needs no "point me at the right bank folder" wiring — it *asks REAPER* which project it's
in. This is strictly more than the file seam and it's REAPER-coupled by construction.
The design question below — "is the JSON sufficient, or does playback need mapping the
index doesn't carry" — is unchanged. What the bridge changes is *where that mapping can
live and how the instrument gets it*: for native, the mapping can be **live shared state**
between extension and instrument, not just a file handed across.
**What the current index carries** (from `bank_model`'s `Sample`, per CONTEXT.md §Data **What the current index carries** (from `bank_model`'s `Sample`, per CONTEXT.md §Data
model): id, display name, relative path, source range, channel count, sample rate, model): id, display name, relative path, source range, channel count, sample rate,
@@ -179,18 +242,29 @@ opinionated part. It also means **the bank index grows only by file-intrinsic fi
seam ReaSampler already knows how to add (mirror of how `provenance` was added as a seam ReaSampler already knows how to add (mirror of how `provenance` was added as a
field in M1 and populated later). field in M1 and populated later).
**A note on format:** if the instrument is native (Option A), it reads the bank JSON **A note on format (revised for the bridge):** the friction is asymmetric, and more so
directly — trivial. If it's JSFX (Option B), reading arbitrary JSON is friction; the than the prior draft said.
seam might need a **simpler sidecar** (a flat `.txt`/key-value map ReaSampler writes - *Native (Option A):* doesn't just "read the bank JSON off disk" — it can read the map
next to the bank, JSFX-parseable) rather than making JSFX parse the index JSON. That's out of **live project ext-state** via the bridge (whichever of i/ii/iii you choose, the
a concrete cost of the JSFX path and a reason the seam design and the format choice are extension and instrument can share the *same* `"reasampler"` state rather than one
coupled. writing a file the other re-parses). The `.wav` audio is still files; the mapping need
not be. Cleanest possible integration.
- *JSFX (Option B):* no bridge — reading arbitrary JSON is friction, so the seam likely
needs a **simpler sidecar** (a flat `.txt`/key-value map ReaSampler writes next to the
bank, JSFX-parseable). On the JSFX path the seam is unavoidably file-based. That's a
concrete cost of JSFX and a reason the seam design and the format choice stay coupled.
**Portability caveat:** the bank is project-relative and travels with the `.rpp` **Portability caveat (partly dissolved on the native path).** The bank is project-relative
(M4 machinery). Any instrument consuming it must resolve paths the same way — so the and travels with the `.rpp` (M4 machinery). Any *file-seam* consumer must resolve paths
instrument needs to know the *current* project bank folder. A native plugin instance the same way — so a JSFX instrument still needs to be told the current project bank folder
on a track can be told its folder (saved in plugin state); a JSFX likewise. This is a (saved in its own state), a real wiring detail, don't hand-wave it. But a **native**
solvable wiring detail but a real one — flag it, don't hand-wave it. instance sidesteps this: via the context callback it asks REAPER which project it's in,
then reads that project's bank location straight from ext-state — no "point me at the
folder" step. What was a shared wiring cost is now a **cost only on the JSFX path.** Note
the flip side: a native instrument that resolves its bank *only* through the bridge won't
work outside REAPER at all — so if a portable/exportable instrument is ever a goal, keep
the file seam as the source of truth and treat the live-state seam as an accelerator, not
the sole path.
--- ---
@@ -245,13 +319,29 @@ None of these are pre-decided here. Each is a genuine fork.
**D1 — Instrument format: JSFX vs. native VSTi (VST3/CLAP, likely via JUCE).** The **D1 — Instrument format: JSFX vs. native VSTi (VST3/CLAP, likely via JUCE).** The
central fork. JSFX = no new binary, no JUCE, cross-platform free, real ceiling on central fork. JSFX = no new binary, no JUCE, cross-platform free, real ceiling on
sophistication and awkward bank-JSON reading. Native = full sophistication and clean sophistication, awkward bank-JSON reading, **and no VST-host bridge** (file-seam only).
JSON integration, at the cost of a whole second codebase in a second format with its Native = full sophistication, clean integration, **plus the bridge**: live `"reasampler"`
own build/release/dependency/signing story. *My lean, for discussion only:* if the goal ext-state, project-awareness, embedded TCP/MCP UI (see below) — at the cost of a whole
is Tier 01, **prototype in JSFX first** — it proves the seam and the value with near-zero second codebase in a second format with its own build/release/dependency/signing story,
format commitment, and if it hits a ceiling the seam you designed still serves a later **and REAPER-coupling if it leans on the bridge.**
native build. Reach for native when Tier 2+ is a firm goal, not before. But this is
squarely Daniel's call and depends on how sophisticated he actually wants this. *What the bridge does to the native column (the honest update).* The prior draft's native
pitch was "more DSP power + parses JSON cleanly." The bridge adds a category the prior
draft didn't weigh: native can be **part of ReaSampler's live state model**, not a
file-coupled consumer. That is a real pull toward native beyond raw DSP — it's the
difference between "an instrument that reads a ReaSampler export" and "an instrument that
*is* ReaSampler's playback surface." It doesn't make JSFX wrong; it makes native's ceiling
higher and its integration tighter than I credited. Weigh it against the coupling cost: a
bridge-dependent VST is a **REAPER-only** artifact, no longer a portable VST.
*My lean, updated but unchanged in direction, for discussion only:* if the goal is
Tier 01, **still prototype in JSFX first** — it proves the *sampler value and the
key/velocity seam* with near-zero format commitment, and the file seam you design there is
exactly the portable/export seam a native build would keep anyway. The bridge is an
*integration* advantage, not a *does-the-sampler-work* advantage, so it doesn't change what
the cheapest proof is. Reach for native when either (a) Tier 2+ sophistication is a firm
goal, or (b) **tight live integration with ReaSampler's project state is itself the point**
— that second trigger is new, and it's the bridge's real contribution to this fork.
**D2 — Whether to take a JUCE (or any external plugin-SDK) dependency at all.** The **D2 — Whether to take a JUCE (or any external plugin-SDK) dependency at all.** The
project today is a clean C++ extension with two vendored submodules and a proud project today is a clean C++ extension with two vendored submodules and a proud
@@ -260,6 +350,17 @@ second build target of a fundamentally different kind. That's a real architectur
weight. JSFX sidesteps it entirely. Flagging it as its own decision because "should we weight. JSFX sidesteps it entirely. Flagging it as its own decision because "should we
depend on JUCE" is a bigger standing commitment than "should we build a sampler." depend on JUCE" is a bigger standing commitment than "should we build a sampler."
*The bridge's bearing on D2.* Weigh two coupling costs, not one. A native instrument that
uses the bridge takes on **both** a plugin-SDK dependency (JUCE/VST3/CLAP) *and* a
REAPER-API coupling (the same `reaper_plugin_functions.h` surface the extension binds — so
the instrument would vendor and bind against it too, likely resolving pointers via `hostcb`
rather than the extension's startup path). The upside is that the pure sampler core stays
REAPER-free and testable regardless (D3); it's only the *shell* that touches the bridge, so
the coupling is contained where the project already puts REAPER coupling. The downside is
plain: choosing the bridge means choosing a REAPER-only instrument. If Daniel wants a
sampler that also runs in other hosts, the bridge is off the table and D1's native pitch
loses its integration edge — collapsing back toward "native = more DSP power only."
**D3 — Does the pure-core discipline survive the format boundary?** ReaSampler's **D3 — Does the pure-core discipline survive the format boundary?** ReaSampler's
identity is *pure REAPER-free testable core + thin shells*. A sampler's **voice engine, identity is *pure REAPER-free testable core + thin shells*. A sampler's **voice engine,
envelope math, key/velocity mapping, and repitch logic are exactly the kind of pure, envelope math, key/velocity mapping, and repitch logic are exactly the kind of pure,
@@ -292,8 +393,50 @@ feature *of* ReaSampler — the way a sample library and a sampler that reads it
related-but-distinct products. That framing might keep ReaSampler sharp (it stays the related-but-distinct products. That framing might keep ReaSampler sharp (it stays the
capture tool it is) while letting the instrument evolve on its own clock and format. capture tool it is) while letting the instrument evolve on its own clock and format.
The alternative reading — it's all one integrated sampler-workstation — is also The alternative reading — it's all one integrated sampler-workstation — is also
coherent. **Which of those two ReaSampler *is* is the highest-order question here, and coherent.
it's Daniel's to answer before format/tier decisions mean much.**
*Re-argued with the bridge on the scale (the prior draft weighed this fork without it).*
The prior draft implicitly assumed the two products could only ever share a **file
format** — which makes "companion product" feel clean and natural, because a shared file
is exactly what independent products share. The bridge changes the terms. A native
instrument can be **tightly, live-integrated with ReaSampler's project state**: same
`"reasampler"` ext-state, same project identity, following the active project — a degree
of coupling that is *more than* "two products that happen to read the same files" and
starts to look like "two faces of one tool sharing one state model." That is a genuine
thumb toward the **one-integrated-product** reading. It's not decisive, and I won't
pretend it is:
- It only applies if the instrument is **native** (JSFX can't reach the bridge, so a
JSFX instrument *is* naturally a file-coupled companion — the format choice and this
fork are entangled: pick JSFX and you've half-answered D6 toward "companion").
- It's an *available* integration, not a *required* one — a native instrument could
still be built to the file seam only, deliberately staying a loose companion for
portability. The bridge widens the option, it doesn't force it.
- The tight-integration reading buys **REAPER-coupling**; the companion reading buys
**portability and independent evolution.** Those are real, opposed goods, and the
bridge doesn't resolve which Daniel wants — it just makes the integrated option
*materially more capable* than the prior draft credited, so the companion reading no
longer wins by default on "well, they can only share a file anyway."
**Net:** the bridge strengthens "one integrated product" from a coherent-but-equal option
into the one with the stronger technical affordance — *if* native-in-REAPER is the path.
It does not pre-decide the fork. This is still the highest-order question and it's Daniel's
to answer, but he should answer it knowing the integrated option is richer than the
file-sharing picture implied — and knowing that choosing JSFX quietly tilts it the other
way.
**D7 — Embedded TCP/MCP UI (new option the bridge surfaces; native/CLAP only).** While
verifying the bridge I found a second REAPER-VST-specific affordance the prior draft never
mentioned: a hosted plugin can **draw its own embedded UI directly in the track/mixer
control panel** (`reaper_plugin_fx_embed.h`: VST2 answers `canDo("hasCockosEmbeddedUI")`
with `0xbeef0000` and draws via `effVendorSpecific/effEditDraw`; VST3 implements
`IReaperUIEmbedInterface`; CLAP exposes `cockos.reaper_embedui`). Concretely: a native
ReaSampler instrument could render a compact keymap/level strip *inline in the TCP/MCP*,
not only in its own plugin window — the same surface REAPER's own JS/embedded FX use. This
is **native/CLAP-only** (JSFX can't do it) and is pure polish, not a Tier-0 need — but it's
a real integration affordance that only exists on the native-in-REAPER path, and it
compounds the D1/D6 tilt: if "ReaSampler's playback surface, deeply woven into REAPER's UI"
is the vision, this is a lever only native reaches. Flagging it for completeness, not
recommending it — it's a Tier-3-ish nicety, deferred by default.
--- ---