6659c2dbdf
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.
456 lines
30 KiB
Markdown
456 lines
30 KiB
Markdown
# 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, 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.
|
||
|
||
Status: framed by product-designer (2026-07-26), **revised 2026-07-26** to correct a
|
||
material omission — the earlier draft missed REAPER's **VST-host bridge**: a VST/VST3
|
||
plugin *hosted inside REAPER* can call back into REAPER's own API from within the
|
||
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.**
|
||
|
||
---
|
||
|
||
## The need, stated plainly
|
||
|
||
ReaSampler today **captures and organizes** audio into per-project banks (on-disk
|
||
32-bit float WAVs + a JSON index, multi-bank via `bank_book`). It has **no way to
|
||
play those samples back from a MIDI keyboard.** The panel can *audition* a sample on
|
||
click — but a one-shot, one-voice audition is not an instrument. There is no
|
||
key→sample mapping, no velocity response, no polyphony, no envelope, no round-robin.
|
||
|
||
The ask: *what would let me play my captured banks MIDI-triggered, and what does the
|
||
REAPER surface actually allow toward that?*
|
||
|
||
The rest of this doc is organized as the task framed it: (1) the plugin-format
|
||
reality, honestly; (2) the integration seam between capture and playback; (3) product
|
||
shape and scope tiers; (4) the forks that are Daniel's to call.
|
||
|
||
---
|
||
|
||
## 1. The plugin-format reality (the honest part)
|
||
|
||
**The load-bearing fact: a REAPER extension cannot be a MIDI-triggered instrument.**
|
||
This is not a limitation we can engineer around by trying harder — it is what the
|
||
plugin *format* is. ReaSampler today is a `reaper_*.dll` loaded at startup via the
|
||
extension API (`ReaperPluginEntry`). That format gives an extension enormous reach
|
||
over the *project* — tracks, items, actions, rendering, project state — but it is
|
||
**not a node in any track's audio signal chain.** An instrument track's "read live
|
||
MIDI, emit audio per-voice" contract belongs to VST/VST3/CLAP/JSFX plugins, which
|
||
REAPER hosts through an entirely different mechanism.
|
||
|
||
What the extension SDK *does* expose near audio/MIDI, verified in the headers, and why
|
||
none of it is "be an instrument":
|
||
|
||
- **`Audio_RegHardwareHook` + `audio_hook_register_t::OnAudioBuffer`** (`reaper_plugin.h`
|
||
~1228). This is a real-time callback into the audio thread, called pre- and
|
||
post-REAPER-processing, handing you `GetBuffer(isOutput, idx)` for the **hardware
|
||
I/O** channels. It is a *hardware tap*, not a track insert. You could technically
|
||
mix sample playback into the hardware output buffer here — but you'd be bypassing
|
||
REAPER's entire routing, metering, recording, and rendering path. Output from this
|
||
hook **cannot be recorded, rendered, routed, or FX-processed** as track audio. It is
|
||
the wrong tool for an instrument by construction, and it runs in the audio thread
|
||
with all the real-time-safety hazards that implies. Rejected on sight for this
|
||
purpose — noting it only to be complete.
|
||
|
||
- **`kbd_OnMidiEvent` / `kbd_OnMidiList`** (`reaper_plugin_functions.h` ~4138). REAPER
|
||
*informs* the extension of MIDI events (for action-triggering — this is how MIDI can
|
||
fire the capture/insert actions we already plan to bind). It is **notification, not a
|
||
routable MIDI input** feeding a synth voice allocator. Great for "MIDI note fires the
|
||
capture-to-slot action"; useless as an instrument's note input.
|
||
|
||
- **`PlayPreview` / `PlayTrackPreview` + `preview_register_t`** (`reaper_plugin.h`
|
||
~1302). This is REAPER's **preview-playback** mechanism, and **ReaSampler already
|
||
uses it** — `bank_panel.cpp` streams a bank WAV through a caller-owned
|
||
`preview_register_t` for click-audition (`PCM_Source_CreateFromFile` +
|
||
`PlayPreview`). This is the one audio-emitting surface the extension already drives,
|
||
and it is load-bearing for the hybrid option below. But note its shape: it plays *a*
|
||
source at *a* position with *a* volume — it is a fire-and-forget preview, **not a
|
||
polyphonic, per-note, velocity-scaled, envelope-shaped voice engine.** You do not get
|
||
MIDI→voice allocation for free; you'd be building a sampler engine on top of a
|
||
preview primitive.
|
||
|
||
- **`pcmsrc` registration + `PCM_source` subclassing** (`reaper_plugin.h` ~804, the
|
||
`PCM_SOURCE_EXT_*` surface). An extension *can* register a custom `PCM_source` type.
|
||
This is the surface REAPER itself and extensions like SWS use for custom media. It is
|
||
a real seam — but it produces a *media source* (something an item plays), not a
|
||
*live-MIDI instrument*. Interesting for "a bank-backed media source" but not the
|
||
keyboard-instrument ask.
|
||
|
||
**Conclusion, stated for the record:** to get a genuine MIDI-triggered instrument —
|
||
play a note, hear the mapped sample, velocity-scaled, polyphonic, with an envelope —
|
||
the playback engine must be a **standard instrument plugin** (VST3 / CLAP / JSFX),
|
||
*not* the extension. The extension SDK is the wrong format for that job, and this is
|
||
the single most important thing for Daniel to internalize before scoping anything.
|
||
|
||
### The three honest options
|
||
|
||
**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
|
||
that the user instantiates on an instrument track. It maps samples across the keyboard
|
||
and plays them MIDI-triggered with a real voice engine. This is the "sophisticated
|
||
sampler" answer.
|
||
- *Gives you:* everything an instrument is — polyphony, velocity, envelopes, the works,
|
||
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
|
||
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,
|
||
its own build/release/signing story, its own UI toolkit. This is a **product-sized
|
||
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
|
||
(text `.jsfx` files, JIT-compiled by REAPER, hostable as an instrument). A JSFX
|
||
instrument *can* receive live MIDI and emit audio on a track. It can load samples
|
||
(`Xen`-style file reads / the JSFX file/serialize API) and play them back.
|
||
- *Gives you:* a real in-track instrument with **zero new binary, zero new SDK, zero
|
||
JUCE** — ships as a text file alongside the extension, cross-platform for free
|
||
(REAPER runs the JIT everywhere it runs).
|
||
- *Costs:* JSFX is a constrained DSP scripting language, not C++. A polyphonic
|
||
multisample engine with round-robin/velocity-layers/streaming is *doable* but you're
|
||
writing DSP in JSFX's idiom, and large-sample streaming / disk I/O is more awkward
|
||
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
|
||
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
|
||
as the sole owner of **capture + organization** (which is its whole existing identity
|
||
and the load-bearing "capture and placement are separate acts" principle). Add a
|
||
**separate instrument** (Option A *or* B) that **consumes the banks** as a
|
||
shared-artifact contract. The extension never becomes an instrument; the instrument
|
||
never captures. Each does what its format is good at. This is the honest shape of the
|
||
whole thing — the two options above are really "which instrument technology" *within*
|
||
the hybrid, because the extension is staying regardless.
|
||
|
||
The real fork, then, is **not** "extension vs. plugin" (the extension stays either
|
||
way) — it is **"which instrument format consumes the banks: JSFX or native VSTi/CLAP,"**
|
||
and **"how much sampler do we actually want."** Those are §3 and §4.
|
||
|
||
---
|
||
|
||
## 2. The integration seam
|
||
|
||
The prior draft assumed one seam: **the bank folder (project-relative WAVs) + the
|
||
bank/index JSON on disk**, with the playback engine as a file-reader. That's still the
|
||
seam for *sample audio* (WAVs are files; there's no getting a live PCM stream across the
|
||
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
|
||
model): id, display name, relative path, source range, channel count, sample rate,
|
||
length, capture tempo, an **optional key**, peak/RMS/LUFS, content hash, tier,
|
||
provenance, timestamp. Notably it *already* has an optional key field and capture
|
||
tempo — the seeds of pitch-mapping are there.
|
||
|
||
**What a playback engine additionally needs** — none of which the index carries today:
|
||
|
||
- **Root note** (the pitch the sample was recorded at, so it can be repitched across
|
||
the keyboard). The optional `key` is close but is "musical key," not "root MIDI
|
||
note" — different thing.
|
||
- **Key range / zone** (low note, high note) — which keys trigger this sample.
|
||
- **Velocity layers** (which sample plays at which velocity band; a mapping of samples
|
||
to velocity zones).
|
||
- **Round-robin groups** (cycle through N samples on repeated same-note hits).
|
||
- **Loop points** (sustain loop start/end for held notes; sample-accurate,
|
||
zero-crossing-aware — CONTEXT already flags loop/zero-crossing handling as
|
||
day-one-relevant for wavetable material).
|
||
- **Amplitude envelope** (ADSR) and optionally filter/pitch envelopes.
|
||
- **Tuning/gain trim** per sample.
|
||
|
||
**The design decision this forces:** does the *mapping* (key ranges, velocity layers,
|
||
round-robin, envelopes) live —
|
||
|
||
- **(i) in the bank index** (ReaSampler owns the instrument definition; the plugin is a
|
||
dumb player of a ReaSampler-authored map), or
|
||
- **(ii) in the instrument plugin's own state** (the plugin owns the map; the bank is
|
||
just a WAV+metadata pool it maps *over*), or
|
||
- **(iii) split** — the bank carries per-sample *intrinsics* (root note, loop points —
|
||
facts about the file), and the instrument carries the *arrangement* (zones, layers,
|
||
envelopes — a performance choice)?
|
||
|
||
**(iii) is the principled answer and worth leading with.** It mirrors ReaSampler's own
|
||
deepest instinct — the *capture/placement separation*. Root note and loop points are
|
||
**facts about the captured file** (analogous to sample rate, length, peaks) and belong
|
||
in the bank, computed or set at/after capture. Key zones, velocity layers, envelopes,
|
||
round-robin are a **performance mapping** — a creative arrangement of those facts — and
|
||
belong in the instrument. This keeps the bank a clean, portable, tool-agnostic library
|
||
(a bank is still just WAVs + facts, readable by anything) while the instrument owns the
|
||
opinionated part. It also means **the bank index grows only by file-intrinsic fields**
|
||
(root note, loop points), which is a small, safe, additive change — exactly the kind of
|
||
seam ReaSampler already knows how to add (mirror of how `provenance` was added as a
|
||
field in M1 and populated later).
|
||
|
||
**A note on format (revised for the bridge):** the friction is asymmetric, and more so
|
||
than the prior draft said.
|
||
- *Native (Option A):* doesn't just "read the bank JSON off disk" — it can read the map
|
||
out of **live project ext-state** via the bridge (whichever of i/ii/iii you choose, the
|
||
extension and instrument can share the *same* `"reasampler"` state rather than one
|
||
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 (partly dissolved on the native path).** The bank is project-relative
|
||
and travels with the `.rpp` (M4 machinery). Any *file-seam* consumer must resolve paths
|
||
the same way — so a JSFX instrument still needs to be told the current project bank folder
|
||
(saved in its own state), a real wiring detail, don't hand-wave it. But a **native**
|
||
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.
|
||
|
||
---
|
||
|
||
## 3. Product shape & scope tiers
|
||
|
||
Sketched as tiers, minimal → sophisticated. The point of tiering is that **Tier 0
|
||
delivers the core promise** ("play my captured banks from a MIDI keyboard") and each
|
||
tier above is optional depth, not a prerequisite for the one below.
|
||
|
||
**Tier 0 — "the bank plays."** One sample mapped chromatically across the keyboard from
|
||
a root note; monophonic-or-basic-polyphony; a simple amp envelope; velocity → volume.
|
||
Point one bank sample at a MIDI track and play it repitched. *This is the smallest
|
||
thing that delivers the promise* and is the honest MVP. On the JSFX path this is a
|
||
genuinely small build; on the native path it's the skeleton of the plugin.
|
||
|
||
**Tier 1 — "a keymap."** Multiple samples zoned across the keyboard (key ranges), each
|
||
with its own root note. Now a captured *kit* (several one-shots) or a *multisampled
|
||
instrument* (same instrument sampled at several pitches) plays correctly. This is where
|
||
the **root-note + key-range** seam fields (§2) earn their place. Still one sample per
|
||
key-region.
|
||
|
||
**Tier 2 — "expressive."** Velocity layers (soft vs. hard hits pick different samples),
|
||
round-robin (repeated hits cycle samples — the anti-machine-gun feature), full ADSR,
|
||
per-sample tuning/gain trim, sustain loops for held notes. This is a *real* sampler and
|
||
where "sophisticated" lives. It's also where the mapping data (§2) gets rich enough that
|
||
the "who owns the map" fork (i/ii/iii) really bites.
|
||
|
||
**Tier 3 — "instrument polish."** Filter + filter envelope, LFOs, per-voice pan, choke
|
||
groups (hi-hat open/closed), maybe a modest built-in FX slot. Standard sampler feature
|
||
creep. Explicitly *not* needed to deliver value; a direction to leave room for, not
|
||
commit to.
|
||
|
||
**The recommended reading:** Tier 0–1 is the "does this even belong in ReaSampler's
|
||
world" proof. If the answer is yes, Tier 2 is where it becomes a tool people reach for.
|
||
Tier 3 is optional forever. **Scope the decision at Tier 0–1 first** and treat 2/3 as
|
||
held futures — don't let a Tier-3 feature list drive the format choice.
|
||
|
||
### The one-source-multiple-views angle
|
||
|
||
Worth flagging because it recurs in Daniel's work: the bank is **one source** (WAVs +
|
||
facts). The panel is one view (organize/audition). A MIDI instrument is **another view
|
||
of the same source** (play). That's a clean framing — the instrument doesn't fork the
|
||
bank, it consumes it — and it argues for keeping the bank as the single authoritative
|
||
artifact and the instrument as a pure consumer (seam option ii/iii, never "the
|
||
instrument keeps its own copy of the samples").
|
||
|
||
---
|
||
|
||
## 4. Risks & open decisions — Daniel's to call
|
||
|
||
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
|
||
central fork. JSFX = no new binary, no JUCE, cross-platform free, real ceiling on
|
||
sophistication, awkward bank-JSON reading, **and no VST-host bridge** (file-seam only).
|
||
Native = full sophistication, clean integration, **plus the bridge**: live `"reasampler"`
|
||
ext-state, project-awareness, embedded TCP/MCP UI (see below) — at the cost of a whole
|
||
second codebase in a second format with its own build/release/dependency/signing story,
|
||
**and REAPER-coupling if it leans on the bridge.**
|
||
|
||
*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 0–1, **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
|
||
project today is a clean C++ extension with two vendored submodules and a proud
|
||
pure-core/shell discipline. A native instrument means a *third* major dependency and a
|
||
second build target of a fundamentally different kind. That's a real architectural
|
||
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."
|
||
|
||
*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
|
||
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,
|
||
testable core** this project excels at — they could live in a REAPER-free, DAW-free,
|
||
unit-tested module (mirror of `bank_model`/`peaks`/`view_mode_model`) with the plugin
|
||
format (JSFX or VST3 wrapper) as the thin shell around it. **This is the most
|
||
ReaSampler-native way to build it** and I'd argue strongly for it regardless of D1: the
|
||
sampler DSP core is pure and tested; the format is a shell. The open question is whether
|
||
that discipline can hold across a *different plugin format* — with native it's clean C++
|
||
so yes; with JSFX the "pure core" would be JSFX code, harder to unit-test in the CTest
|
||
harness. That tension is real and feeds back into D1.
|
||
|
||
**D4 — Where the mapping lives (seam fork i/ii/iii from §2).** Bank-owned map,
|
||
instrument-owned map, or split (file-intrinsics in the bank, performance-map in the
|
||
instrument). *My lean:* **(iii) split** — it's the one that honors ReaSampler's
|
||
capture/placement instinct and keeps the bank tool-agnostic. But it's Daniel's call
|
||
whether ReaSampler should author instrument definitions at all, or stay purely a
|
||
sample library that a *separate* mapping tool arranges.
|
||
|
||
**D5 — Cross-platform.** The extension is already cross-platform (SWELL). JSFX inherits
|
||
that for free. A native plugin re-opens the full cross-platform DSP + UI + build matrix
|
||
(Win/mac/Linux, code-signing on mac, etc.) as a *separate* artifact. A cost that lands
|
||
entirely on the native path.
|
||
|
||
**D6 — Is this even one product?** The reframe worth surfacing: ReaSampler's thesis is
|
||
"a precision *capture/organize* tool; capture and placement are separate acts." A
|
||
MIDI-playback instrument is a *different act* — playback. There's a legitimate reading
|
||
where the instrument is a **companion product** that shares the bank format, not a
|
||
feature *of* ReaSampler — the way a sample library and a sampler that reads it are
|
||
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.
|
||
The alternative reading — it's all one integrated sampler-workstation — is also
|
||
coherent.
|
||
|
||
*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.
|
||
|
||
---
|
||
|
||
## What this doc is asking for
|
||
|
||
A direction on the two highest-order forks, in order:
|
||
|
||
1. **D6 — one product or two?** Is the instrument a feature of ReaSampler, or a
|
||
companion product sharing the bank format? Everything else sits under this.
|
||
2. **D1 — JSFX-first prototype, or straight to native?** Given a target of Tier 0–1 to
|
||
start, and the pure-core-as-shell discipline (D3) held either way.
|
||
|
||
Once those two are called, the seam fields (§2, D4) and the tier scope (§3) become
|
||
concrete enough to write an actual phase spec. Until then this stays a framing doc with
|
||
no PLAN.md footprint — deliberately, so we don't scope an instrument before deciding
|
||
whether we're building one.
|