a4e33b1c04
Adds S-VIEW-11/12/13 to PLAN, r11 addendum, and forks R11-F1 (window default height) + R11-F2 (Zone-panel knob parity) for Daniel.
1388 lines
99 KiB
Markdown
1388 lines
99 KiB
Markdown
# 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-27 (r11)**. r11 records the
|
||
**Sample-face recomposition** (Daniel's post-landing DAW pass, 2026-07-27): all linear sliders →
|
||
grouped radial knobs, compact mode toggles, the inline velocity-curve box → a preview button +
|
||
popup editor with right-click node delete, and the hero waveform full-width — the layout spec for
|
||
the Wave B editor rebuild. Two new forks open (R11-F1 hero height/default window, R11-F2
|
||
Zone-panel parity); all r9/r10 forks — including S-VIEW-F3 — are settled and landed. See the r11
|
||
Addendum in §4. r9 records the
|
||
**three-view editor redesign** (Sample / Browse / Zone) and its follow-up settlements: the
|
||
S-VIEW-F1 and S-VIEW-F2 forks are now **SETTLED** (2026-07-27) — **preview velocity persists** via
|
||
the instrument's own VST3 component state (envelope bump to v6, *not* the extension's project
|
||
ext-state), and the **envelope overlay's nodes are draggable** via a new pure `envelope_edit`
|
||
module — and S-VIEW builds **directly on the finished, merged Phase L L3 look-and-feel** in a
|
||
**single implementation pass** (no two-pass restyle-after; L3 landed 2026-07-27, commit `c53683e`).
|
||
Only S-VIEW-F3 (Browse modal presentation) remains open. Full r9 record in the Addendum below. r8
|
||
records Daniel's **duration-preserving correction** (2026-07-26, verbatim: *"isn't that ratio stuff
|
||
going to change the playback rate? I want duration-preserving repitching"*): the ratio path is
|
||
**varispeed** (pitch/duration coupled), so S16 is reshaped from "pitch envelope only" into a
|
||
**pitch-engine mode — Varispeed vs Preserve — plus the pitch envelope** riding either engine.
|
||
The WDL verdict flips: `WDL_SimplePitchShifter` (duration-preserving OLA), previously dismissed
|
||
as the wrong tool, is **now the Preserve-engine candidate** and got a real per-voice RT
|
||
viability assessment. Two S16 forks flagged: **S16-F1** (engine default — lean Preserve) and
|
||
**S16-F2** (Preserve implementation — WDL shifter first, hand-rolled pure module held). See the
|
||
r8 Addendum in §4. r7 records the
|
||
**sampling-modes engine directive** (Daniel, 2026-07-26): Trigger vs Gate play modes (Gate =
|
||
AHDSR, Trigger = one-shot with %-length + fades), a modifiable start point in both, and an
|
||
off-by-default AD pitch envelope — specced as **new Phase S points S15/S16**, with the WDL
|
||
pitch surface swept and reported. See the "sampling modes" Addendum in §4. r6 records the
|
||
**workflow-first reframe of S10** (Daniel, 2026-07-26): the editor's default face becomes a
|
||
**capture browser + guided single-capture setup**, a fresh instance is **silent with a "pick
|
||
a capture" empty state** (reversing the S4 first-sample auto-play), and multi-zone editing is
|
||
demoted to an opt-in Zones panel — see the r6 Addendum in §4. r6 also settles **S-NAME-1**
|
||
(rename the binary filename too, UID locked). r5 records the post-DAW-test directives on the
|
||
S1–S6 instrument: the product name **ReaSampler 9000** and the **"better than RS5K" UX
|
||
overhaul** (Phase S points S10–S13) — see the r5 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 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 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` / `release` — **verified**,
|
||
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.
|
||
- **`SingleComponentEffect`** — **combines 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 **~200–400 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 0–2 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 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. 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 ~200–400 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 0–1 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-0–1 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 (S7–S9), 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-S1–S6 DAW test)
|
||
|
||
Daniel DAW-tested the S1–S6 instrument and set two directives. These are **settled
|
||
directions**, specced as new Phase S points (S10–S13) 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 r5 lean was **keep the filename, change only display
|
||
strings**; flagged, not decided. *(Now SETTLED in r6, below: rename the filename too, UID
|
||
locked, compat is a DAW-verify — the r5 lean is superseded.)*
|
||
|
||
**The UX bar is "better than ReaSamplOMatic5000."** Verdict verbatim: "okay it works, but
|
||
the UX is awful." The S1–S6 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
|
||
(S10–S13) 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.
|
||
|
||
### Addendum — S10 workflow-first reframe + S-NAME-1 settled (Daniel, 2026-07-26, r6)
|
||
|
||
After the r5 UX-overhaul directive was specced (keymap-first S10), Daniel reframed the
|
||
workflow before S10 was implemented. This **revises S10** and settles S-NAME-1. Settled
|
||
directions, not open forks — recorded here per the doc's settled-decisions convention; PLAN.md
|
||
§S10 and CONTEXT.md §Phase S (workflow hierarchy) carry the spec.
|
||
|
||
**The reframe, verbatim (Daniel, 2026-07-26):** *"We need to think hard about the workflow
|
||
with this plugin. Have a giant list of 'item' blocks is visually useless. When the plugin is
|
||
loaded, we should not have any samples selected. We also need to show the peaks for each
|
||
capture. Filters for a specific bank would be useful. We need to be graphic and descriptive
|
||
with the controls, and guide the user QUICKLY towards setting up a sampler. Most of the time
|
||
the zones won't be used, but it's a nice-to-have. So we should optimize the UX for working
|
||
with individual captures, not a huge list of everything."*
|
||
|
||
**What changed in S10 (the hierarchy is Daniel's; details are product judgment):**
|
||
|
||
1. **Primary flow = one capture, fast.** The metric is **time-to-first-note**: open → pick a
|
||
capture → see it → play it. The default face serves the single capture, not a keymap.
|
||
2. **Fresh instance is SILENT — nothing auto-selected (policy reversal of S4).** The S4
|
||
"first sample plays" convenience is **removed**: open with no stored selection → the
|
||
instrument plays nothing and shows a **"pick a capture" empty state**, not auto-play of
|
||
sample #1. Concretely retires the `selectSample` first-sample fallback (`sample_map.cpp`)
|
||
and the processor's Tier-0 fallback that resolved it (an empty stored id → silence). This
|
||
is a deliberate reversal of the S4 default, recorded as such — not a regression.
|
||
3. **Capture browser, not an item list.** Scannable cards with **peak thumbnails** (the
|
||
`Sample` peaks bank_model already carries — the same data the dock panel thumbnails draw),
|
||
name, root/key badge, and a **bank filter** (bank_book named banks). "A giant list of item
|
||
blocks" is the named anti-pattern; the browser is designed for scanning by eye. `SampleChoice`
|
||
grows to carry the peaks + badge + bank (today it is only `{id, displayName}`).
|
||
4. **Graphic, descriptive controls with a guided fast path.** Once a capture is picked, a
|
||
prominent single-capture setup surface (root note, play-mode basics, level); the keyboard
|
||
strip serves the single-capture case first (shows the capture's root).
|
||
5. **Zones demoted to an opt-in "Zones" panel (S10-Z), not the default face.** "Most of the
|
||
time the zones won't be used." The keyboard-strip drag machinery is still built (it serves
|
||
both the single-capture root-set and the opt-in zoning), but multi-zone editing is behind a
|
||
toggle. Some S12 list ergonomics **pulled into S10**: the browser card layout, peak
|
||
thumbnails, and bank filter are S10's; S12 keeps **scroll** + **type-to-filter search**
|
||
layered over the S10 browser. S11's waveform is the same surface S10 shows for the picked
|
||
capture ("see it"). No renumber — S11/S12/S13 keep their numbers and their boundaries were
|
||
annotated, not moved wholesale.
|
||
|
||
**S-NAME-1 → SETTLED: rename the binary filename too.** The r5 lean (keep the filename,
|
||
display-strings-only) is superseded. The on-disk module is renamed to match the product (e.g.
|
||
`reasampler_9000.vst3`) — full surface: **CMake `OUTPUT_NAME`**, factory vendor/name strings,
|
||
editor title, embed label. The **VST3 class UID stays locked** as the compat anchor.
|
||
**Compat is a DAW-verify, not an asserted fact:** the working assumption is REAPER rebinds a
|
||
saved instance by class UID (not filename), so a rename with an unchanged UID keeps saved
|
||
projects working — but a web check surfaced a JUCE/VST3-replace-VST2 case suggesting REAPER's
|
||
binding can be more nuanced than "UID only" (an FXID match is involved), so UID-only rebinding
|
||
is **not** safe to assert from source. Verify by save-rename-reopen in the DAW; if REAPER keys
|
||
partly on filename, fall back to keeping the filename and record that as shipped.
|
||
|
||
### Addendum — sampling modes (Trigger/Gate) + pitch envelope (Daniel, 2026-07-26)
|
||
|
||
> **Superseded in part by the r8 Addendum below (2026-07-26).** Daniel's duration-preserving
|
||
> correction reshaped S16 from "pitch envelope only" into a Varispeed/Preserve pitch-engine
|
||
> mode, and **flipped this addendum's WDL verdict** — `WDL_SimplePitchShifter` (called the
|
||
> "wrong tool" in item 3 below) is now the Preserve-engine candidate. Read this as the r7
|
||
> point-in-time record; the r8 Addendum carries the current S16 shape.
|
||
|
||
Daniel directed a set of engine features for the sampler, specced as **new Phase S points
|
||
S15 (Trigger vs Gate) and S16 (pitch envelope)**. **The feature set is settled** — recorded
|
||
here per the doc's settled-decisions convention; PLAN.md §S15/S16 and CONTEXT.md §Sampling
|
||
modes carry the spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param
|
||
granularity); the WDL question was resolved by inspection.
|
||
|
||
**Directive, verbatim (Daniel, 2026-07-26):** *"let's have product spec out some features
|
||
for the sampler: Sampling mode: Trigger vs Gate. Gate has an AHDSR envelope. Trigger has
|
||
fade in, % length, and fade out. Both modes have modifiable start point, Gate has modifiable
|
||
loop points too. In addition to amp env, there will be a pitch envelope/curve (AD?) which is
|
||
off by default. Explore using WDL pitch capabilities."*
|
||
|
||
**What was specced (the shape is product judgment; the feature set is Daniel's):**
|
||
|
||
1. **Play mode — Gate vs Trigger (S15), per-sample/per-zone, instrument-owned (D-B).**
|
||
- **Gate** = classic held note: the current ADSR grows a **Hold** stage → **AHDSR**
|
||
(hold=0 is exactly today's ADSR, back-compat); note-off → release; **sustain loop
|
||
applies** (S11's loop markers become Gate-mode UI).
|
||
- **Trigger** = one-shot drum-pad: note-on fires a **% of sample length** with a
|
||
**fade-in** and **fade-out**, **ignores note-off**, **no loop**. Fade default
|
||
**equal-power** (click-free); note-off is a no-op (choke held, fork S15-F1).
|
||
- **Both:** a **modifiable start point** (non-zero initial read position).
|
||
- **Confirmed from `sampler_core.cpp`:** the read loop already advances by an arbitrary
|
||
per-frame ratio with linear interp and applies a per-frame amp tick, so both envelopes
|
||
are per-frame amplitude functions and the start point is a non-zero initial `readPos_`
|
||
— no resampler or voice-lifecycle rewrite.
|
||
2. **Pitch envelope — AD, off by default (S16).** A per-voice AD curve biasing the read
|
||
increment (the classic pitch drop). **RT clean, confirmed:** the resampler is already an
|
||
arbitrary per-frame `readPos_ += ratio_`, so the envelope is a per-frame multiply of
|
||
`ratio_` by `2^(semitones/12)` — **hand-rolled, no new resampler, no WDL dependency**.
|
||
Off by default → bit-identical to pre-S16.
|
||
3. **WDL pitch capabilities — verified, not lore (full surface swept).** The whole vendored
|
||
WDL pitch/resample surface is two headers: **`resample.h`** (`WDL_Resampler`, a real
|
||
sinc/linear RT-suitable resampler — its sinc mode *beats* the core's 2-point linear interp
|
||
for base-repitch quality at a CPU cost; **held as an optional quality upgrade**, not
|
||
needed for S15/S16) and **`simple_pitchshift.h`** (`WDL_SimplePitchShifter`, a time-domain
|
||
OLA *duration-preserving* pitch shifter — wrong tool for a sampler; `set_formant_shift` is
|
||
an **empty stub**). **No elastique / formant-preserving / time-stretch exists in WDL** —
|
||
REAPER's elastique is licensed (zplane), not in the vendored tree. **Recommendation:** S16
|
||
modulation stays hand-rolled; `WDL_Resampler` (sinc) is the only WDL piece worth adopting
|
||
and only as a held base-repitch quality upgrade.
|
||
4. **Sequencing.** S15 before S16 (S16 reuses S15's param plumbing). Both are S3-core
|
||
extensions but **channel-count-agnostic by construction** (per-frame amplitude + read-rate,
|
||
pre-mix), so they **compose with S7 stereo** rather than conflicting. Core halves land in
|
||
CTest independently of the editor; the mode toggle / Trigger handles / AD control surface
|
||
through the S10/S11 waveform + setup work.
|
||
|
||
**Forks flagged (leans given):** *S15-F1 (choke on note-off)* — **held**, out of S15 scope
|
||
(Trigger ignores note-off; choke-groups are Tier-3-adjacent). *S15-F2 (param granularity)* —
|
||
**lean per-zone only** (the single capture is already a one-zone map), flagged because it
|
||
touches S10's single-capture setup surface.
|
||
|
||
### Addendum — duration-preserving correction: S16 becomes pitch-engine modes (Daniel, 2026-07-26, r8)
|
||
|
||
**Correction, verbatim (Daniel, 2026-07-26):** *"isn't that ratio stuff going to change the
|
||
playback rate? I want duration-preserving repitching."* Daniel is right about the mechanics.
|
||
The r7 S16 spec modulated pitch by biasing the per-frame read ratio (`readPos_ += ratio_`) —
|
||
that is **varispeed**: pitch and duration are coupled (an octave up halves the note's
|
||
duration). Daniel wants **duration-preserving** repitch (a transposed note keeps its length).
|
||
This reshapes S16 and **flips the r7 WDL verdict** on `WDL_SimplePitchShifter`.
|
||
|
||
**What changed (the reframe, then the spec):**
|
||
|
||
1. **The reframe — this is a mode, not a replacement.** Both behaviors are musically
|
||
legitimate, so the answer is not "swap varispeed for preserve" but **a per-zone/per-capture
|
||
pitch-engine mode**:
|
||
- **Varispeed** (current path, cheap, zero-latency) — pitch/duration coupled. The
|
||
**classic sampler / RS5K** default; right for **drums / one-shots** (pitch-down-lengthens-
|
||
the-hit is a feature there).
|
||
- **Preserve** (duration-preserving) — a pitch shifter transposes the output while the read
|
||
holds the source duration. Right for **tempo-locked loops and phrases** (a transposed loop
|
||
still lines up to the bar) — which is what captured banks skew toward (project slices).
|
||
The pitch envelope (r7's S16 body) then rides **either** engine: under Varispeed it biases
|
||
the read ratio (as specced); under Preserve it biases the shifter's shift amount. So the
|
||
envelope is preserved, re-homed onto the engine seam.
|
||
|
||
2. **Fork S16-F1 (Daniel's call): the default engine. Lean Preserve.** Argued honestly:
|
||
Preserve because Daniel asked for it **unprompted** (reads as his expectation) and the
|
||
capture workflow is **loop/phrase-heavy**; but Varispeed is the **classic-sampler
|
||
expectation**, is **cheaper + zero-latency**, is **bit-identical to today's shipped feel**,
|
||
and is what percussive one-shots want. Recommendation: **default Preserve, prominent cheap
|
||
per-zone toggle to Varispeed.** Daniel's call.
|
||
|
||
3. **WDL verdict corrected — `WDL_SimplePitchShifter` is now the right category.** Under
|
||
"duration-preserving is the requirement," r7's dismissal ("wrong tool, duration-preserving
|
||
OLA") inverts: **duration-preserving is exactly what we need.** A real per-voice RT
|
||
viability assessment (from `vendor/WDL/WDL/simple_pitchshift.h`):
|
||
- **API:** push/pull block (`GetBuffer`/`BufferDone`/`GetSamples`); `set_shift(2^(semi/12))`
|
||
for pitch with an **independent** `set_tempo(1.0)` duration knob — pitch and duration
|
||
separately controllable, exactly Preserve.
|
||
- **Per-voice:** modest memory (OLA ring ≈ window·srate ≈ a few KB/voice at the 50 ms
|
||
quality-0 window). CPU cheap (O(length), a few mults + one OLA crossfade/frame, **no
|
||
FFT**) → **N polyphonic voices each running one is feasible** in RT discipline.
|
||
- **Costs owned:** (i) **onset latency** ~half-window (~25 ms @ 50 ms) — the load-bearing
|
||
cost; pre-warm at voice-allocation, and it lands on sustained/loop material (Varispeed
|
||
serves tight one-shots); (ii) **queue-growth** allocation in `BufferDone` — settled by a
|
||
silence pre-warm so no `process`-thread allocation in steady state; (iii) **basic
|
||
quality** (SimpleWindowed warble on big transpositions; `set_formant_shift` is an empty
|
||
stub → no formant preservation) — acceptable for loops, replaceable by route (b).
|
||
- **Fork S16-F2:** **route (a)** `WDL_SimplePitchShifter` (low-cost proof) vs **route (b)** a
|
||
hand-rolled pure `pitch_shift` OLA/granular module (house pattern, CTest-testable, full
|
||
control). **Lean (a) first, (b) held** as the quality/latency upgrade — same
|
||
`PitchEngine::Preserve` contract behind the seam.
|
||
|
||
4. **Not proposed / restated ceilings.** `WDL_Resampler` is a *resampler* (couples duration) —
|
||
a held **Varispeed-quality** upgrade, **not** a Preserve engine. **elastique is NOT
|
||
available** (licensed zplane, not vendored). JUCE / rubberband / signalsmith are each a
|
||
**new-dependency fork carrying full D-A weight** (bare-VST3-no-framework is the locked D-A
|
||
choice) — **not proposed**.
|
||
|
||
5. **S15 interaction (cleaner under Preserve).** Trigger's **%-length** becomes **pitch-
|
||
independent** under Preserve (wall-clock stable under transpose — cleaner than Varispeed,
|
||
where transposing a Trigger also scales its length); Gate's **sustain loop** contract under
|
||
Preserve is *loop the source read, shift the output* (loop points stay source-frame facts);
|
||
the **start point** is engine-independent (a source-frame offset). Channel-agnostic for S7
|
||
(the shifter is `set_nch`-aware; one instance per voice carries all channels).
|
||
|
||
6. **RT/CPU honesty.** Preserve is **meaningfully heavier** than varispeed — a per-voice DSP
|
||
object with its own budget, pre-warm, and a possible **Preserve-mode-specific voice cap**
|
||
(below the Varispeed cap) if per-voice cost demands it. Put in Verify: pre-warm → no
|
||
`process` allocation; measure per-voice CPU + onset latency against the polyphony cap. Treat
|
||
S16's Preserve-engine point as the phase's next real DSP spike, not a thin envelope add-on.
|
||
|
||
**Where the spec lives:** PLAN.md §S16 (reshaped to "pitch engine modes + pitch envelope",
|
||
with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 interaction note;
|
||
CONTEXT.md §Pitch engine modes — Varispeed vs Preserve + the corrected WDL surface finding.
|
||
|
||
### Addendum — VST channel isolation (Daniel, 2026-07-26)
|
||
|
||
**Daniel's directive (2026-07-26, settled):** *"support the beta/stable channels for the VST
|
||
as well. The VST in beta should talk to the beta extension only."* This extends Phase V's V4
|
||
beta/stable split — which fully isolated the **extension** per channel — to the **ReaSampler
|
||
9000 VST3 instrument**. Spec'd as **S18**, an immediate Phase S wave, dispatchable in parallel
|
||
with or right after the in-flight waves (S9 ext_keys, S15/S16 processor/editor) — it touches
|
||
`vst_entry.cpp` / `reasampler_vst.h` / the CMake VST3 block, mostly disjoint from those.
|
||
|
||
**Established honestly from the tree — what already works vs. what's missing:**
|
||
- **Already isolated (the V4↔S4 reconcile did this): data pairing.** A beta-built VST already
|
||
*reads* the beta namespace — `ext_keys.h`'s `kProjExtNamespace()` delegates to
|
||
`app_version::extStateNamespace()`, and every wire key (`banks`, `assign_request`, S9's
|
||
generation key, S17's blob key, any future key) is a plain constant *under* that namespace.
|
||
Channel data-isolation is therefore **structural, not per-key** — new keys inherit it for
|
||
free. No S18 work here.
|
||
- **Missing: the VST's *plugin identity*.** Its class UID, binary filename, and display
|
||
strings are single-valued (same for both channels), so two installed channels would collide
|
||
on UID and filename. S18 closes exactly this.
|
||
|
||
**The shape of S18 (mirrors V4's philosophy — one channel per binary, one bit drives it):**
|
||
1. **A UID pair.** The stable class UID is locked forever (S-NAME-1). Beta needs its own
|
||
forever-stable UID (a second constant, minted once, locked identically). Both frozen
|
||
forever; the channel bit selects which is compiled in. **One class per binary, not both**
|
||
— the V4 fully-isolated-binary philosophy, so a beta build never presents the stable
|
||
identity. Saved-project isolation follows: a beta-saved instance rebinds only to the beta
|
||
VST. *Fork S18-F1 (Daniel's call):* mint the beta UID **now** (lean — mirrors the stable
|
||
UID minted at the S1 spike, removes a pre-ship landmine, zero cost unused) vs. defer to
|
||
first beta release behind a locked-once placeholder.
|
||
2. **Channel-derived binary + display identity.** `reasampler_9000` / `reasampler_9000_beta`
|
||
filename (mirror the extension's `OUTPUT_NAME` fork); "ReaSampler 9000" / "ReaSampler 9000
|
||
beta" display; editor title + embed label channel-aware — all from the ONE bit via
|
||
`app_version` accessors, no scattered `#ifdef`s (the V4 invariant).
|
||
3. **The pairing guarantee, stated as an invariant.** A channel's VST talks to that channel's
|
||
extension only, because identity keeps the plugins distinct and the channel-derived
|
||
namespace keeps the data distinct. **Structural, not per-key** — S8/S9/S17's cross-artifact
|
||
keys all inherit it; a future key that forgets to isolate is impossible by construction.
|
||
4. **DAW-verify contract.** Both channels installed side-by-side; each browser sees only its
|
||
channel's banks; stable-project + beta-VST = clean empty (not error); the S-NAME-1
|
||
rename/rebind test extends to the beta UID.
|
||
|
||
**Where the spec lives:** PLAN.md §S18; CONTEXT.md §VST3 channel identity — the UID pair + the
|
||
pairing surface. The pairing surface's data half is already load-bearing V4 machinery; S18
|
||
adds only the identity fork on top.
|
||
|
||
---
|
||
|
||
### Addendum — three-view editor model + drop-to-FX + default window size (Daniel, 2026-07-27, r9)
|
||
|
||
**Framing.** Daniel opened a fresh design pass on the ReaSampler 9000 **editor** after living
|
||
with the S10–S18 UX in the DAW. The instrument *works* and the capture browser is genuinely
|
||
good — but the two-view editor (today's "Browser" + "Zones" toggle) misallocates its space,
|
||
the default window is undersized for a 1080p world, and the drop-a-capture-onto-FX gesture is
|
||
broken in practice. The directive: **make the one job — pick a capture, tune it, play it —
|
||
fast, easy, and fun. Style is a critical ingredient. No spreadsheet aesthetics.** These are the
|
||
`r9` calls. Authoritative spec: **CONTEXT.md §Phase S — editor view-model redesign (S-VIEW)**;
|
||
build roadmap: **PLAN.md §Phase S — editor view-model redesign**.
|
||
|
||
**The reference devices (the north star for control density).** Daniel named Ableton **Simpler**
|
||
and a Kilohearts/Phase-Plant **sampler group** as the composition targets. Both share one
|
||
grammar, and it is the grammar the redesign adopts:
|
||
|
||
- **A large waveform up top is the hero**, with markers and the envelope drawn *over it at
|
||
accurate time* — not a separate abstract envelope box. Simpler shades the sustain-loop region
|
||
teal directly on the waveform; Phase Plant traces the amp envelope as a bright curve across the
|
||
sample. The waveform is where the eye lives.
|
||
- **A thin, dense value-strip of labelled controls** sits under the waveform (Simpler's
|
||
Gain/Start/Loop/Length/Fade ribbon; Phase Plant's LEVEL/SEMI/HARMONIC/SHIFT/PHASE row) — bold
|
||
ALL-CAPS micro-labels over a value, compact, scannable in one pass.
|
||
- **Root note is fenced off as its own affordance** (Phase Plant boxes "Root C 4 +00" and an
|
||
"Offset" knob to the right; Simpler puts Key% and Transp in the pitch cluster). Root is not
|
||
buried in a list of sliders — it is a first-class, always-visible control.
|
||
- **Modular horizontal bands.** Each concern (sample/pitch, then envelope) is its own band with
|
||
its own drawn visual. This is how both devices avoid the spreadsheet look: every band leads
|
||
with a *picture* (waveform, filter curve, envelope shape), and the numbers hang off it.
|
||
|
||
**The three calls (r9).**
|
||
|
||
1. **Three views, not two — and they are NOT a flat toggle.** Today "Browser" and "Zone" sit as
|
||
two equal segments. That framing is wrong: it implies the browser and the zone editor are
|
||
peers, when in truth **the loaded sample is the home** and browsing/zoning are things you
|
||
*reach for*. So: **Sample is the home view** (the default face — the hero waveform, the
|
||
envelope overlay, all the per-sample controls). **Browse** is a **modal page layered over
|
||
Sample** — a full-window overlay you summon to change the loaded capture (select + confirm),
|
||
the way a file-open sheet layers over a document. **Zone** is a **dedicated editing surface**
|
||
opened by its own button when you want to map across the keyboard. This is a
|
||
*document-with-modal-picker* model (Simpler's own shape: the device face is the sample, and
|
||
loading a new one is a distinct act), not a three-way radio. *Why the reframe matters:* it
|
||
makes "I just want to play this capture" the zero-click default, and "I want a different one"
|
||
a single deliberate gesture, instead of making the user re-choose their whole stance every
|
||
time. See CONTEXT.md §S-VIEW for the precise navigation model.
|
||
|
||
2. **The Sample view earns the hero treatment; Browse gets ruthlessly cut.** Browse today
|
||
carries a waveform preview, root-note piano-roll, loop-point labels, a track-root message, and
|
||
a Mono/Stereo toggle — all of which are *tuning* concerns, not *choosing* concerns. Browse's
|
||
only job is **pick a capture**; the grid thumbnails already show the waveform, so every one of
|
||
those tuning affordances moves to Sample (where the hero waveform lives) and Browse shrinks to
|
||
filters + grid + select/confirm. Conversely, Sample **gains a new envelope overlay** drawn on
|
||
the hero waveform at accurate time (the Simpler/Phase-Plant move), a **preview-trigger button +
|
||
velocity knob** (audition the sample at its root without a MIDI controller — the single most
|
||
"fun/fast" affordance the current editor lacks), and inherits the Mono/Stereo toggle and every
|
||
"Modes-and-down" control that lives on Zone today.
|
||
|
||
3. **Two engineering prerequisites, framed but routed to implementation.** The **drop-to-FX bug**
|
||
(dropping a capture onto a track's FX chain does not instantiate + init ReaSampler 9000) and
|
||
the **undersized default window** are not design decisions — they are a bug and a one-line
|
||
default. Both are framed in CONTEXT.md §S-VIEW with the SDK reality swept (drop-to-FX: the S17
|
||
machinery is SDK-correct, so this is a *diagnosis* task, not a redesign; window size: the
|
||
`getSize`/`checkSizeConstraint` mechanism is verified), and both are flagged for
|
||
staff-engineer, not for a product fork.
|
||
|
||
**New parameters this introduces (both instrument performance state, D-B — never bank facts):**
|
||
|
||
- **Key-tracking (Zone view).** A 0%–200% scalar on how sample pitch tracks the keyboard around
|
||
the root note. **100% (default) = standard 12-tone equal-temperament tracking** (a semitone up
|
||
the keyboard = a semitone of repitch — exactly today's behavior). 0% = the sample plays at root
|
||
pitch on every key (a fixed, un-tracked one-shot — the classic drum-machine "no tracking"
|
||
mode). 200% = double-rate tracking (an octave of keyboard spans two octaves of pitch — an
|
||
intentional special effect). This is a *per-zone performance choice*, additive on
|
||
`PerformanceZone`, defaulting to 100% so existing playback is bit-identical. It is the sampler
|
||
idiom every serious sampler has (RS5K's "Pitch for start/end note", Kontakt's "Tracking",
|
||
Simpler's "Key %" — visible in the reference screenshot at "Key 100%"). *Why 200% and not
|
||
higher:* the useful musical range is 0–200%; past 200% it is noise, and a bounded slider reads
|
||
cleaner than an open one.
|
||
- **Preview velocity (Sample view).** The velocity level the preview-trigger button fires at,
|
||
set by an adjacent knob. It is a *utility* setting for the audition button (like a metronome
|
||
level), not a musical parameter of the sample — but **it PERSISTS across reloads** (S-VIEW-F1
|
||
SETTLED, Daniel 2026-07-27). The seam matters and was verified against the existing VST source,
|
||
not recalled: it lives in the **instrument's own VST3 component state** (the envelope-versioned
|
||
`ComponentState` blob the processor round-trips through `getState`/`setState`, bumped to v6 for a
|
||
new top-level `previewVelocity` field), **not** the extension's `persist` project ext-state —
|
||
that module is REAPER-project-scoped and extension-owned, so it would make the level
|
||
project-global instead of per-instance and route an instrument concern through a bank-read-only
|
||
seam. See CONTEXT.md §S-VIEW for the round-trip and back-compat lift. This is what makes the
|
||
preview button *fun*: tap it hard or soft without reaching for a controller — and it remembers.
|
||
|
||
**Two visual components the redesign commits to:**
|
||
|
||
- **The envelope overlay — with DRAGGABLE nodes** — on the Sample view's hero waveform: the amp
|
||
envelope (AHDSR for Gate, the fade/%-length shape for Trigger) drawn as a curve *at accurate
|
||
wall-clock time* across the waveform, the way Simpler draws its amp envelope over the sample and
|
||
Phase Plant traces it in bright blue. This is the single biggest "reads like a real sampler, not
|
||
a spreadsheet" move — the envelope stops being four abstract sliders and becomes a *shape you see
|
||
over the sound*. And it is **directly editable** (S-VIEW-F2 SETTLED, Daniel 2026-07-27 — in
|
||
scope, not deferred): the breakpoints are **draggable handles** (X → segment time, Y → level on
|
||
the sustain node; monotonic-in-time, range-clamped), the exact ADSR-editor grammar Simpler /
|
||
Phase Plant / Serum all use. The sliders stay as the precise numeric-entry surface, and **both
|
||
surfaces read/write the same envelope params** — one source of truth, so a drag moves the sliders
|
||
and a slider edit re-lays the nodes with no divergence. The geometry/hit-test math lives in a new
|
||
pure `envelope_edit` module (mirror of `card_drag`, unit-tested at the clamp/monotonic
|
||
boundaries); the shell only draws the handles and routes the mouse. This is the affordance that
|
||
makes envelope-shaping *feel* like a sampler instead of a form.
|
||
- **The real piano-keyboard pattern** on the Zone view's keyboard strip. Today's spectral strip
|
||
is pretty but does not *read as a keyboard* — Daniel's note. The redesign keeps the pastel
|
||
spectral hue but overlays the **actual alternating white/black key pattern** (bright/dark per
|
||
the palette) so the strip is instantly identifiable as a keyboard. It need not be *shaped* like
|
||
a keyboard (no protruding black keys) — just carry the pattern as an overlay, so a glance reads
|
||
"this is C, this is F#" without counting.
|
||
|
||
**What does NOT change (guardrails restated).** The instrument stays a **read-only bank
|
||
consumer** — every new control (key-tracking, preview velocity, the loop/start markers Sample
|
||
inherits) is *performance map*, never written to `Sample` or the bank (D-B). LICE/SWELL drawing
|
||
only, all layout/hit-test in pure geometry modules (D-A). RT discipline untouched — the
|
||
preview-trigger fires a note through the same voice engine off the audio-thread commit path, no
|
||
new audio-thread work. The VST3 class UID is unchanged — a view reorganization plus additive
|
||
persisted fields is not a compat event; saved instances rebind and restore. And this is an
|
||
*editor* redesign: the S3 voice engine, the keymap resolution, and the component-state format
|
||
(extended additively — `keyTrack` per-zone, `previewVelocity` per-instance via an envelope bump to
|
||
v6, both with back-compat defaults on read) are the same load-bearing core.
|
||
|
||
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW); PLAN.md
|
||
§Phase S — editor view-model redesign. This Addendum is the *why*; those are the *what/how*.
|
||
|
||
---
|
||
|
||
### Addendum — velocity → amp transfer-curve editor (Daniel, 2026-07-27, r10)
|
||
|
||
**Framing.** Folds one more control into the S-VIEW redesign: a **visual velocity → amp
|
||
transfer-curve editor**. Today the engine maps velocity to gain *linearly* (`velocityGain_ =
|
||
velocity / 127.0`, `sampler_core.cpp:261`), applied once at note-on in `Voice::start()`. Daniel
|
||
wants that mapping to become an **editable transfer curve** — a bezier from a default flat line to
|
||
an arbitrary multi-point curve — so velocity dynamics are fully shapeable per sound.
|
||
|
||
**Directive, verbatim (Daniel, 2026-07-27):** *"By default any velocity plays at full level. Add
|
||
a visual transfer-curve editor: an editable curve from the default flat y=1 to any bezier curve
|
||
with arbitrary points, where x = velocity (0–127) and y = amp scalar (0–1). Enables very dynamic
|
||
control over velocity → level."*
|
||
|
||
**The reference grammar.** A 2-D transfer-curve editor is the standard sampler/synth velocity-
|
||
response surface — Kontakt's velocity/mod curve editor, Serum's env/curve nodes, Ableton's
|
||
velocity device, any DAW's automation-curve editor. X is the input (velocity, 0–127), Y is the
|
||
output multiplier (amp scalar, 0–1), and draggable control points bend the mapping. It is the
|
||
same direct-manipulation, one-source-multiple-views instinct as the r9 envelope overlay — and it
|
||
**reuses the r9 `envelope_edit` house pattern almost verbatim** (a pure geometry/clamp module +
|
||
a LICE shell that draws handles and routes the mouse).
|
||
|
||
**The four calls (r10).**
|
||
|
||
1. **State home → per-zone (`PerformanceZone`), sibling of the amp envelope + keyTrack.** Velocity
|
||
response is a *per-sound performance characteristic*, exactly like the AHDSR envelope, the pitch
|
||
engine, and the r9 key-tracking scalar — all of which already live on `PerformanceZone` (D-B,
|
||
instrument-owned, never a bank fact). A punchy drum wants a different velocity curve than a
|
||
sustained pad, so the curve must vary per zone, not per instance. This rules out `ComponentState`
|
||
(per-instance, where preview velocity correctly lives — that is a *utility* setting, one per
|
||
instrument; the transfer curve is a *musical* setting, one per sound). The curve is an additive
|
||
field on `PerformanceZone`, defaulting to the flat identity, and rides the **zones-payload
|
||
version axis** — NOT the envelope version axis (contrast preview velocity's envelope-v6 bump,
|
||
which is a per-instance top-level field). The single-capture Sample face reads/writes the same
|
||
one-zone storage site (S15-F2), so the Sample view and the Zone view share one curve store.
|
||
|
||
2. **Default → flat y=1 (all velocities full level), which is a DELIBERATE behavior change — fork
|
||
R10-F1.** Daniel's verbatim default is *"any velocity plays at full level"* — a flat curve at
|
||
y=1. That is **not** bit-identical to today's shipped linear `velocity/127` map: today a soft hit
|
||
is quieter; under a flat-y=1 default every hit plays at unity. This is the one genuine fork the
|
||
feature carries, and it is Daniel's call:
|
||
- *Option A (Daniel's stated default): flat y=1.* Honors the verbatim directive; velocity is
|
||
inert until the user draws a curve. Con: **not** back-compat — already-saved instances (and new
|
||
captures) get louder/flatter dynamics than today's build until a curve is drawn. Every existing
|
||
zone's felt response changes.
|
||
- *Option B: default = the current linear ramp (y = x/127).* Bit-identical to today's engine; the
|
||
editor's "flat y=1" is then one drawn state among many, not the default. Con: contradicts the
|
||
verbatim *"by default … full level"* — the default line would be a diagonal, not flat.
|
||
- *Recommendation: Option A (flat y=1) as the stored default, because it is what Daniel asked for
|
||
and the feature's whole point is that velocity is opt-in dynamics — but flag it loudly as a
|
||
shipped-behavior change so it is a decision, not a silent regression.* If the changed feel on
|
||
existing projects is unwanted, Option B is the safe fallback and costs only the default curve's
|
||
control-point values. **Daniel's call.** (Whichever wins, the *stored* default is a curve the
|
||
editor can draw and the core can evaluate — the two options differ only in which curve is
|
||
seeded.)
|
||
|
||
3. **Pure module → `velocity_curve` (REAPER/LICE-free, unit-tested).** Mirror of `envelope_edit` /
|
||
`card_drag`. Two responsibilities: **(a) evaluation** — `eval(velocity 0–127) → amp scalar 0–1`
|
||
for the voice engine (a bezier through the control points, clamped to the 0–127 × 0–1 box,
|
||
monotonic-in-x by construction so a given velocity has exactly one output); and **(b) editing** —
|
||
add / move / delete control points, each clamped into the box, x-ordered (a point cannot cross
|
||
its neighbours in x, same monotonic grammar as the envelope nodes), with a point hit-test
|
||
(point → which control point, pick radius) and a pixel-delta → clamped-point inverse map. Both
|
||
halves are unit-tested at the boundaries: a known curve + known velocity asserts the eval output;
|
||
a known drag asserts the clamped point set and the box/order constraints. The flat identity curve
|
||
(whichever default R10-F1 picks) is a named constructor. **No RT concern in the module** —
|
||
evaluation is called at note-on, not per frame (see call 4).
|
||
|
||
4. **Voice-engine application point → `Voice::start()`, replacing the linear `velocity/127`.**
|
||
Confirmed from source: `sampler_core.cpp:261` computes `velocityGain_ = velocity / 127.0` **once
|
||
at note-on** inside `Voice::start()`; the per-frame render path (`advanceFrame`, line 408:
|
||
`gain = amp * velocityGain_`) then just multiplies the cached scalar. So the transfer curve
|
||
slots in at exactly one line: `velocityGain_ = curve.eval(velocity)` at note-on — **off the
|
||
audio-thread-hostile per-frame path**, evaluated once per voice, honoring the house RT
|
||
discipline (no new per-frame work, no allocation in `process`). The curve travels to the voice
|
||
the same way the AHDSR/keyTrack params do: on the zone's `SampleData::play` bundle (resolved from
|
||
the stored `PerformanceZone` at keymap build), read by the voice at `start()`. The pure core owns
|
||
the eval; the voice reads it.
|
||
|
||
**The curve-editor UI — Sample view, near the envelope overlay.** It lives on the **Sample view**
|
||
(the r9 home face), as a compact band adjacent to the hero-waveform envelope overlay — the two are
|
||
the same grammar (a drawn 2-D curve with draggable handles) and reading them side by side is
|
||
natural (amp-over-time next to amp-over-velocity). Draws through the **L1 kit** like every S-VIEW
|
||
surface: a bordered box (X = velocity 0–127, Y = amp 0–1), the bezier traced in an accent hue,
|
||
small draggable node markers at each control point (hover/drag-lit via kit states), add-point on
|
||
click in empty space, delete on a modifier-click or drag-off. The shell (`reasampler_editor.cpp`)
|
||
does the LICE draw + mouse routing; **all geometry/hit-test/clamp math is in the pure
|
||
`velocity_curve` module**. On the Zone view the same editor appears in the per-zone param panel
|
||
(one curve per zone). Additive and bit-identical-for-existing-projects only under R10-F1 Option B;
|
||
under Option A (the lean) it is additive-but-behavior-changing, per call 2.
|
||
|
||
**What does NOT change (guardrails).** Instrument stays a **read-only bank consumer** — the curve
|
||
is *performance map* on `PerformanceZone`, never written to `Sample` or the bank (D-B). LICE/SWELL
|
||
drawing only; all math in the pure `velocity_curve` module (D-A). RT discipline untouched — eval
|
||
is at note-on, not per frame. VST3 class UID unchanged — an additive per-zone field is not a compat
|
||
event. Zones-payload version bump only; the envelope version axis (preview velocity's v6) is
|
||
independent and untouched.
|
||
|
||
**Wave-plan slot (concurrency-aware — six engineers are in worktrees now).** Because the curve
|
||
lands on `PerformanceZone`, it is **blocked by Wave 1 track T-KEYTRK** (which owns the
|
||
`PerformanceZone` schema + the zones-payload version bump right now). The velocity-curve field must
|
||
sequence as a **LATER additive payload bump AFTER T-KEYTRK merges**, so the two do not collide on a
|
||
single payload version number — T-KEYTRK's `keyTrack` field and this `velocityCurve` field are
|
||
sequential additive extensions of the same zones-payload record, not simultaneous ones. It is
|
||
**not** blocked by T-STATE (that track owns the per-instance `ComponentState` v5→v6 envelope bump
|
||
for preview velocity — a different struct on a different version axis). Concretely this is:
|
||
- **a follow-on foundation track** (pure `velocity_curve` module + the core application point at
|
||
`Voice::start` + the additive `PerformanceZone` field + payload bump), gated on T-KEYTRK; plus
|
||
- **a Wave 2 shell-integration item** (the Sample-view + Zone-panel curve-editor UI drawn through
|
||
the L1 kit, routing mouse through `velocity_curve`), gated on the foundation track and composing
|
||
with the S-VIEW-2 Sample face + S-VIEW-3 envelope-overlay work.
|
||
|
||
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW), velocity-curve
|
||
sub-section; PLAN.md §Phase S — editor view-model redesign (S-VIEW-9/S-VIEW-10 + fork R10-F1). This
|
||
Addendum is the *why*; those are the *what/how*.
|
||
|
||
---
|
||
|
||
### Addendum — Sample-face recomposition: knob deck, curve popup, full-width hero (Daniel, 2026-07-27, r11)
|
||
|
||
**Framing.** S-VIEW-1..10 landed and Daniel DAW-tested the three-view editor. The verdict on the
|
||
*model* is good (Sample-as-home holds); the verdict on the *composition* is not: a 12-row stack of
|
||
full-width horizontal sliders and full-width two-segment toggles reads as a settings form, not an
|
||
instrument. r11 recomposes the Sample face into the hardware-sampler grammar the references
|
||
(Simpler, Phase Plant, and every hardware groovebox) actually use — **a big waveform and a deck of
|
||
small labelled knobs in functional clusters** — and drives the **Wave B editor rebuild**.
|
||
|
||
**Directives (Daniel, 2026-07-27, relayed):**
|
||
- **Radial knobs (issue 4).** All the linear sliders become smaller radial knobs — minimal arc
|
||
(~6→4 o'clock), needle indicator, vertical drag. The knob *primitive* is being built on its own
|
||
track; r11 specs the layout that consumes it. Group the controls "in a reasonable way," and
|
||
**group the envelope controls together intuitively** as a unit.
|
||
- **Mode toggles (issue 4).** The pitch-engine and Gate/Trigger toggles must **not** be
|
||
full-width — size and place them sensibly within the grouped composition.
|
||
- **Velocity curve as button + popup (issue 3d).** Replace the inline curve box with a **small
|
||
square preview button** (the curve drawn on it in miniature) **to the right of the
|
||
preview-velocity control**; clicking it opens a **popup** hosting the full-size curve editor.
|
||
This frees the **waveform preview to become full-width**.
|
||
- **Right-click delete (issue 3c).** In the popup curve editor, right-click removes a control
|
||
point.
|
||
|
||
**The composition calls (r11).**
|
||
|
||
1. **Three fenced knob groups: AMP ENVELOPE / PITCH / PITCH ENV.** The grouping follows the
|
||
parameter taxonomy the engine already has, not an invented one. AMP ENVELOPE hosts the
|
||
Gate|Trigger toggle in its caption row (the toggle *selects the envelope shape*, so it heads
|
||
the envelope group — that is the "envelope controls as a unit" directive made structural) over
|
||
the AHDSR knobs (Gate) or Fade In · Length % · Fade Out (Trigger, reordered to time order).
|
||
PITCH hosts the Varisp|Preserve toggle + the Key Track knob (key tracking is repitch math — it
|
||
lives with the engine that applies it). PITCH ENV is its own small envelope unit (Off|On
|
||
caption toggle + P.Attack/P.Decay/P.Depth), mirroring AMP ENVELOPE's grammar.
|
||
2. **Toggles live in the group caption rows.** Compact two-segment toggles (Mono/Stereo-sized)
|
||
right-anchored in each group's micro-caps caption row — not-full-width, and *placed where they
|
||
explain themselves* (each toggle captions the knobs it reconfigures).
|
||
3. **The hero inverts from fixed to elastic.** Collapsing ~312px of slider rows into a ~92px deck
|
||
makes the control surface the fixed band and the full-width waveform the elastic one — the
|
||
Simpler/Phase-Plant proportion. Fork **R11-F1** (Daniel): keep the 840×620 default (hero
|
||
~430px, waveform-dominant — the lean) or shrink the default to ~840×520.
|
||
4. **Popup over inline for the velocity curve.** The inline box bought a permanently visible
|
||
editor at the cost of 168px of hero width for a control most sessions touch once. The
|
||
mini-button keeps the curve *visible* (live miniature, glanceable) while the popup gives the
|
||
*editor* more room than the inline box ever had. All landed interactions carry over; right-click
|
||
becomes the primary node-delete, with Alt-click and drag-off kept as alternates — nothing
|
||
dropped.
|
||
5. **One grammar on both surfaces (fork R11-F2, lean yes).** The Zone param panel should adopt the
|
||
same knob deck + button/popup — Sample and Zone already edit one storage site (S15-F2); the lean
|
||
is that they should speak one control grammar too. Daniel's call (it grows Wave B scope).
|
||
|
||
**What does NOT change (guardrails).** Zero engine/state/persistence change — this is a view
|
||
recomposition of *existing* controls; no new params, no component-state bump, VST3 class UID
|
||
unchanged. All drawing through the L1 kit by palette role; all layout/hit-test in new pure modules
|
||
(`knob_deck`, `curve_popup` — mirrors of `action_bar`/`overflow_menu`); the knobs and the hero's
|
||
envelope nodes remain two surfaces on one param model (S-VIEW-F2's structural sync, untouched).
|
||
The full inventory contract (every landed element → its r11 home) is in the CONTEXT.md spec.
|
||
|
||
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW) → "The
|
||
Sample-face recomposition (r11)"; PLAN.md §Phase S — editor Wave B (S-VIEW-11/12/13 + forks
|
||
R11-F1/R11-F2). This Addendum is the *why*; those are the *what/how*.
|
||
|
||
---
|
||
|
||
## 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 0–1 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):
|
||
|
||
5. **D-E → channel mode (mono | stereo), per-instance, bus-negotiated** (**S7**) — an
|
||
S3-core channel-dimension extension, sequenced first after the editor/embed work.
|
||
6. **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):
|
||
|
||
7. **Product name → `ReaSampler 9000`** (VST3 class UID unchanged; **binary filename renamed
|
||
too — S-NAME-1 SETTLED r6**, compat is a DAW-verify).
|
||
8. **UX overhaul → workflow-first, "better than RS5K"** (**S10–S13**; S10 **reframed r6**):
|
||
S10 = **capture browser (peak thumbnails + bank filter) + guided single-capture setup**,
|
||
**silent-on-open / no auto-select** (reverses S4), multi-zone editing demoted to an opt-in
|
||
Zones panel (S10-Z); S11 waveform + draggable loop points; S12 scroll/search over the S10
|
||
browser + numeric entry + ADSR; S13 drop-to-load folding in the S8 relay. Metric:
|
||
time-to-first-note.
|
||
9. **Visual design language → modern/sleek, system-wide — moved to its own Phase L
|
||
(2026-07-26).** The look-and-feel work (a shared LICE drawing kit + the surfaces that
|
||
adopt it) was originally drafted here as Phase S points S0-DS + S14; it has been **lifted
|
||
out of Phase S into its own Phase L** (Look-and-feel) on `dev`, taken up by a parallel team
|
||
so Phase S feature work proceeds ungated. S0-DS → **L1** (shared kit); S14 → **L2**
|
||
(expanded to a thorough dock-panel layout redesign per DS-3); VST editor + embed restyle →
|
||
**L3** (gated on Phase S landing on dev). Forks DS-1 (LICE + WDL free game, no external
|
||
frameworks), DS-2 (Direction B "Neon Console" + Direction C's spectral keyboard strip), and
|
||
DS-3 (thorough panel layout) are all **SETTLED (2026-07-26)**. Framing + palette + the three
|
||
visual directions + forks: `docs/product/visual-design-language.md` (on `dev`); roadmap +
|
||
spec: **PLAN.md §Phase L + CONTEXT.md §Phase L** (on `dev`). **S10–S13 build with the
|
||
current drawing and adopt the L1 kit when it lands — not gated on Phase L.** Answers
|
||
Daniel's "the VST is dogshit / temple os / does Cockos have a toolkit" (2026-07-26,
|
||
post-S1–S6 DAW test).
|
||
10. **Sampling modes + pitch engine → engine features** (**S15** Trigger vs Gate, **S16**
|
||
pitch-engine modes + pitch envelope; see the "sampling modes" r7 + "duration-preserving"
|
||
r8 Addenda in §4). Gate = AHDSR held note (hold added to today's ADSR); Trigger = one-shot
|
||
with %-length + fade-in/out, ignores note-off; both carry a modifiable start point; Gate
|
||
keeps loop points. **S16 reshaped (r8, Daniel's duration-preserving correction):** a per-
|
||
zone **pitch-engine mode — Varispeed** (current, cheap, pitch/duration coupled — classic
|
||
sampler, right for drums) **vs Preserve** (duration-preserving via a per-voice pitch
|
||
shifter — right for tempo-locked loops/phrases). Pitch envelope = per-voice AD, off by
|
||
default, riding either engine (biases `ratio_` under Varispeed, the shift amount under
|
||
Preserve). WDL verdict corrected: **`WDL_SimplePitchShifter` is the Preserve-engine
|
||
candidate** (duration-preserving OLA — RT-viable per-voice with pre-warm; the load-bearing
|
||
cost is onset latency), `WDL_Resampler` (sinc) held as a Varispeed-quality upgrade only; no
|
||
formant-preserving/elastique in WDL. Forks: **S16-F1** (engine default — lean Preserve,
|
||
Daniel's call), **S16-F2** (Preserve impl — WDL shifter first / hand-rolled held), plus
|
||
S15-F1 (choke, held) / S15-F2 (param granularity, lean per-zone). Feature set settled;
|
||
the engine default is Daniel's fork.
|
||
|
||
**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1–S6 the original
|
||
dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded
|
||
UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10–S13** the ReaSampler
|
||
9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes engine features);
|
||
**CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the channel-mode
|
||
/ ingest / bank-generation / sampling-mode / pitch-engine contracts, the UX-overhaul spec,
|
||
the product-name convention, the pure/shell split, the WDL finding, 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)
|
||
|
||
- Steinberg VST3 SDK — `SingleComponentEffect` class reference (combined processor +
|
||
controller; "non-distributable" caveat):
|
||
https://steinbergmedia.github.io/vst3_doc/vstsdk/classSteinberg_1_1Vst_1_1SingleComponentEffect.html
|
||
- Steinberg VST3 SDK — `IAudioProcessor` class reference (process/setupProcessing/bus
|
||
contract):
|
||
https://steinbergmedia.github.io/vst3_doc/vstinterfaces/classSteinberg_1_1Vst_1_1IAudioProcessor.html
|
||
- VST3 Developer Portal (overview, tutorials, example plugins — AGain / Note Expression
|
||
Synth as the skeleton copy-source): https://steinbergmedia.github.io/vst3_dev_portal/
|
||
- JUCE 8 EULA (dual AGPLv3 / commercial; tier structure):
|
||
https://juce.com/legal/juce-8-licence/
|
||
- JUCE forum — revenue limits & JUCE 8 pricing (Starter ~$20K/yr free, Indie ~$200K/yr
|
||
limit, ~$3,500): https://forum.juce.com/t/revenue-limits-for-juce-tiers/61058
|
||
|
||
*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 (~200–400)**, 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.
|