Files
reasampler/docs/product/midi-playback.md
T
daniel 8c9cf38780 docs(product): scope ReaSampler 9000 UX overhaul (S10-S13) + record instrument name
Post-DAW-test: name the instrument ReaSampler 9000; spec the better-than-RS5K
overhaul — S10 keyboard-strip keymap editor, S11 waveform + draggable loop
points, S12 scroll/search/direct-entry/ADSR, S13 drop-to-load via the S8 relay.
VST3 class UID locked unchangeable; S-NAME-1 (binary filename) is Daniel's call.
2026-07-27 04:09:49 -04:00

52 KiB
Raw Blame History

MIDI playback — opportunity & design-space framing

Framing for a MIDI-triggered audio sampler that plays back ReaSampler's captured 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 (r5). r5 records the post-DAW-test directives on the S1S6 instrument: the product name ReaSampler 9000 and the "better than RS5K" UX overhaul (Phase S points S10S13) — see the Addendum in §4. r4 (below) settled the four residual forks D-A..D-D. 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 VST3 SDK documentation (portal + class reference — cited inline in §1a). Where a claim is experienced estimate rather than a verified-from-source fact, it is flagged as such in §1a. The genuine remaining forks are flagged as Daniel's to decide; nothing here pre-decides them.

Locked decisions (SETTLED — do not re-present as open)

Daniel has nailed these down. Everything downstream assumes them.

  • D6 → DECIDED: two products, but integrated. The instrument is a separate product/artifact from the ReaSampler extension, but tightly integrated via the VST-host bridge — it reads live "reasampler" project ext-state and is project-aware. Not a divorced, file-only companion. (The old §4 D6 reasoning is preserved below, marked decided.)
  • D1 → DECIDED: native VST3. JSFX is off the table. The instrument is a native VST3 plugin. (The old JSFX option and its whole-doc entanglement are retired below, marked decided; the JSFX reasoning is kept as the record of why it was considered and set aside.)
  • D5 → DECIDED: no cross-platform, no multiformat. REAPER-specific, Windows-only. The existing extension is Windows-only; Daniel does not work on other platforms. So: no mac/Linux DSP/build/signing matrix, no CLAP-for-portability argument, no "runs standalone in other hosts" concern. REAPER-coupling via the bridge is fine and intended. This collapses several costs the prior draft carried (§1a, §4).

What these lock-downs do to the shape: the doc is no longer weighing "extension vs. plugin," "JSFX vs. native," or "portable vs. coupled." It is weighing how to build one native, Windows-only, REAPER-coupled VST3 instrument — and the single biggest live question inside that is now bare Steinberg VST3 SDK vs. JUCE (§1a, D-A below).

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 itbank_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 shape, now that D1/D6 are locked

The old three-way option set (native VSTi / JSFX / hybrid) has collapsed to a single resolved shape:

A separate native VST3 instrument that consumes ReaSampler's banks, tightly integrated via the bridge. The extension stays the sole owner of capture + organization (its whole existing identity and the load-bearing "capture and placement are separate acts" principle). The instrument is a separate build artifact — a native VST3 sampler the user instantiates on an instrument track — that maps samples across the keyboard and plays them MIDI-triggered with a real voice engine. It reaches ReaSampler's project state directly via the VST-host bridge: it reads the live "reasampler" ext-state persist writes, knows its own host project, and follows the active project. This is the "two products, but integrated" shape D6 locked in. The extension never becomes an instrument; the instrument never captures.

What this gives: everything an instrument is — polyphony, velocity, envelopes, the works, fully in REAPER's routing/render/record path like any VSTi — plus live, project-aware integration with the extension's state, not a divorced file-reader.

What it costs: a second codebase, the Steinberg VST3 SDK (or JUCE) as a dependency, and a real DSP/voice-engine build — a product-sized commitment, not a feature. The Windows-only + VST3-only + REAPER-only lock-downs (D5) remove the costs the prior draft carried around cross-platform DSP, code-signing, and multi-format wrappers — a material simplification. The remaining cost question is almost entirely "how much scaffolding does the VST3 surface demand, and do we take JUCE to get it" — answered honestly in §1a.

