docs: scope MIDI-playback instrument as Phase S (native VST3, second artifact)

Settle forks D-A..D-D: bare Steinberg VST3 SDK + LICE editor; split seam
with root-note/loop-points on Sample now; Tier 0-1 committed; embedded
TCP/MCP UI scheduled. Phase S added to PLAN.md and CONTEXT.md spec.
This commit is contained in:
2026-07-26 14:45:02 -04:00
parent 0d07d3cbfe
commit 10ceb95ddd
3 changed files with 579 additions and 62 deletions
+89 -62
View File
@@ -1,19 +1,25 @@
# MIDI playback — opportunity & design-space framing
Framing for a **MIDI-triggered audio sampler** that plays back ReaSampler's captured
banks. This is a **discussion-shaping doc, not a build plan** — no phase, no PLAN.md
points. Several forks that were open in prior drafts are now **locked** (see below);
what remains open is re-presented in §4 so Daniel can make the residual calls before
anything is scoped.
banks. This began as a discussion-shaping doc; with all forks now settled it has become
the **product framing behind a scoped phase**. Its build roadmap lives in **PLAN.md
§Phase S** and its authoritative spec in **CONTEXT.md §Phase S** — this doc holds the
*why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the settled
decision record).
Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r3)**. Prior
revisions (a) established that a REAPER *extension* cannot be a MIDI instrument, and (b)
corrected a material omission — REAPER's **VST-host bridge**, which lets a VST3 plugin
*hosted inside REAPER* call back into REAPER's own API by resolving function pointers by
name over the host callback. This revision folds in **Daniel's locked decisions** (D1,
D5, D6 — see the box below), retires the now-settled forks in place (marked **DECIDED**,
with the reasoning preserved, not deleted), and adds a substantive, honest assessment of
**what complying with the VST3 surface actually costs without JUCE** (§1a). Grounded in
Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r4)**. The
"no PLAN.md footprint" era is **over** — with D-A through D-D settled (below), the
instrument was scoped into **Phase S** (codename Daniel's: "S" for Sampler, because "D"
collides with the existing Design View phase). **PLAN.md §Phase S is now the
authoritative roadmap; CONTEXT.md §Phase S is the authoritative spec.** This doc is the
framing/decision record they point back to. Prior revisions (a) established that a REAPER
*extension* cannot be a MIDI instrument, (b) corrected a material omission — REAPER's
**VST-host bridge**, which lets a VST3 plugin *hosted inside REAPER* call back into
REAPER's own API by resolving function pointers by name over the host callback — (c) [r3]
folded in **Daniel's locked decisions** (D1, D5, D6) and added the honest **bare-VST3
assessment** (§1a). This revision [r4] records **Daniel's calls on the four residual
forks D-A..D-D** (all settled 2026-07-26) and points to the now-live phase docs.
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`), REAPER's
published VST-extensions SDK page (`reaper.fm/sdk/vst/vst_ext.php`), and the Steinberg
@@ -515,14 +521,27 @@ the thin shell. Confirmed clean for native, and — importantly — **invariant
bare-vs-JUCE** (§1a): the shell choice doesn't touch the core. This is the most
ReaSampler-native way to build it and it's assumed, not debated, going forward.
### Residual forks — Daniel's to call, in priority order
### Residual forks — SETTLED (Daniel, 2026-07-26; reasoning preserved, not re-opened)
Four decisions remain live. Ordered by how much they gate the rest.
All four residual decisions are now called. Each is marked **SETTLED** with Daniel's
choice and the reasoning kept as the record of *why* — do not re-litigate. They are
scoped into **PLAN.md §Phase S** / **CONTEXT.md §Phase S**.
**D-A (highest) — Bare Steinberg VST3 SDK vs. JUCE (with the UI-toolkit sub-question).**
This is now *the* central fork, and §1a is the honest assessment feeding it. The
sub-question — **who draws the editor?** — is really the whole fork, because §1a shows the
audio-processing scaffolding is bounded either way.
**D-A — SETTLED: bare Steinberg VST3 SDK + LICE editor (no JUCE).** *(The central fork.
§1a is the assessment that fed it. The sub-question — who draws the editor? — was the
whole fork, because §1a showed the audio-processing scaffolding is bounded either way.)*
Daniel took **Option A**: bare Steinberg SDK, `SingleComponentEffect` for the bounded
audio scaffolding, editor drawn in the **same LICE/SWELL stack `bank_panel` already
uses** — no JUCE dependency, no AGPL-or-pay posture, house-consistent UI. The
`IPlugView`↔LICE bridge (the one real unknown) is **not** a gate on the decision (the
decision is made) but remains the right *first implementation step*: it is scheduled as
**Phase S's opening spike (S1)** to convert §1a's experienced-estimates — Windows
module-export names, factory-macro spellings, exact bridge marshalling — into verified
fact before the engine build leans on them. (The stale §1a "is VSTGUI even bundled"
question is dropped as moot under Option A; VSTGUI remains a noted fallback rung only if
the LICE bridge proves gnarlier than the panel work suggests, with JUCE the last resort
behind that.) The full option analysis (A/B/C, the JUCE license posture, the LICE-bridge
caveat) is preserved below as the record.
- *Option A — bare Steinberg SDK + LICE editor.* Take only the VST3 SDK (permissive,
royalty-free, no revenue gate). Use `SingleComponentEffect` for the ~200400 lines of
@@ -552,58 +571,66 @@ conceding to a full framework. **Concrete ask: greenlight a small `IPlugView`↔
before committing** — it's the one unknown that decides A vs. B, and §1a's LICE-bridge and
Windows-export-name claims are experienced estimates that a spike would convert to fact.
**D-B — The seam fields: what becomes a bank intrinsic (§2, old D4).** Where the mapping
lives: (i) bank-owned map, (ii) instrument-owned map, or (iii) split — file-*intrinsics*
(root note, loop points) in the bank; *performance-map* (zones, velocity layers,
round-robin, envelopes) in the instrument. *My lean, unchanged:* **(iii) split.** It
mirrors ReaSampler's capture/placement instinct — root note and loop points are *facts
about the captured file* (like sample rate, length, peaks) and belong in the bank;
zones/layers/envelopes are a *performance choice* and belong in the instrument. It keeps
the bank a clean library and makes the bank-index change **small and additive** (root note
+ loop points — the same shape as adding `provenance` in M1). The live-state seam (§2)
means the instrument reads even the performance-map out of shared `"reasampler"` state, so
"who owns which field" is a data-ownership decision, not a transport one. *The one thing to
settle early:* whether root note / loop points get added to `Sample` **now** (so capture
starts populating them and no backfill cliff opens later) or deferred — this is the "design
the seam even if you defer the feature" instinct, and it's cheap to add the fields early.
**D-B — SETTLED: split seam (option iii), and the intrinsic fields are added NOW.**
*(The mapping-ownership fork, old D4. Options were: (i) bank-owned map, (ii)
instrument-owned map, or (iii) split.)* Daniel took **(iii) split** — and, critically,
called that the `Sample` intrinsic fields land **now**, not deferred. **Root note + loop
points become bank intrinsics on `Sample`** (facts about the captured file, like sample
rate/length/peaks) — a small additive change, the same shape as adding `provenance` in
M1. **Zones, velocity layers, round-robin, envelopes are the instrument's performance
map** (a creative arrangement, not a file fact). The live-state seam (§2) means the
instrument reads even the performance-map out of shared `"reasampler"` state, so "who owns
which field" is a data-ownership decision, not a transport one. Adding the fields now
closes the **backfill cliff** (the *design-the-seam-even-if-you-defer-the-feature*
instinct, same as Fork R-D's owned-file manifest): every sample captured before the
fields exist would otherwise lack a root note / loop points and need hand-backfilling. The
field addition touches the **extension** codebase, is independently shippable, and is
scheduled as an **early Phase S point (S2)** ahead of the instrument that consumes it.
**D-C — Tier scope to commit to first (§3).** Tier 0 ("the bank plays" — one sample,
chromatic, amp envelope, velocity→volume) / Tier 1 ("a keymap" — zoned multisamples with
per-sample root notes) / Tier 2 ("expressive" — velocity layers, round-robin, ADSR, loops)
/ Tier 3 ("polish" — filters, LFOs, choke groups). *My lean:* **scope Tier 01 first** as
the "does this belong in ReaSampler's world" proof; hold Tier 2 as the "becomes a tool
people reach for" follow-on; treat Tier 3 as optional-forever. Don't let a Tier-3 feature
list inflate the D-A decision — the editor and DSP needs of Tier 01 are modest, which
further weakens the "need JUCE" pull.
**D-C — SETTLED: Tier 01 scoped now; Tier 2 held; Tier 3 optional-forever.** *(The
tier-scope fork, §3. Tiers: 0 "the bank plays" / 1 "a keymap" / 2 "expressive" / 3
"polish".)* Daniel took the recommended scope: **Tier 0** ("the bank plays" — one sample,
chromatic, amp envelope, velocity→volume) then **Tier 1** ("a keymap" — zoned multisamples
with per-sample root notes) are the committed scope and the "does this belong in
ReaSampler's world" proof (Phase S points S4/S5). **Tier 2** ("expressive" — velocity
layers, round-robin, ADSR, loops) is held as an explicit follow-on — **noted, not
specified** (its points are not drawn up). **Tier 3** ("polish" — filters, LFOs, choke
groups) is optional-forever. The Tier-01 editor and DSP needs are modest, which is part of
why the D-A "no JUCE" call is comfortable.
**D-D (deferred by default) — Embedded TCP/MCP UI.** A REAPER-hosted VST3 can draw its own
UI *inline in the track/mixer control panel* (`reaper_plugin_fx_embed.h`: VST3 implements
`IReaperUIEmbedInterface`; the same Cockos surface REAPER's own JS/embedded FX use). A
native ReaSampler instrument could render a compact keymap/level strip inline in the
TCP/MCP, not only in its own window — and since this uses the same LICE-class drawing as
Option A above, it composes naturally with the bare-SDK-plus-LICE path. **Pure polish, not
a Tier-0 need — deferred by default.** Flagged only because it's a lever that exists on
this exact (native-in-REAPER, LICE-drawing) path and nowhere else, so if "deeply woven into
REAPER's UI" ever becomes the vision, the groundwork is already the groundwork.
**D-D — SETTLED: embedded TCP/MCP UI is SCHEDULED (not deferred).** *(The
inline-in-REAPER-UI lever.)* A REAPER-hosted VST3 can draw its own UI *inline in the
track/mixer control panel* (`reaper_plugin_fx_embed.h`: VST3 implements
`IReaperUIEmbedInterface`; the same Cockos surface REAPER's own JS/embedded FX use) — a
compact keymap/level strip inline in the TCP/MCP, not only in its own window. Because it
uses the **same LICE-class drawing as the D-A editor path**, it composes naturally with the
bare-SDK-plus-LICE build. **Daniel's call: schedule this, don't defer it** — it is a real,
in-phase later point on the Phase S roadmap (**S6**), sequenced *after* the main
`IPlugView` editor exists (it composes with that LICE path), not a someday-note. It is
polish rather than a Tier-0 need, so it sequences last — but it is on the roadmap.
---
## What this doc is asking for
## Where this landed
With D1/D5/D6 locked, the decision this doc now puts to Daniel is a single ordered set:
With D1/D5/D6 locked and **D-A..D-D all settled (2026-07-26)**, the instrument is scoped
into **Phase S** — a native VST3 sampler as a **second build artifact** alongside the
`reaper_reasampler` extension. The settled set:
1. **D-A bare Steinberg SDK + LICE editor, or JUCE?** (My lean: bare SDK + LICE, gated on
a small `IPlugView`↔LICE spike. This is the one that decides the instrument's whole
build shape and dependency posture.)
2. **D-B — split seam, and do root note / loop points get added to `Sample` now?** (My
lean: yes, split; add the intrinsic fields early to avoid a backfill cliff.)
3. **D-C — commit Tier 01 first?** (My lean: yes; hold 2, defer 3.)
4. **D-D embedded TCP/MCP UI:** deferred by default; note only.
1. **D-A bare Steinberg VST3 SDK + LICE editor** (no JUCE). The `IPlugView`↔LICE bridge
is the opening implementation spike (**S1**), not a decision gate.
2. **D-B → split seam; root note + loop points added to `Sample` now** (**S2**, in the
extension) to close the backfill cliff.
3. **D-C → Tier 01 committed** (**S4/S5**); Tier 2 held (noted, not specified); Tier 3
optional-forever.
4. **D-D embedded TCP/MCP UI scheduled** (**S6**), after the main editor exists — on the
roadmap, not deferred.
Once D-A and D-B are called, this becomes concrete enough to write an actual phase spec
(the instrument as its own artifact, with the pure sampler core as a testable module
mirroring `bank_model`). Until then it stays a framing doc with **no PLAN.md footprint**
deliberately, so nothing is scoped until the build shape (D-A) is chosen.
**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1S6, sequenced by
dependency order: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded
UI); **CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the
pure/shell split in the new artifact, the must-verify SDK/bridge surfaces). This doc is the
framing/decision record they point back to. The "no PLAN.md footprint" era is over.
---