diff --git a/CONTEXT.md b/CONTEXT.md index a3479da..5efd279 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1043,3 +1043,288 @@ No new REAPER *audio* API. New surfaces to verify before use: **Build-time residual (not a fork):** the owned-file manifest's exact persistence shape (sibling `"reasampler"` ext-state key vs. folded into the `banks` blob). + +--- + +# MIDI-playback instrument — additive phase spec (Phase S — Sampler) + +> **New pillar, its own lettered phase, and — uniquely — its own build artifact.** +> Every prior phase (M / D / B / R / V) ships inside the one `reaper_reasampler` +> extension binary. Phase S does **not**: a REAPER extension *cannot* be a +> MIDI-triggered instrument (it is not a node in any track's signal chain), so the +> instrument is a **second, separate binary** — a native **VST3** plugin the user +> instantiates on an instrument track — that reads ReaSampler's banks and plays them +> MIDI-triggered. Namespaced **`S` (Sampler)** rather than "D" (which would collide +> with Design View). The `M`/`D`/`B`/`R`/`V` extension pillars are untouched. Product +> framing, the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, and the +> settled decision record: `docs/product/midi-playback.md`. Same standing discipline: +> **verify every Steinberg VST3 SDK and REAPER/SWELL API name/signature against the +> vendored headers before use.** + +## What it is + +A native **VST3 sampler instrument** — a separate product/artifact from the +extension — that maps ReaSampler's captured bank samples across a MIDI keyboard and +plays them back with a real voice engine (polyphony, velocity, envelopes). The +extension stays the sole owner of **capture + organization**; the instrument is the +**playback surface**. The two are *tightly integrated but distinct acts*: the +extension captures and organizes; the instrument plays. Neither crosses into the +other's role — the instrument never captures, the extension never becomes an +instrument. + +**Why a VST3 and not the extension (load-bearing, settled — see D1/D5/D6 below).** An +instrument track's "read live MIDI, emit audio per-voice, in REAPER's +routing/record/render path" contract belongs to VST/VST3/CLAP/JSFX plugins, hosted +through an entirely different mechanism than the extension API. The extension SDK's +audio-adjacent surfaces (`Audio_RegHardwareHook`, `kbd_OnMidiEvent`, `PlayPreview`, +`pcmsrc` subclassing) are each the wrong tool for a live-MIDI instrument — the full +reasoning is in `docs/product/midi-playback.md` §1. The instrument is therefore a +standard VST3 plugin; this is not an engineering-around-able limitation, it is what +the plugin *format* is. + +## The three locked decisions this spec assumes + +Settled by Daniel (2026-07-26); everything below assumes them. Reasoning preserved in +`docs/product/midi-playback.md` §4. + +- **D1 — native VST3.** Not JSFX. Full sampler sophistication, clean integration, and + access to the REAPER VST-host bridge. JSFX retired (cross-platform-for-free is + worthless under D5, and JSFX gets no bridge). +- **D5 — Windows-only, VST3-only, REAPER-only.** No cross-platform DSP/build/signing + matrix, no multi-format wrapper, no standalone-in-other-hosts concern. REAPER-coupling + via the bridge is intended. This is the single biggest simplifier — it deletes most of + what makes VST3 painful. +- **D6 — two products, tightly integrated.** A separate artifact, but **not** a + divorced file-only companion: via the VST-host bridge it reads the live + `"reasampler"` project ext-state and is project-aware. + +## The VST-host bridge (the integration mechanism, stated once) + +A VST3 *hosted inside REAPER* can call back into REAPER's own API by resolving +function pointers by name over the host callback +(`hostcb(&effect, 0xdeadbeef, 0xdeadf00d, 0, "FunctionName", 0.0)` — the same +string-keyed API table the extension uses; verified in `video_processor.h` and the +`reaper_plugin_functions.h` `GetProjExtState`/`SetProjExtState`/`EnumProjExtState` +entries). The plugin can also fetch its **host context** — the track/take/project it +was instantiated in (opcode `0xdeadf00e`). **Consequence:** the instrument reads the +*same* live `"reasampler"` ext-state that `persist` writes, follows the active +project, and needs no "point me at the bank folder" wiring — it asks REAPER which +project it is in. This capability exists *because* the plugin is hosted in REAPER; it +is the technical affordance D6 leaned on. **Must-verify before build:** confirm the +bridge opcodes and the by-name resolution against the vendored +`vendor/reaper-sdk/sdk/` headers (`reaper_plugin.h`, `video_processor.h`, +`reaper_plugin_functions.h`) — the framing doc's opcode citations are verified from +those headers but the exact call marshalling should be confirmed at the spike. + +## The two seams (audio via files, mapping via live state) + +- **File seam (audio, permanent).** The sample **audio** is the on-disk 32-bit-float + WAVs — project-relative, travelling with the `.rpp` via the M4 machinery. The + instrument resolves those paths **the same way `persist` does** (a shared convention, + not a re-implementation). There is no live PCM stream across the bridge, by design. +- **Live-state seam (the mapping, via the bridge).** For everything that is *not* raw + audio — the bank index, the mapping, which project is active — the instrument reads + the live `"reasampler"` ext-state via the bridge. It sees what `persist` last wrote + and follows the active project. + +## The seam fields — what becomes a bank intrinsic (D-B, settled 2026-07-26) + +**The split model (option iii) is the settled answer.** It mirrors the +capture/placement separation: + +- **Bank intrinsics (facts about the captured file) live on `Sample`.** **Root note** + (the MIDI note the sample was recorded at, so it can be repitched across the + keyboard — distinct from the existing optional *musical key* field) and **loop + points** (sustain-loop start/end for held notes; sample-accurate, zero-crossing-aware) + are *facts about the file*, analogous to sample rate, length, and peaks. They are added + to `Sample` as an **additive field extension** — the same shape as how `provenance` + was added in M1: new optional fields with JSON round-trip, populated at/after capture, + defaulting cleanly for pre-existing samples. This keeps the bank a clean, + tool-agnostic library (WAVs + facts, readable by anything). +- **The performance map (a creative arrangement) lives in the instrument.** **Key + zones** (low/high note per sample), **velocity layers**, **round-robin groups**, + **amplitude envelopes** (ADSR), and per-sample tuning/gain trim are a *performance + choice*, not a fact about a file — they belong to the instrument, not the bank. Under + the live-state seam the instrument may still *read* performance-map data out of shared + `"reasampler"` state, so "who owns which field" is a data-ownership decision, not a + transport one. + +**Why the intrinsic fields are added early (D-B, the backfill-cliff reasoning).** The +`Sample` field addition is scheduled as an **early Phase S point** even though the +instrument that consumes them lands later. Rationale (the *design-the-seam-even-if-you- +defer-the-feature* instinct, same as Fork R-D's owned-file manifest): if the fields are +added only when the instrument needs them, every sample captured before then lacks a +root note / loop points and must be backfilled by hand. Adding the fields now — so +capture starts populating them (or at least defaulting them cleanly) — costs almost +nothing and closes the cliff. The field addition touches the **extension** codebase +(`bank_model` + capture + persist), is independently shippable, and lands before the +instrument build leans on it. + +## Scope tiers (D-C, settled 2026-07-26 — Tier 0–1 committed, Tier 2 held, Tier 3 optional-forever) + +Tiers are minimal → sophisticated; **Tier 0 delivers the core promise** and each tier +above is optional depth, not a prerequisite for the one below. + +- **Tier 0 — "the bank plays" (committed).** One sample mapped chromatically across + the keyboard from its root note; basic polyphony; a simple amp envelope; velocity → + volume. The honest MVP: point a bank sample at a MIDI track and play it repitched. + On the native path this is the `SingleComponentEffect` skeleton plus a single-voice + core, editor deferrable behind a parameters-only default view. +- **Tier 1 — "a keymap" (committed).** Multiple samples zoned across the keyboard (key + ranges), each with its own root note — a captured *kit* or a *multisampled instrument* + plays correctly. This is where the root-note + key-range seam fields earn their place. + One sample per key-region. +- **Tier 2 — "expressive" (HELD — noted, not specified).** Velocity layers, round-robin + (the anti-machine-gun feature), full ADSR, per-sample tuning/gain trim, sustain loops. + Where it becomes a tool people reach for. **Explicitly a follow-on** — its points are + not drawn up in this spec; it is recorded as the next depth increment once Tier 0–1 + proves the instrument belongs in ReaSampler's world. +- **Tier 3 — "instrument polish" (optional-forever).** Filters, filter/pitch envelopes, + LFOs, per-voice pan, choke groups, a modest FX slot. A direction to leave room for, + never a commitment. Do **not** let a Tier-3 feature list inflate the build-shape + decisions. + +## The build shape (D-A, settled 2026-07-26 — bare Steinberg VST3 SDK + LICE editor) + +**Settled: bare Steinberg VST3 SDK, no JUCE, with the editor drawn in the same +LICE/SWELL stack `bank_panel` already uses.** Reasoning (full assessment in +`docs/product/midi-playback.md` §1a and §4 D-A): + +- **The audio-processing scaffolding is bounded.** Using `SingleComponentEffect` (the + SDK's combined processor+controller base — sanctioned for a non-distributable, + REAPER-only plugin under D5/D6) plus the SDK's factory macros, a silent-but-loading + VST3 instrument skeleton is order-of-magnitude a few-hundred lines of + adapt-from-example ceremony, written once. The AGain / Note Expression Synth SDK + examples are the copy-source. Not a tar pit. +- **D5 deletes JUCE's biggest justification.** JUCE exists largely for multi-format / + cross-platform, both of which D5 removed. Its one genuine remaining pull is the editor + UI — and ReaSampler is the atypical case where even that is weak, because it already + has a working, docked, custom-drawn LICE UI (`bank_panel`) and a house style. Drawing + the editor in a VST3 `IPlugView` that hosts a LICE surface reuses that muscle, keeps + the look house-consistent, and avoids JUCE's AGPL-or-pay license posture (the Steinberg + SDK is permissive, no revenue gate). +- **The one real edge — the `IPlugView`↔LICE bridge** (window lifecycle, sizing, event + routing from the host into the draw/hit-test loop) — is *the same class of work* + ReaSampler already did to dock `bank_panel`, not a new competence, but it is less + trodden than dropping in a JUCE editor. It is therefore the phase's **opening spike** + (below), which also converts §1a's experienced-estimates (Windows module-export + symbol names, factory-macro spellings, exact bridge marshalling) into verified fact + before the engine build leans on them. VSTGUI (the SDK's bundled toolkit) is the noted + fallback rung *only if* the LICE bridge proves gnarlier than the panel work suggests; + JUCE is the last resort behind that. + +## The pure core (D3 — the load-bearing split, transplanted) + +**The sampler's voice engine, envelope math, key/velocity mapping, repitch/interpolation, +and keymap resolution are a pure, REAPER-free, DAW-free, unit-tested module** — the +mirror of `bank_model` / `peaks` / `view_mode_model` / `bank_book`, tested in the CTest +harness outside any host. This is the heart of the phase; **test it hard.** The VST3 +wrapper — the `SingleComponentEffect` subclass, bus setup, the `process` call +marshalling MIDI→core and core→audio-buffer, the `IPlugView` LICE editor, and the bridge +calls that read `"reasampler"` ext-state — is the **thin shell**, the only part that +touches VST3 or REAPER at all. Critically, this split is **invariant under the build-shape +choice**: whether the shell is bare-SDK or (hypothetically) JUCE, the pure core is +identical, REAPER-free, and tested the same way. The format choice is a shell choice; the +core is invariant. + +## Module architecture (preserve the pure/shell split — in the new artifact) + +Pure (no REAPER types, no VST3 types, unit-tested — the mirror of `bank_model`): +- **Sampler core** — voice allocation/polyphony, amplitude envelope (ADSR), key→sample + and velocity→sample mapping (the keymap), repitch/interpolation from root note, and + keymap resolution. REAPER-free *and* VST3-free, unit-tested in CTest against known + signals (mirror of how `peaks` asserts an envelope). This is D3's pure core and the + heart of the phase. + +Shell (VST3-facing / REAPER-facing, thin): +- **VST3 wrapper** — `SingleComponentEffect` subclass: `initialize` (declare an event + input bus + an audio output bus, no audio input), `setupProcessing`, `setActive`, + `setState`/`getState`, and the hot-path `process` that reads MIDI off the event bus, + drives the pure core, and writes the core's per-voice audio to the output bus. Plus the + module factory (`GetPluginFactory` + Windows `InitDll`/`ExitDll` — verify exact export + names at the spike). +- **`IPlugView` LICE editor** — hosts a LICE-drawn surface in the VST3 view seat + (window creation/sizing, host→draw/hit-test event routing). Reuses the `bank_panel` + LICE/SWELL competence and house style. +- **Bridge/state reader** — resolves `GetProjExtState`/`EnumProjExtState` by name over + the host callback, fetches the host project context, reads the live `"reasampler"` + ext-state (bank index + intrinsic fields + performance map), and resolves WAV audio + paths the same project-relative way `persist` does. + +## Precision / invariant implications + +- **The bank is one source; the instrument is another view of it (never a fork).** The + instrument is a pure *consumer* of the bank — it does not copy samples, does not own a + private sample store, and does not mutate the bank. The bank stays the single + authoritative artifact (the one-source-multiple-views instinct). Any instrument path + that writes back into the bank or keeps its own sample copies is a bug. +- **Capture/placement/playback stay distinct acts.** The instrument reads and plays; it + never captures and never inserts into the arrange. The capture load-bearing principle + is untouched — Phase S adds a *third* distinct act (playback) without weakening the + capture↔placement separation. +- **`Sample` field addition is additive and lossless.** Root note + loop points are new + optional fields with JSON round-trip, defaulting cleanly for samples captured before + the addition — the same additive, backward-compatible shape as `provenance` (M1). No + existing `Sample` field changes; no `BankIndex` behavior changes. +- **Relative-paths-only survives.** The instrument resolves audio via the M4 + project-relative machinery; it introduces no absolute paths. + +## Embedded TCP/MCP UI (D-D, settled 2026-07-26 — SCHEDULED as a later Phase S point) + +A REAPER-hosted VST3 can draw its own UI **inline in the track/mixer control panel** +via `reaper_plugin_fx_embed.h` (the plugin implements `IReaperUIEmbedInterface`; the +same Cockos surface REAPER's own embedded FX use). A ReaSampler instrument can render a +compact keymap/level strip inline in the TCP/MCP, not only in its own window. Because +this uses the **same LICE-class drawing as the D-A editor path**, it composes naturally +with the bare-SDK-plus-LICE build — the groundwork is the groundwork. + +**Settled: scheduled, not deferred.** This is a real, in-phase later point — it lands +**after** the main `IPlugView` editor exists (it composes with that LICE path), not a +someday-note. It is polish, not a Tier-0 need, so it sequences last in the phase; but it +is on the roadmap. **Must-verify before build:** the `IReaperUIEmbedInterface` contract +and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`. + +## REAPER / Steinberg API surface (verify all signatures) + +- **VST3 SDK (a new vendored dependency — vendor it at the spike).** `FUnknown` and the + `IComponent` / `IAudioProcessor` / `IEditController` interface family; the + `SingleComponentEffect` / `EditControllerEx1` / `AudioEffect` base classes; the class + factory (`GetPluginFactory` + factory macros); `IPlugView` for the editor; + `ProcessData` / `ProcessSetup` for the hot path. **Verify** interface members, the + base-class overrides, factory-macro spellings, and the Windows module-export symbol + names (`InitDll`/`ExitDll`/`GetPluginFactory`) against the vendored SDK at the spike — + the framing doc flags several of these as experienced estimates. +- **REAPER VST-host bridge.** `hostcb` opcode `0xdeadf00d` (resolve API function by + name) and `0xdeadf00e` (host context); the by-name resolution of + `GetProjExtState`/`SetProjExtState`/`EnumProjExtState`. **Verify** against + `vendor/reaper-sdk/sdk/reaper_plugin.h` + `video_processor.h` + + `reaper_plugin_functions.h`. +- **Embedded UI (D-D, later point).** `IReaperUIEmbedInterface` and the embed + message/lifecycle contract — verify against + `vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h` before use. +- **LICE/SWELL editor.** Reuses the `bank_panel` LICE/SWELL drawing surface; verify the + `IPlugView`↔LICE window/bitmap bridge at the spike (window creation, sizing, event + routing) — the least-trodden edge of the phase. + +## Non-goals / guardrails + +- **The instrument never captures and never inserts into the arrange.** Playback is a + read-only act over the bank. Any instrument path that captures, places a timeline item, + or writes back into the bank is a bug — reject in review. +- **The instrument keeps no private copy of the samples.** It consumes the one + authoritative bank; per-instance sample stores are a non-goal (they refork the source + the one-source-multiple-views instinct keeps single). +- **No cross-platform / multi-format.** Windows-only, VST3-only, REAPER-only (D5). Do + not add an AU/AAX/VST2/CLAP wrapper, a mac/Linux build, or a standalone host target. +- **The pure core stays REAPER-free *and* VST3-free.** The voice engine / envelope / + keymap / repitch module takes no VST3 or REAPER type at its boundary — the shell + marshals. Any VST3 or REAPER type leaking into the core is a bug (the D3 split). +- **Additive to the extension.** The `Sample` intrinsic-field addition is additive + (new optional fields; no existing field or `BankIndex` behavior changes); everything + else in Phase S lives in the *second* artifact and does not alter the extension's + M/D/B/R/V pillars. +- **Do not spec Tier 2/3.** Tier 2 is held (noted, not specified); Tier 3 is + optional-forever. Do not let their feature lists drive Tier 0–1's build shape. +- **Verify Steinberg SDK, bridge, embed, and LICE-view surfaces** against the vendored + headers before use — several §1a claims are experienced estimates until the spike + confirms them. diff --git a/PLAN.md b/PLAN.md index a1e6e16..88d1adf 100644 --- a/PLAN.md +++ b/PLAN.md @@ -475,3 +475,208 @@ CONTEXT.md §Prune (Settled forks). locked in:** a second forever-stable command-id prefix and a second ext-state namespace. **Dev-ops:** the build now produces two named artifacts (stable + beta) per platform. + +--- + +# Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact) + +> **New pillar, own lettered namespace, and — uniquely — a second build artifact.** +> Every prior phase ships inside the one `reaper_reasampler` extension binary; Phase +> S does not. A REAPER extension *cannot* be a MIDI-triggered instrument (it is not a +> node in any track's signal chain), so the instrument is a **separate native VST3 +> plugin** the user instantiates on an instrument track, reading ReaSampler's banks +> and playing them MIDI-triggered. Namespaced **`S` (Sampler)** rather than "D" +> (Daniel's call — "D" collides with Design View). Authoritative spec: **CONTEXT.md +> §MIDI-playback instrument — additive phase spec (Phase S)**. Product framing + +> the settled decision record (D1/D5/D6 locked, D-A..D-D settled 2026-07-26): +> `docs/product/midi-playback.md`. When a point lands, doc-keeper moves it to +> `COMPLETED.md`. +> +> **Locked (see `docs/product/midi-playback.md` §4):** D1 native VST3 (not JSFX); +> D5 Windows-only / VST3-only / REAPER-only; D6 two products, tightly integrated via +> the VST-host bridge (live `"reasampler"` ext-state, project-aware). **Settled forks +> (2026-07-26):** D-A bare Steinberg VST3 SDK + LICE editor (no JUCE); D-B split seam +> with root-note + loop-points added to `Sample` *now*; D-C Tier 0–1 committed (Tier 2 +> held, Tier 3 optional-forever); D-D embedded TCP/MCP UI **scheduled** as a later +> in-phase point (after the main editor exists). +> +> **Second build artifact (load-bearing, flagged up front):** Phase S produces a +> *separate* VST3 binary alongside `reaper_reasampler`. The Steinberg VST3 SDK is a +> **new vendored dependency** (vendor at the spike — an implementation-time +> prerequisite, not done here), and CMake grows a second target with Windows VST3 +> module-export/bundle wiring. Both are established by S1 so nothing downstream leans +> on an unbuilt target. + +## S1 — opening spike: VST3 skeleton + `IPlugView`↔LICE bridge (proof + second target) +**Goal:** Stand up the second build artifact and prove the two least-trodden +unknowns before the engine build leans on them: (1) a silent-but-loading VST3 +`SingleComponentEffect` skeleton that REAPER hosts, and (2) an `IPlugView` that hosts +a LICE-drawn surface. Converts §1a's experienced-estimates (Windows module-export +names, factory-macro spellings, exact bridge marshalling) into verified fact. +CONTEXT.md §Phase S (build shape, module architecture, API surface). +**Prerequisite (implementation-time):** vendor the Steinberg VST3 SDK (a new +submodule/dependency alongside `reaper-sdk` / `WDL`); confirm whether VSTGUI is +bundled (moot for D-A but resolves the noted fallback rung). +**Verify (in DAW):** the VST3 skeleton loads in REAPER on an instrument track, +enumerates via `GetPluginFactory`, sets up an event-in + audio-out bus, and runs an +empty `process` without error; an `IPlugView` opens and draws a LICE surface with a +working hit-test; the VST-host bridge resolves `GetProjExtState` by name and reads a +known `"reasampler"` value. Nothing plays yet — this is the loading/drawing/bridge +proof. + +- [ ] CMake second target: a separate VST3 module artifact built alongside + `reaper_reasampler` (Windows VST3 export/bundle wiring; `GetPluginFactory` + + `InitDll`/`ExitDll` — **verify exact export names against the vendored SDK**). +- [ ] `SingleComponentEffect` skeleton: factory + class registration, `initialize` + declaring an event-input bus + an audio-output bus (no audio input), + `setupProcessing`, `setActive`, empty `process`. Loads silently in REAPER. +- [ ] `IPlugView`↔LICE bridge spike: open a plugin editor window hosting a LICE-drawn + surface (window creation/sizing, host→draw/hit-test event routing), reusing the + `bank_panel` LICE/SWELL competence. **The decision's one real unknown — prove it + here.** (VSTGUI is the noted fallback only if this proves gnarlier than the panel + work suggests.) +- [ ] Bridge read spike: resolve `GetProjExtState`/`EnumProjExtState` by name over the + host callback (`hostcb` opcode `0xdeadf00d`), fetch host project context + (`0xdeadf00e`), and read a known `"reasampler"` ext-state value. **Verify opcodes + + marshalling against `reaper_plugin.h` / `video_processor.h` / + `reaper_plugin_functions.h`.** + +## S2 — `Sample` intrinsic fields (root note + loop points; in the *extension*) +**Goal:** Add the two bank-intrinsic seam fields to `Sample` — **root note** (MIDI +note the sample was recorded at; distinct from the existing optional *musical key*) +and **loop points** (sustain-loop start/end, sample-accurate, zero-crossing-aware) — +as an additive field extension with JSON round-trip, populated at/after capture. This +touches the **extension** codebase, is independently shippable, and lands early to +close the backfill cliff before the instrument consumes the fields. CONTEXT.md +§Phase S (seam fields, D-B). **Same additive shape as `provenance` (M1).** +**Verify:** CTest green. Round-trip lossless across the new fields; pre-existing +samples (no root note / loop points) deserialize with clean defaults (no loss, no +migration break); capture populates root note where derivable and loop points where +set; relative-paths-only unaffected; `BankIndex` behavior unchanged (purely +additive). +**Depends on:** nothing in Phase S (extension-only; can land before or in parallel +with S1). + +- [ ] Add `rootNote` (optional MIDI note) + `loopStart`/`loopEnd` (optional + sample-accurate loop points) to `Sample`; JSON serialize/deserialize with clean + defaults for samples lacking them (additive, backward-compatible — mirror of how + `provenance` was added). +- [ ] Populate the fields on capture where derivable (root note) / settable (loop + points); leave them cleanly empty otherwise. No existing `Sample` field changes. +- [ ] Tests: full round-trip lossless including the new fields; a legacy `Sample` + JSON (no new fields) parses with defaults and re-serializes without loss; additive + invariant (no change to existing fields, dedup, tier, or `BankIndex` behavior). + +## S3 — pure sampler core (voice engine / envelope / keymap / repitch) +**Goal:** The REAPER-free **and** VST3-free sampler core — voice allocation/polyphony, +amplitude envelope (ADSR), key→sample and velocity→sample mapping (the keymap), +repitch/interpolation from root note, keymap resolution — unit-tested in CTest against +known signals. **The heart of the phase (D3); the mirror of +`bank_model`/`peaks`/`view_mode_model`/`bank_book`; test it hard.** The core is +invariant under the build-shape choice — no VST3 or REAPER type at its boundary. +CONTEXT.md §Phase S (pure core, module architecture). +**Verify:** CTest green. Voice allocation is correct under polyphony (note-on/off, +voice stealing where bounded); ADSR shape asserted against a known signal (mirror of +`peaks`); repitch from root note produces the expected pitch ratio; keymap resolution +maps a (note, velocity) to the correct sample/zone; the core takes and returns only +plain data (no VST3/REAPER types) — enforced by the test target linking neither SDK. +**Depends on:** S2 (consumes `rootNote` / loop points as core inputs). + +- [ ] Voice engine: polyphonic voice allocation (note-on/off, bounded voice stealing), + per-voice state, mono-and-basic-polyphony sufficient for Tier 0. +- [ ] Amplitude envelope (ADSR) math — asserted against a known signal. +- [ ] Repitch/interpolation from root note (chromatic pitch ratio across the + keyboard); loop-point-aware sustain for held notes. +- [ ] Keymap model + resolution: key ranges/zones (Tier-1 shape) and the + (note, velocity) → sample/zone query; Tier-0 chromatic-from-single-root as the + degenerate case. +- [ ] Tests: voice allocation under polyphony + stealing; ADSR envelope shape; + repitch pitch-ratio correctness; keymap resolution (single-root chromatic + zoned); + core boundary is plain-data-only (no VST3/REAPER types). + +## S4 — Tier 0: "the bank plays" (single sample, chromatic) +**Goal:** The honest MVP — one bank sample mapped chromatically across the keyboard +from its root note, basic polyphony, a simple amp envelope, velocity→volume. Wire the +S3 core into the S1 VST3 shell over the live-state seam (bridge-read bank + audio via +the M4 project-relative path machinery). Editor deferrable behind a parameters-only +default view. CONTEXT.md §Phase S (Tier 0, seams). **Delivers the core promise.** +**Verify (in DAW):** on an instrument track, the VST3 plays a chosen bank sample +MIDI-triggered, repitched chromatically from its root note, with basic polyphony, +an amp envelope, and velocity→volume; it reads the live `"reasampler"` bank via the +bridge and resolves the WAV audio the same project-relative way `persist` does; +following the active project works; it never captures and never inserts into the +arrange (read-only over the bank). +**Depends on:** S1, S2, S3. + +- [ ] VST3 `process` marshalling: read MIDI note-on/off/velocity off the event bus, + drive the S3 core, write per-voice audio to the output bus. +- [ ] Live-state seam: read the bank index + selected sample's root note from + `"reasampler"` ext-state via the bridge; resolve the WAV audio path the M4 + project-relative way (shared convention with `persist`, not re-implemented). +- [ ] Sample selection UI (minimal, in the `IPlugView` LICE editor or a + parameters-only default view): choose which bank sample this instance plays. +- [ ] Tier-0 playback: chromatic-from-root, basic polyphony, amp envelope, + velocity→volume — plays in REAPER's routing/record/render path like any VSTi. + +## S5 — Tier 1: "a keymap" (zoned multisamples, per-sample root notes) +**Goal:** Multiple bank samples zoned across the keyboard (key ranges), each with its +own root note — a captured *kit* (one-shots) or a *multisampled instrument* (same +instrument sampled at several pitches) plays correctly. One sample per key-region. +CONTEXT.md §Phase S (Tier 1). **Where the root-note + key-range seam fields earn +their place.** +**Verify (in DAW):** a keymap of several bank samples plays correctly zoned across +the keyboard, each repitched from its own root note within its range; a captured kit +and a multisampled instrument both play as expected; the keymap is authored in the +instrument (performance map) while root notes come from the bank intrinsics (S2); +editing the keymap does not touch the bank. +**Depends on:** S4. + +- [ ] Keymap editor in the `IPlugView` LICE editor: assign bank samples to key ranges + (low/high note per sample), each with its own root note (from S2 intrinsics, + overridable in the performance map). +- [ ] Tier-1 playback: zoned resolution — a note picks its zone's sample and repitches + from that sample's root note; one sample per key-region. +- [ ] Performance-map persistence: the keymap (zones, per-sample assignment) is the + instrument's own state — held in the instrument (read/written over the live + `"reasampler"` seam per D-B's data-ownership split), never written back as a bank + intrinsic. + +## S6 — embedded TCP/MCP UI (D-D — scheduled in-phase, after the editor) +**Goal:** Render a compact keymap/level strip **inline in the track/mixer control +panel** via `reaper_plugin_fx_embed.h` (`IReaperUIEmbedInterface`) — the same +Cockos surface REAPER's own embedded FX use — so the instrument draws inline, not only +in its own window. Composes with the S1/S5 LICE editor path (same LICE-class drawing). +**Scheduled, not deferred (D-D settled 2026-07-26):** a real later point, sequenced +last because it is polish over a Tier-0 need — but on the roadmap. CONTEXT.md §Phase S +(embedded UI, D-D). +**Verify (in DAW):** the instrument draws a compact inline strip in the TCP/MCP (not +only its own editor window); the inline surface reflects and (where offered) edits the +keymap/levels; the embed lifecycle is clean (open/close/resize); the same LICE drawing +as the main editor is reused. +**Depends on:** S5 (composes over the existing LICE editor). **Must-verify before +build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle against +`vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h`. + +- [ ] Implement `IReaperUIEmbedInterface` on the VST3; draw a compact keymap/level + strip inline in the TCP/MCP using the same LICE surface as the editor. +- [ ] Embed lifecycle (open/close/resize/hit-test inline) handled cleanly; reflects + the live keymap/levels. + +## Phase S — held and optional-forever (noted, not specified) +- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun), + full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once + Tier 0–1 proves the instrument belongs — **its points are not drawn up here.** +- **Tier 3 — "instrument polish" (optional-forever).** Filters, filter/pitch + envelopes, LFOs, per-voice pan, choke groups, a modest FX slot. A direction to leave + room for, never a commitment. + +## Phase S — must-verify-before-build (carried from CONTEXT.md §Phase S) +- **Steinberg VST3 SDK surface** — interface members, base-class overrides, + factory-macro spellings, Windows module-export symbol names + (`InitDll`/`ExitDll`/`GetPluginFactory`), and whether VSTGUI is bundled. Several are + §1a experienced-estimates until S1 confirms them against the vendored SDK. +- **VST-host bridge** — opcodes `0xdeadf00d` (resolve-by-name) / `0xdeadf00e` (host + context) and the exact call marshalling, against `reaper_plugin.h` / + `video_processor.h` / `reaper_plugin_functions.h`. +- **`IReaperUIEmbedInterface`** — embed contract + message/lifecycle, against + `reaper_plugin_fx_embed.h` (needed only at S6). diff --git a/docs/product/midi-playback.md b/docs/product/midi-playback.md index d6c4dda..d9d5f0d 100644 --- a/docs/product/midi-playback.md +++ b/docs/product/midi-playback.md @@ -1,19 +1,25 @@ # MIDI playback — opportunity & design-space framing Framing for a **MIDI-triggered audio sampler** that plays back ReaSampler's captured -banks. This is a **discussion-shaping doc, not a build plan** — no phase, no PLAN.md -points. Several forks that were open in prior drafts are now **locked** (see below); -what remains open is re-presented in §4 so Daniel can make the residual calls before -anything is scoped. +banks. This began as a discussion-shaping doc; with all forks now settled it has become +the **product framing behind a scoped phase**. Its build roadmap lives in **PLAN.md +§Phase S** and its authoritative spec in **CONTEXT.md §Phase S** — this doc holds the +*why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the settled +decision record). -Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r3)**. Prior -revisions (a) established that a REAPER *extension* cannot be a MIDI instrument, and (b) -corrected a material omission — REAPER's **VST-host bridge**, which lets a VST3 plugin -*hosted inside REAPER* call back into REAPER's own API by resolving function pointers by -name over the host callback. This revision folds in **Daniel's locked decisions** (D1, -D5, D6 — see the box below), retires the now-settled forks in place (marked **DECIDED**, -with the reasoning preserved, not deleted), and adds a substantive, honest assessment of -**what complying with the VST3 surface actually costs without JUCE** (§1a). Grounded in +Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r4)**. The +"no PLAN.md footprint" era is **over** — with D-A through D-D settled (below), the +instrument was scoped into **Phase S** (codename Daniel's: "S" for Sampler, because "D" +collides with the existing Design View phase). **PLAN.md §Phase S is now the +authoritative roadmap; CONTEXT.md §Phase S is the authoritative spec.** This doc is the +framing/decision record they point back to. Prior revisions (a) established that a REAPER +*extension* cannot be a MIDI instrument, (b) corrected a material omission — REAPER's +**VST-host bridge**, which lets a VST3 plugin *hosted inside REAPER* call back into +REAPER's own API by resolving function pointers by name over the host callback — (c) [r3] +folded in **Daniel's locked decisions** (D1, D5, D6) and added the honest **bare-VST3 +assessment** (§1a). This revision [r4] records **Daniel's calls on the four residual +forks D-A..D-D** (all settled 2026-07-26) and points to the now-live phase docs. +Grounded in the vendored REAPER SDK headers (`vendor/reaper-sdk/sdk/reaper_plugin.h`, `reaper_plugin_functions.h`, `video_processor.h`, `reaper_plugin_fx_embed.h`), REAPER's published VST-extensions SDK page (`reaper.fm/sdk/vst/vst_ext.php`), and the Steinberg @@ -515,14 +521,27 @@ the thin shell. Confirmed clean for native, and — importantly — **invariant bare-vs-JUCE** (§1a): the shell choice doesn't touch the core. This is the most ReaSampler-native way to build it and it's assumed, not debated, going forward. -### Residual forks — Daniel's to call, in priority order +### Residual forks — SETTLED (Daniel, 2026-07-26; reasoning preserved, not re-opened) -Four decisions remain live. Ordered by how much they gate the rest. +All four residual decisions are now called. Each is marked **SETTLED** with Daniel's +choice and the reasoning kept as the record of *why* — do not re-litigate. They are +scoped into **PLAN.md §Phase S** / **CONTEXT.md §Phase S**. -**D-A (highest) — Bare Steinberg VST3 SDK vs. JUCE (with the UI-toolkit sub-question).** -This is now *the* central fork, and §1a is the honest assessment feeding it. The -sub-question — **who draws the editor?** — is really the whole fork, because §1a shows the -audio-processing scaffolding is bounded either way. +**D-A — SETTLED: bare Steinberg VST3 SDK + LICE editor (no JUCE).** *(The central fork. +§1a is the assessment that fed it. The sub-question — who draws the editor? — was the +whole fork, because §1a showed the audio-processing scaffolding is bounded either way.)* +Daniel took **Option A**: bare Steinberg SDK, `SingleComponentEffect` for the bounded +audio scaffolding, editor drawn in the **same LICE/SWELL stack `bank_panel` already +uses** — no JUCE dependency, no AGPL-or-pay posture, house-consistent UI. The +`IPlugView`↔LICE bridge (the one real unknown) is **not** a gate on the decision (the +decision is made) but remains the right *first implementation step*: it is scheduled as +**Phase S's opening spike (S1)** to convert §1a's experienced-estimates — Windows +module-export names, factory-macro spellings, exact bridge marshalling — into verified +fact before the engine build leans on them. (The stale §1a "is VSTGUI even bundled" +question is dropped as moot under Option A; VSTGUI remains a noted fallback rung only if +the LICE bridge proves gnarlier than the panel work suggests, with JUCE the last resort +behind that.) The full option analysis (A/B/C, the JUCE license posture, the LICE-bridge +caveat) is preserved below as the record. - *Option A — bare Steinberg SDK + LICE editor.* Take only the VST3 SDK (permissive, royalty-free, no revenue gate). Use `SingleComponentEffect` for the ~200–400 lines of @@ -552,58 +571,66 @@ conceding to a full framework. **Concrete ask: greenlight a small `IPlugView`↔ before committing** — it's the one unknown that decides A vs. B, and §1a's LICE-bridge and Windows-export-name claims are experienced estimates that a spike would convert to fact. -**D-B — The seam fields: what becomes a bank intrinsic (§2, old D4).** Where the mapping -lives: (i) bank-owned map, (ii) instrument-owned map, or (iii) split — file-*intrinsics* -(root note, loop points) in the bank; *performance-map* (zones, velocity layers, -round-robin, envelopes) in the instrument. *My lean, unchanged:* **(iii) split.** It -mirrors ReaSampler's capture/placement instinct — root note and loop points are *facts -about the captured file* (like sample rate, length, peaks) and belong in the bank; -zones/layers/envelopes are a *performance choice* and belong in the instrument. It keeps -the bank a clean library and makes the bank-index change **small and additive** (root note -+ loop points — the same shape as adding `provenance` in M1). The live-state seam (§2) -means the instrument reads even the performance-map out of shared `"reasampler"` state, so -"who owns which field" is a data-ownership decision, not a transport one. *The one thing to -settle early:* whether root note / loop points get added to `Sample` **now** (so capture -starts populating them and no backfill cliff opens later) or deferred — this is the "design -the seam even if you defer the feature" instinct, and it's cheap to add the fields early. +**D-B — SETTLED: split seam (option iii), and the intrinsic fields are added NOW.** +*(The mapping-ownership fork, old D4. Options were: (i) bank-owned map, (ii) +instrument-owned map, or (iii) split.)* Daniel took **(iii) split** — and, critically, +called that the `Sample` intrinsic fields land **now**, not deferred. **Root note + loop +points become bank intrinsics on `Sample`** (facts about the captured file, like sample +rate/length/peaks) — a small additive change, the same shape as adding `provenance` in +M1. **Zones, velocity layers, round-robin, envelopes are the instrument's performance +map** (a creative arrangement, not a file fact). The live-state seam (§2) means the +instrument reads even the performance-map out of shared `"reasampler"` state, so "who owns +which field" is a data-ownership decision, not a transport one. Adding the fields now +closes the **backfill cliff** (the *design-the-seam-even-if-you-defer-the-feature* +instinct, same as Fork R-D's owned-file manifest): every sample captured before the +fields exist would otherwise lack a root note / loop points and need hand-backfilling. The +field addition touches the **extension** codebase, is independently shippable, and is +scheduled as an **early Phase S point (S2)** ahead of the instrument that consumes it. -**D-C — Tier scope to commit to first (§3).** Tier 0 ("the bank plays" — one sample, -chromatic, amp envelope, velocity→volume) / Tier 1 ("a keymap" — zoned multisamples with -per-sample root notes) / Tier 2 ("expressive" — velocity layers, round-robin, ADSR, loops) -/ Tier 3 ("polish" — filters, LFOs, choke groups). *My lean:* **scope Tier 0–1 first** as -the "does this belong in ReaSampler's world" proof; hold Tier 2 as the "becomes a tool -people reach for" follow-on; treat Tier 3 as optional-forever. Don't let a Tier-3 feature -list inflate the D-A decision — the editor and DSP needs of Tier 0–1 are modest, which -further weakens the "need JUCE" pull. +**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 (deferred by default) — Embedded TCP/MCP UI.** A REAPER-hosted VST3 can draw its own -UI *inline in the track/mixer control panel* (`reaper_plugin_fx_embed.h`: VST3 implements -`IReaperUIEmbedInterface`; the same Cockos surface REAPER's own JS/embedded FX use). A -native ReaSampler instrument could render a compact keymap/level strip inline in the -TCP/MCP, not only in its own window — and since this uses the same LICE-class drawing as -Option A above, it composes naturally with the bare-SDK-plus-LICE path. **Pure polish, not -a Tier-0 need — deferred by default.** Flagged only because it's a lever that exists on -this exact (native-in-REAPER, LICE-drawing) path and nowhere else, so if "deeply woven into -REAPER's UI" ever becomes the vision, the groundwork is already the groundwork. +**D-D — SETTLED: embedded TCP/MCP UI is SCHEDULED (not deferred).** *(The +inline-in-REAPER-UI lever.)* A REAPER-hosted VST3 can draw its own UI *inline in the +track/mixer control panel* (`reaper_plugin_fx_embed.h`: VST3 implements +`IReaperUIEmbedInterface`; the same Cockos surface REAPER's own JS/embedded FX use) — a +compact keymap/level strip inline in the TCP/MCP, not only in its own window. Because it +uses the **same LICE-class drawing as the D-A editor path**, it composes naturally with the +bare-SDK-plus-LICE build. **Daniel's call: schedule this, don't defer it** — it is a real, +in-phase later point on the Phase S roadmap (**S6**), sequenced *after* the main +`IPlugView` editor exists (it composes with that LICE path), not a someday-note. It is +polish rather than a Tier-0 need, so it sequences last — but it is on the roadmap. --- -## What this doc is asking for +## Where this landed -With D1/D5/D6 locked, the decision this doc now puts to Daniel is a single ordered set: +With D1/D5/D6 locked and **D-A..D-D all settled (2026-07-26)**, the instrument is scoped +into **Phase S** — a native VST3 sampler as a **second build artifact** alongside the +`reaper_reasampler` extension. The settled set: -1. **D-A — bare Steinberg SDK + LICE editor, or JUCE?** (My lean: bare SDK + LICE, gated on - a small `IPlugView`↔LICE spike. This is the one that decides the instrument's whole - build shape and dependency posture.) -2. **D-B — split seam, and do root note / loop points get added to `Sample` now?** (My - lean: yes, split; add the intrinsic fields early to avoid a backfill cliff.) -3. **D-C — commit Tier 0–1 first?** (My lean: yes; hold 2, defer 3.) -4. **D-D — embedded TCP/MCP UI:** deferred by default; note only. +1. **D-A → bare Steinberg VST3 SDK + LICE editor** (no JUCE). The `IPlugView`↔LICE bridge + is the opening implementation spike (**S1**), not a decision gate. +2. **D-B → split seam; root note + loop points added to `Sample` now** (**S2**, in the + extension) to close the backfill cliff. +3. **D-C → Tier 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. -Once D-A and D-B are called, this becomes concrete enough to write an actual phase spec -(the instrument as its own artifact, with the pure sampler core as a testable module -mirroring `bank_model`). Until then it stays a framing doc with **no PLAN.md footprint** — -deliberately, so nothing is scoped until the build shape (D-A) is chosen. +**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1–S6, sequenced by +dependency order: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded +UI); **CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the +pure/shell split in the new artifact, the must-verify SDK/bridge surfaces). This doc is the +framing/decision record they point back to. The "no PLAN.md footprint" era is over. ---