JSFX — retired (D1 DECIDED: native VST3). Prior drafts weighed a JSFX sampler (REAPER's built-in scriptable format: zero new binary, zero SDK, cross-platform free) as the cheap-prototype path. It is off the table. The reasons it lost, for the record: (1) JSFX is a constrained DSP scripting language — a polyphonic multisample engine with round-robin/velocity-layers/streaming is doable but fights the idiom, and large-sample disk streaming is awkward; (2) JSFX gets no VST-host bridge, so it could never read persist's live "reasampler" ext-state — it would be a permanently file-coupled consumer needing a sidecar seam, which is exactly the loose companion D6 rejected; (3) its "pure core" would be JSFX code, outside the CTest harness (see D3). With D5 locking Windows-only, JSFX's one real edge — cross-platform-for-free — is worth nothing here. Native VST3 wins cleanly given the locks.


1a. How crazy is bare VST3 without JUCE? (the honest assessment)

Daniel's question, directly: how crazy is it to comply with the VST3 surface without something like JUCE? Short answer: not crazy — the audio-processing side is a few-hundred-lines-of-ceremony-you-write-once problem, not a tar pit. The one genuine question is the editor UI, and ReaSampler is unusually well-positioned to answer it without JUCE. The detail, honestly, with estimate-vs-verified flagged.

What the raw Steinberg VST3 SDK actually demands

A working VST3 instrument must present these interfaces (all VST3 interfaces descend from FUnknown, a COM-like base with queryInterface / addRef / releaseverified, Steinberg VST3 SDK class reference):

  • IComponent — the plugin's identity and bus/state setup: initialize, setActive, getBusCount / getBusInfo, activateBus, setState / getState.
  • IAudioProcessor — the DSP contract: setBusArrangements, setupProcessing (ProcessSetup: sample rate, block size, symbolic sample size), setProcessing, and the hot path process(ProcessData&) — where you read MIDI events off the event input bus and write audio to the output bus. For an instrument you declare an event input bus (MIDI in) and an audio output bus, no audio input. (Verified: IAudioProcessor reference; the instrument bus topology is standard.)
  • IEditController — parameter model + editor: getParameterCount / getParameterInfo, getState / setState, setComponentState, normalized↔plain parameter conversion, and createView("editor") returning an IPlugView if you have a GUI.
  • IPluginFactory (via the module's exported GetPluginFactory) — enumerates the classes the module offers (the processor and, in the two-component model, the controller), keyed by class UIDs. On Windows the module also exports InitDll / ExitDll (bundle entry points differ per-OS, but D5 makes Windows the only target, so it's just these two plus GetPluginFactory). (Experienced estimate on the exact Windows export names — the SDK's dllmain.cpp / public.sdk main glue provides these; I have not re-read the header this session, so treat the exact symbol names as to-verify-against-public.sdk/source/main/ before build, not as a load-bearing claim.)

The COM plumbing is real but bounded. queryInterface/addRef/release plus the class-factory macros (BEGIN_FACTORY / DEF_CLASS2 / END_FACTORY, and the DECLARE_FUNKNOWN_METHODS / IMPLEMENT_REFCOUNT helper macros) are provided by the SDK's pluginterfaces and public.sdk layers — you do not hand-write refcounting; you invoke macros. (Experienced estimate on the exact macro names — these are the long-standing VST3 SDK factory macros; verify spelling against the vendored SDK headers at build time.) This is the "ceremony you write once" — it is copy-adapt-from-the-example work, not design work.

And the SDK hands you base classes that absorb most of it. The critical fact for Daniel's question: you do not implement those four interfaces from scratch. The SDK's public.sdk layer provides:

  • AudioEffect (with Component / AudioEffect base) — implements IComponent + IAudioProcessor boilerplate; you override initialize (declare busses), setupProcessing, setActive, setState/getState, and process.
  • EditControllerEx1 — implements IEditController boilerplate; you override parameter registration and state.
  • SingleComponentEffectcombines processor and controller into one class (descends from EditControllerEx1 and the component hierarchy). You override initialize (call addAudioOutput + addEventInput), setupProcessing, and process. (Verified: Steinberg SDK SingleComponentEffect class reference — "default implementation for a non-distributable Plug-in that combines processor and edit controller in one component." The SDK cautions to prefer the two-component split for distributable plugins, but for a REAPER-only, non-distributable instrument (D5/D6), SingleComponentEffect is exactly the sanctioned shortcut and cuts the interface surface roughly in half.)

Honest quantification of the audio side: with SingleComponentEffect + the factory macros, a silent-but-loading VST3 instrument skeleton — factory, class registration, module entry, bus setup, empty process — is on the order of ~200400 lines of adapt-from-example ceremony, written once, then largely untouched. The AGain / Note Expression Synth examples that ship with the SDK are exactly this skeleton and are the copy-source. This is not a tar pit. The tar-pit reputation VST3 has comes from (a) multi-format wrappers (AU/AAX/VST2 — not our problem, D5) and (b) the GUI, addressed next. (Experienced estimate on the line-count band — grounded in the shape of the SDK examples, not a line-counted measurement; treat as an order-of-magnitude honest estimate, not a promise.)

What JUCE buys — and whether ReaSampler needs it

JUCE exists largely to solve problems D5 has already deleted for us. Weighed against the locked constraints:

What JUCE provides Do we need it, given the locks?
Multi-format wrapper (one codebase → VST3/AU/AAX/VST2/standalone) No. Single format, VST3 only, REAPER only. This is JUCE's biggest reason to exist and it's moot here.
Plugin boilerplate / AudioProcessor Marginal. The Steinberg SDK's SingleComponentEffect already absorbs the VST3 boilerplate; JUCE's abstraction sits on top of the same SDK.
Parameter management (AudioProcessorValueTreeState) Nice-to-have. Genuinely convenient, but a sampler's parameter set (envelope, gain, tuning) is small; hand-rolling over IEditController is bounded.
DSP utilities (juce::dsp, filters, oscillators, interpolators) Nice-to-have for Tier 3 (filters/LFOs). For Tier 02 the sampler DSP — repitch interpolation, envelopes, voice allocation — is exactly the pure core we want to write and test ourselves (D3).
Editor/UI framework (juce::Component, graphics, widgets) The one real pull. VST3 gives you IPlugView and no toolkit whatsoever. Something has to draw the editor. This is the crux — see below.

Costs of JUCE, honestly: it is a large dependency (a whole framework, not a library); it brings its own build system (Projucer / CMake integration) and idioms that would sit oddly beside ReaSampler's lean two-submodule CMake discipline; and its licensing is a real standing commitment. JUCE 8 is dual-licensed AGPLv3 or commercial. The free tier (Starter) is usable below ~$20K/yr revenue and — notably in JUCE 8 — no longer requires a splash screen. Above that, Indie ($200K/yr revenue limit) is a paid license ($3,500 as of JUCE 8), and Pro above that. (Verified via web, JUCE forum + license pages — see Sources.) For a personal/ReaSampler-scale tool the free Starter tier likely applies today, but taking JUCE means accepting AGPL-or-pay as a permanent posture on the instrument. That's a bigger standing commitment than the Steinberg SDK, which is a permissive (proprietary-but-royalty-free, GPLv3-optional) license with no revenue gate.

The UI is the real question — and ReaSampler is unusually well-armed

Strip everything else away and the honest "reach for JUCE?" decision reduces to one thing: who draws the editor? VST3 gives you an IPlugView seat and nothing to fill it with. The options on Windows-only (D5):

  1. JUCE — for its Component graphics stack alone. This is the usual reason indie devs take JUCE, and if ReaSampler had no UI competence it'd be the default.
  2. VSTGUI — the UI toolkit that ships with the Steinberg SDK itself. Lighter than JUCE, purpose-built for VST editors, no extra dependency beyond the SDK you already took. A real middle option. (Experienced estimate — VSTGUI is bundled with the VST3 SDK; verify the vendored SDK includes it before relying on it.)
  3. Win32 / GDI / Direct2D directly — Windows-only makes this viable; you own an HWND in the IPlugView.
  4. The same LICE/SWELL stack the extension already uses.This is the one worth staring at for Daniel specifically.

Why option 4 changes the calculus for this project. ReaSampler's bank_panel is already a LICE-drawn UI — a real, working, docked, custom-drawn panel (grid, thumbnails, tab strip, hit-testing) built on LICE/SWELL, the same stack REAPER itself and SWS use. That's 67 LICE call-sites in bank_panel.cpp today. The team (Daniel + the implementers) has already paid the learning cost of drawing a custom audio-tool UI in LICE. The usual "take JUCE because hand-rolling a plugin GUI from nothing is miserable" argument is much weaker here than for a typical indie dev, because ReaSampler is not starting from nothing — it has demonstrated LICE UI competence and a house style. A VST3 IPlugView that hosts a LICE-drawn surface would (a) reuse existing UI muscle, (b) keep the instrument's look consistent with the panel, and (c) avoid the JUCE dependency and its license posture entirely. There's even the embedded-UI affordance (D7 below) that lets a REAPER-hosted plugin draw inline in the TCP/MCP using this same Cockos surface.

The honest caveat: wiring LICE into a VST3 IPlugView (window creation, sizing, event routing from the host into your draw/hit-test loop) is integration work with real edges — you're bridging the SDK's view lifecycle to a LICE HWND/bitmap. It's not free, and it's less trodden than "drop in a JUCE editor." But it is the same class of work ReaSampler already did to dock bank_panel, not a new competence. (Experienced estimate on the difficulty band — grounded in how IPlugView and LICE each work, not a built prototype. Flag: the exact IPlugView↔LICE bridge should be spiked before it's promised in a plan.)

The pure-core discipline (D3) lands cleanly here

Confirmed: the discipline holds, and JUCE-vs-bare doesn't change it. The sampler's voice engine, envelope math, key/velocity mapping, repitch/interpolation, and keymap resolution are pure DSP + data — exactly the kind of REAPER-free, DAW-free, unit-tested core this project already excels at (bank_model / peaks / view_mode_model are the template). They live in a pure module, tested in the CTest harness outside any host. The VST3 wrapper — SingleComponentEffect subclass, bus setup, process marshalling MIDI→core and core→audio-buffer, the IPlugView editor, and the bridge calls that read "reasampler" ext-state — is the thin shell, the only part that touches VST3 or REAPER at all. This is precisely ReaSampler's load-bearing split, transplanted to a new format.

Native VST3 makes this cleaner than the retired JSFX path would have (JSFX's "core" would be JSFX script, un-unit-testable in CTest). And JUCE-vs-bare doesn't move it: whether the shell is a bare-SDK SingleComponentEffect or a juce::AudioProcessor, the pure core underneath is identical, REAPER-free, and tested the same way. The format choice is a shell choice; the core is invariant. That's a reassuring result — it means D-A (bare vs. JUCE, below) can be decided on shell ergonomics and dependency posture alone, without risking the part of the architecture Daniel most cares about.

Bottom line for Daniel's question

Complying with the VST3 surface without JUCE is reasonable, not crazy — the audio-processing scaffolding is bounded, example-driven ceremony that SingleComponentEffect cuts down further, and the D5 lock-downs delete JUCE's biggest justification (multi-format). The only place JUCE earns its weight is the editor UI — and ReaSampler's existing LICE competence makes even that argument weaker than it would be for a typical indie. My honest lean (D-A below): bare Steinberg SDK + LICE editor, with JUCE as the fallback if the IPlugView↔LICE bridge proves gnarlier than the panel work suggests. But it's a genuine fork and it's Daniel's — laid out in §4.


2. The integration seam

There are two seams, and with native VST3 locked (D1) the instrument gets both:

  • File seam (audio, always). The sample audio is WAVs on disk — there's no getting a live PCM stream across the bridge, nor would you want to. WAVs are project-relative and travel with the .rpp (M4 machinery). This is the permanent seam for sample audio.
  • Live-state seam (the mapping / index, via the bridge). For everything that isn't the raw audio — the index, the mapping data, which project's bank is active — 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 the tight integration D6 locked in.

So: audio comes across as files; the mapping comes across as live shared state. The design question below — "is the ext-state/index sufficient, or does playback need mapping it doesn't carry" — is unchanged. What the bridge settles is where that mapping lives: between extension and instrument as live shared "reasampler" state, not a file one writes and the other re-parses.

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).

Wiring note (native, the locked path). The native instance sidesteps the "point me at the right bank folder" problem entirely: via the context callback it asks REAPER which project it's in, then reads that project's bank location straight from "reasampler" ext-state — no user wiring, no saved folder path in the instrument's own state. This is the cleanest possible integration and it's the one D6 chose. The one thing to keep deliberate: the WAV audio still resolves via the project-relative path machinery (M4), so the instrument must resolve those paths the same way persist does — a shared convention, not a hand-wave. Since D5 locks REAPER-only/Windows-only, the old "but it won't run in other hosts" concern is moot by design — the bridge dependency is intended, not a narrowing to regret.


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 native path this is the skeleton of the VST3 plugin — the SingleComponentEffect shell (§1a) plus a single-voice core, with the editor deferrable behind a parameters-only default view.

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 01 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 01 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. Decisions — settled and residual

Settled (DECIDED — reasoning preserved, not re-opened)

These are locked. The reasoning is kept as the record of why, so the choices don't get silently re-litigated.

D1 — Instrument format → DECIDED: native VST3. JSFX is off the table (retirement reasoning in §1, in the JSFX box). Native VST3 gives full sampler sophistication, clean integration, and the VST-host bridge (live "reasampler" ext-state, project-awareness, embedded-UI affordance). The cost — a second codebase in a plugin format — is accepted. The old "JSFX-first cheap prototype" lean is withdrawn: with D5 locking Windows-only, JSFX's one advantage (cross-platform-for-free) is worthless here, and its inability to reach the bridge makes it the wrong tool for the integrated product D6 chose.

D5 — Cross-platform / multiformat → DECIDED: none. Windows-only, VST3-only, REAPER-only. The extension is Windows-only; Daniel does not work on other platforms. This removes costs prior drafts carried: no mac/Linux DSP/build/signing matrix, no multi-format wrapper, no "runs standalone in other hosts" concern, no CLAP-for-portability argument. REAPER-coupling via the bridge is intended, not a narrowing to regret. This is the single biggest simplifier — it is why bare VST3 is reasonable (§1a): most of what makes VST3 painful (multi-format, cross-platform) is deleted.

D6 — One product or two → DECIDED: two products, but tightly integrated. The instrument is a separate artifact from the extension (the extension stays the pure capture/organize tool; the instrument is the playback surface — capture and placement, and now playback, stay distinct acts). But it is not a divorced file-only companion: via the bridge it reads the live "reasampler" project ext-state and is project-aware — "two faces of one tool sharing one state model." The prior draft's open question ("could they only ever share a file?") is resolved: no, and the shared-live-state integration is the chosen shape. The retired reasoning for why the loose-companion reading lost: it only ever looked clean because the prior draft under-weighted the bridge; once the bridge is on the scale, the integrated reading has the stronger technical affordance, and Daniel took it.

D3 — Pure-core discipline across the format boundary → CONFIRMED holds (see §1a). Not a fork so much as a checked assumption: the sampler's voice engine, envelope math, key/velocity mapping, and repitch/interpolation live in a pure REAPER-free, DAW-free, unit-tested core (mirror of bank_model/peaks/view_mode_model); the VST3 wrapper is the thin shell. Confirmed clean for native, and — importantly — invariant under 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 — SETTLED (Daniel, 2026-07-26; reasoning preserved, not re-opened)

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 — 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 once-written audio scaffolding, and draw the editor in LICE/SWELL — the stack bank_panel already uses (67 LICE call-sites today). Pro: no JUCE dependency, no AGPL-or-pay posture, house-consistent UI, reuses existing UI muscle, lean CMake discipline preserved. Con: the IPlugView↔LICE bridge is real integration work with edges (window lifecycle, sizing, event routing) — less trodden than dropping in a JUCE editor, and it should be spiked before it's promised.
  • Option B — JUCE. Take JUCE for its editor framework + parameter management + DSP utilities. Pro: the editor is a solved problem, AudioProcessorValueTreeState is convenient, juce::dsp helps at Tier 3. Con: a large framework dependency with its own build system and idioms sitting oddly beside the two-submodule discipline; and the license posture — AGPLv3-or-commercial, free Starter tier below ~$20K/yr revenue (no splash screen in JUCE 8), Indie ~$3,500 with a ~$200K/yr limit above that. For most of what JUCE solves (multi-format), D5 already deleted the need.
  • Option C — bare SDK + VSTGUI (the SDK's own bundled toolkit). A middle path: no JUCE, but a purpose-built VST editor toolkit instead of hand-bridging LICE. Lighter than JUCE, no extra dependency beyond the SDK. Worth a look if the LICE bridge proves gnarly.

My honest lean: Option A (bare SDK + LICE editor), precisely because ReaSampler is the atypical case where the "take JUCE for the GUI" default is weakest — it already has working LICE UI competence and a house style. JUCE's headline value (multi-format) is moot under D5. Fall back to JUCE (Option B) only if the IPlugView↔LICE spike shows the bridge is genuinely painful; consider VSTGUI (Option C) as the middle rung before conceding to a full framework. Concrete ask: greenlight a small IPlugView↔LICE spike 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 — 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 — 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 — 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.

Addendum — two directions set post-scoping (Daniel, 2026-07-26)

After Phase S was scoped (D-A..D-D), Daniel set two further directions. These are settled directions, not open forks — specced as new Phase S points (S7S9), not re-litigated. Recorded here per the doc's settled-decisions convention.

D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ PLAN.md S7). Captures are often stereo; the current mono downmix is a Tier-0 simplification. The engine gets a per-instance channel-mode toggle (1 mono / 2 stereo) that "works with the REAPER audio bus automatically" — the VST3 declares/negotiates its output bus arrangement (setBusArrangements) so mono/stereo just works in REAPER's routing. Honest scope: this is an S3-core extension, not a shell hack — the core is mono-per-sample by design today, so stereo mode grows a channel dimension (2-channel sample data, per-voice stereo render, per-channel loop/interp). Mono mode keeps the existing downmix path. Cross-mode policy: mono-source-in-stereo → dual-mono; stereo-source-in-mono → downmix (existing). The toggle is instrument-owned per-instance state (D-B: a performance choice, never a bank fact). Sequenced first after the editor/embed work because it touches the engine Daniel smoke-tests.

Ingest routes through the bank — "option 1"; the extension owns ingest (→ PLAN.md S8 + S9). Loading a sample into the sampler is one gesture: capture/import-into-bank

  • auto-assign to the active instance. The extension owns ingest (it has arrange access, Media-Explorer access, and the drop-target surface on its own panels); the instrument stays a read-only bank consumer — it never captures or imports. Sub-parts, with the honest SDK reality verified against the vendored headers:
  • (a) Arrange capture → bank → assign — a one-click action reusing the existing capture path; never inserts a timeline item (capture/placement separation intact).
  • (b) Media Explorer import → bank → assign — the ME surface is thin (OpenMediaExplorer + MediaExplorerGetLastPlayedFileInfo are the whole contract; no enumerate-selected, no ME-drop-handler), so ME import is single-file, pull-on-action, not a push/drop from inside the ME. Spike: does the last-played-file read work for a merely-selected file?
  • (c) Drag-and-drop — REAPER exposes no drag-drop registration API; drop handling is SWELL/Win32 on ReaSampler's own panel HWNDs. Drop onto the VST3 editor window relayed to the extension as a bank-ingest request is a genuine cross-artifact spike, not a promise (drop-onto-panel is the shipped path if it proves gnarly).
  • (d) Recapture / ingest auto-refresh (→ S9) — because instances reference sample ids, a recapture landing under the same id (M10) or an ingest touching the active bank should refresh live instances hands-free. The missing trigger is a bank-generation counter in "reasampler" ext-state: the extension bumps it on any bank-content mutation; the instrument polls it off the audio thread on a safe cadence and calls its existing reloadFromBank() on change (reusing S4's atomic handoff). This seam serves both S8 ingest and M10 recapture.

The genuine spikes flagged (not decisions Daniel owes, just build-time unknowns): the ME merely-selected-file read (b), and the drop-onto-editor cross-artifact relay (c). Both are honestly-flagged as spikes in PLAN.md S8, not promised.

Addendum — product name + UX overhaul (Daniel, 2026-07-26, post-S1S6 DAW test)

Daniel DAW-tested the S1S6 instrument and set two directives. These are settled directions, specced as new Phase S points (S10S13) and a product-name convention — not open forks (the two flagged forks below are the only calls left to Daniel).

The instrument's product name is ReaSampler 9000. The extension stays ReaSampler (capture + organization); the instrument is ReaSampler 9000 (playback). Propagation is a checklist item (PLAN.md §Phase S — product name; CONTEXT.md §Product name): the VST3 class display name string, the IPlugView editor title band (today "ReaSampler Instrument"), the S6 embed-strip label, and the docs. Compat guard (load-bearing): the VST3 class UID must NOT change — instances in saved projects key off it; a UID change orphans every existing instance. The name change is display-string-only on the code side. Fork S-NAME-1 (Daniel's call): the on-disk binary filename — renaming it (reasampler_9000.vst3) carries compat weight (REAPER keys a saved project's plugin reference partly by filename), so the lean is keep the filename, change only display strings; flagged, not decided.

The UX bar is "better than ReaSamplOMatic5000." Verdict verbatim: "okay it works, but the UX is awful." The S1S6 editor was spike-grade — a clickable list, zone rows with seven ±1 nudge/delete mini-buttons each, text labels, no keyboard, no waveform, no drag, no scroll. Setting a zone range by ±1 clicks is the catastrophe. The overhaul (S10S13) makes "better than RS5K" specific:

  • RS5K's strengths, matched or beaten. Drag-a-file-onto-it load (→ S13 relay); note-range + a visual keyboard (→ S10 — RS5K uses two number fields, so a draggable keyboard strip beats it); waveform with draggable start/end/loop markers (→ S11); ADSR sliders (→ S12). Velocity layers / round-robin stay Tier 2 (held).
  • RS5K's weaknesses, our opening. RS5K is one-sample-per-instance (forcing track sprawl — one instance per drum) with no multi-zone view in a single instance. ReaSampler 9000 is multi-zone in one instrument (S5), so the keyboard-strip editor showing every zone at once is a capability RS5K structurally lacks. That is the sharpest "better than RS5K" claim, and it's free — it falls out of the existing model.

The overhaul honors every settled constraint: LICE/SWELL only (D-A), pure geometry modules for all layout/hit-test (mirror of mode_switch/editor_geometry/embed_strip), RT discipline untouched (edits commit off-thread), the instrument stays a read-only bank consumer (loop/root/ADSR edits are the instrument's performance map, D-B — never written to the bank). Component-state persistence and read-only-over-bank stay settled.

Sequencing (product recommendation). S10 leads — the nudge-button zone editor is the friction Daniel hits on every test pass, so removing it buys the most felt improvement per unit of work and de-risks the drag-state machine S11/S12 reuse. Against the queued S7 (stereo): S10 should land before or interleaved with S7 — S7 is real engine work but the reason Daniel keeps smoke-testing is the editor, and every test pass is currently taxed by the UX; the two are orthogonal (S7 = engine/bus, S10 = editor/geometry), so there is no hard ordering, but the live wound is the editor. Honest counter: if the stereo sound is what blocks real use, S7-first is defensible — but "it works, the UX is awful" names the editor as the wound.


Where this landed

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 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.

Two further directions set post-scoping (2026-07-26; see the Addendum in §4):

  1. D-E → channel mode (mono | stereo), per-instance, bus-negotiated (S7) — an S3-core channel-dimension extension, sequenced first after the editor/embed work.
  2. Ingest through the bank ("option 1"), extension-owned (S8) + bank-generation hands-free refresh (S9) — one-gesture capture/import + assign; the instrument stays a read-only consumer.

Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Addendum in §4):

  1. Product name → ReaSampler 9000 (display-string-only; VST3 class UID unchanged; binary filename = fork S-NAME-1, Daniel's call).
  2. UX overhaul → "better than RS5K" (S10S13): direct-manipulation keyboard-strip keymap editor (S10), waveform + draggable loop points (S11), scale/ergonomics — scroll, search, direct numeric entry, ADSR (S12), and drop-to-load folding in the S8 relay (S13).

Authoritative from here: PLAN.md §Phase S is the roadmap (S1S6 the original dependency chain: spike → Sample fields → pure sampler core → Tier 0 → Tier 1 → embedded UI; then S7 stereo, S8 ingest, S9 change-detection, S10S13 the ReaSampler 9000 UX overhaul); CONTEXT.md §Phase S is the spec (seam-field semantics, scope contracts, the channel-mode / ingest / bank-generation contracts, the UX-overhaul spec, the product-name convention, the pure/shell split, 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.


Sources (for §1a's verified claims)

Estimate-vs-verified honesty note: the interface list, SingleComponentEffect's role, the instrument bus topology, and the JUCE license terms are verified from the sources above. The line-count band (~200400), the exact Windows module-export symbol names (InitDll/ExitDll/GetPluginFactory), the factory-macro spellings (BEGIN_FACTORY/DEF_CLASS2), VSTGUI being bundled in the vendored SDK, and the IPlugView↔LICE bridge difficulty are experienced estimates grounded in how the SDK is shaped — each flagged inline in §1a and each cheap to convert to fact by reading the vendored SDK headers / running a spike before any of it lands in a plan.