31 KiB
PLAN.md — ReaSampler milestone roadmap
Living milestone roadmap for ReaSampler. Derived from CONTEXT.md's 11-step build
order; CONTEXT.md remains the authoritative spec — this file is the tickable
checklist, not a re-statement of the spec. When a point lands, doc-keeper removes
it here and appends it to COMPLETED.md.
Conventions
- One checkbox
- [ ]= one discrete, independently-landable point. - Each milestone opens with a Goal (one line) and a Verify criterion (the acceptance gate; precision invariants pulled in where one applies).
- Verify-in-DAW points require a manual REAPER run; pure points are gated by CTest.
- "See CONTEXT.md §…" points at the authoritative detail — do not duplicate it here.
Milestone 9 — slots (MPC-style)
Goal: "Capture to slot N" / "insert slot N", MIDI-bindable. CONTEXT.md Build order 9. Verify (in DAW): Slot capture and slot insert fire from MIDI bindings; slot state persists via the index.
- Slot model + slot↔sample assignment.
- "Capture to slot N" / "insert slot N" actions, MIDI-bindable.
Milestone 10 — provenance (re-capture from source)
Goal: Populate Sample.provenance (parent sample id + a capture-recipe
fingerprint) on resample-from-sample, and ship a "re-capture from source"
action that regenerates a sample from its recorded source. Reconciled with the
dual-canvas (Phase D2) model. CONTEXT.md §Data model, §capture; product framing +
the settled reconciliation in docs/product/provenance.md.
Verify (in DAW): A sample resampled from a bank sample carries its parent id +
recipe fingerprint; "re-capture from source" regenerates the file into the bank
(never auto-inserting into the timeline — load-bearing principle); re-capture with
an unchanged source + request is byte-identical to the original (bit-identical
repeats); non-destructive to source items/tracks.
Reshaped from the old "provenance + null-test verify" M10. Cut (fixed by Daniel): the null-test verification action and the true-pre-FX-dry mechanism the old note required — both dropped, see
docs/product/provenance.md§What was cut. Kept: provenance + re-capture. TheSample.provenancestruct and its JSON round-trip already exist (M1) — M10 populates and consumes the field, it does not add it. Fork picks settled by Daniel (2026-07-23): P1=a thin fingerprint, P2=a bank-only re-capture; P3/P4 moot under P2=a. The points below are locked to that path.
- Populate
Sample.provenanceon resample-from-sample:parentSampleId(the bank sample the capture derived from) +fxChainSnapshotas a thin capture-recipe fingerprint (scope + source FX-chain identity/hash at capture time — a drift/repro fingerprint, NOT a serialized pre-FX-dry chain to restore; P1=a settled). - "Re-capture from source" action: regenerate a provenanced sample by re-running its recorded capture request against the source's current state; update the bank file + Sample in place. Bank-only — never inserts/re-places into the timeline (load-bearing principle). Reports if the source drifted since capture.
- Verify: re-capture of an unchanged source is byte-identical to the original
capture (bit-identical repeats); non-destructive (
FxBypassGuardsnapshot/restore as M7); relative-paths-only preserved.
Dual-canvas reconciliation (settled — docs/product/provenance.md): With
bank-only re-capture (P2=a), provenance is pure per-sample bank metadata,
bank_model and view_mode_model stay decoupled, and M10 touches no canvas
code. Dual-canvas compliance is satisfied by staying on the right side of the
capture-never-places line — not by any new coupling. Forks P3 (canvas/lane memory in
provenance) and P4 (re-capture auto-tag interaction) were only live under
re-capture-and-replace (P2=b) and are closed as moot; if the user manually
re-places a regenerated sample, the existing D2 mode-aware placement rule governs.
Milestone 11 — polish
Goal: Batch capture (per selected item / per razor area), resample-and-mute-source, conform-on-insert, native OS drag-out. CONTEXT.md Build order 11, §Non-goals (drag-out deferred to last). Verify (in DAW): Each polish action works without regressing the precision invariants; drag-out places a valid file in the OS target.
- Batch capture: per selected item / per razor area.
- Resample-and-mute-source.
- Conform-on-insert (explicit).
- Native OS drag-out (deferred final;
InsertMediapath must already work). - Keybinding help labels: in the docked bank_panel, surface the current key
binding for each capture/provenance action (e.g. "Capture Item → ") by
querying the SDK for the key bound to the action's command id
(
kbd_getTextFromCmd(cmd, SectionFromUniqueID(0))— main section) and formatting a reminder label. Unbound case degrades to the action name with a clear "unbound"/"—" marker (empty/blank return handled explicitly). Split: label-text formatting (binding string + fallback → label) is pure/testable; the SDK binding query + label draw is bank_panel shell. - Action trigger buttons: clickable bank_panel buttons that fire the capture and
provenance actions directly, routing through the existing command-id contract
(
Main_OnCommand/KBD_OnMainActionExwith the registered command id — the same id minted atregisterAction), never re-implementing capture. Split: button hit-testing/layout math is pure/testable (mirror ofmode_switch/bank_grid); draw + command dispatch is bank_panel shell.
Open questions to resolve during build
Carried from CONTEXT.md §Open questions — keep visible until each is closed by a landed milestone.
parseIntnarrowing hardening:src/bank_model.cppparseIntcastsint64_t → intviastatic_castwithout a range check; integers that fit in int64 but exceedINT_MAXare implementation-defined. Hardening candidate — add bounds check before the cast when integer-field validation is in scope.- Capture send/routing isolation (TODO): The FX-scope capture neutralizes out-of-scope FX, gain, and pan — but NOT aux sends. So a downstream coloring send (e.g. a folder → reverb-track send) still routes and blends the reverb into an item/track capture, past the intended isolation point. A true item-level capture should be taken at the isolated graph point — the target scope's output before out-of-scope track FX/gain/pan and before out-of-scope aux/parallel sends. The hard part: distinguish source routing that must be preserved (e.g. a MIDI send T1→T2 where T2's synth is where a MIDI item's audio is actually produced — the "item level" for that MIDI item is T2's synth output) from coloring sends that must be excluded (folder→reverb). Repro: folder F1; T1 (MIDI) sends MIDI to T2 (synth); T1+T2 → F1; F1 sends to reverb T3; capturing the MIDI item on T1 currently includes the reverb, should be isolated to T2's synth output pre-F1 with the MIDI send preserved and the reverb send excluded. Likely approach: snapshot + mute out-of-scope tracks' aux sends during the render while preserving the main/source signal path — needs a rule for which sends are load-bearing.
Phase D2 — Two-canvas (item-level mode projection; additive to D1)
Design View sub-phase. Extends D1's track-level mode projection to item level via REAPER 7 fixed lanes: on a track present in both stances, each mode owns a fixed lane — the active mode's lane shows and plays, the inactive mode's is hidden and silenced — so a Design take and an Arrange take can share the same track and time position without colliding on the view. Nothing in D1 changes. Runtime floor rises to REAPER 7 for this sub-phase (no version-gate branch; below v7 it is simply unavailable). Authoritative spec: CONTEXT.md §Two-canvas sub-phase (Phase D2 / Phase E) and the surrounding §Design View — additive phase spec. Product framing:
docs/product/design-view.md§Two-canvas direction. When a point lands, doc-keeper moves it toCOMPLETED.md.D2-W1 (pure lane extension), D2-W2 (shell: lane application + new-content detection), D2-W3-A (lane minting + item→lane assignment + persist round-trip), and D2-W3-B (item-level mode actions + W3-A polish) have all landed — see
COMPLETED.md. Phase D2 is functionally complete.Deferred: panel UI indicator for per-track lane/mode state (a per-track lane-split marker). The mode switch already shows the active mode; no natural cheap home for a per-track indicator was found in the bank panel. Explicitly deferred — not silently dropped. Can be picked up later if wanted.
Phase R — Reclaim (file lifecycle: the prune path)
New pillar, own lettered namespace. Prune is the file-lifecycle path the capture and multi-bank specs forward-reference throughout ("files persist on disk until prune") but that had no phase, module, or point. It is the only operation in ReaSampler that deletes bytes off disk. Namespaced
R(Reclaim) alongsideM/D/Bbecause it is a distinct pillar — it serves every orphan-producing path (delete-bank, sample-remove B5, potentially M10 re-capture), not just Multi-bank, and it carries a new risk class (file deletion) with its own invariants. Authoritative spec: CONTEXT.md §Prune — file-lifecycle spec. Product framing + phase-placement justification + forks:docs/product/removal-and-prune.md§Prune. When a point lands, doc-keeper moves it toCOMPLETED.md.Boundary (load-bearing): remove creates orphans; prune reclaims them. No operation other than prune deletes a file; prune deletes only files no index references. A bank op that deletes a file is still a bug.
Depends on: B1, B2 (needs the multi-bank book to union the referenced-set across all banks) and B5 conceptually (sample-remove is a primary orphan-producer, so remove-then-prune is the coherent pair — mirror of evacuate-then-delete). Does not depend on the B3/B4 UI.
R1 — prune-reconcile core (pure)
Goal: REAPER-free, filesystem-free reconciler — given the files present in the
bank folder, the files referenced by the book (unioned across all banks, pool
included), and the owned-file manifest (fork R-D, written from capture onward by
B-cap), compute the orphan set (owned ∩ present) − referenced. The mirror of
ViewModeModel::reconcile(liveGuids), one level down (files instead of GUIDs).
CONTEXT.md §Prune (Module architecture — pure).
Verify: CTest green. Prune null test: a folder whose every file is
referenced deletes nothing; prune returns exactly (owned ∩ present) − referenced
and nothing else. Referenced-set unioned across every bank (a file referenced by any
bank — including via a copy — is never an orphan); a present-but-not-owned file (a
hand-dropped file) is never an orphan.
- Prune-reconcile pure function:
(present, referenced, owned) → orphans, computing(owned ∩ present) − referenced; referenced unioned across the whole book (copies keep a file alive). - Tests: prune null test (all-referenced → empty); orphan = (owned∩present)− referenced; a copied file referenced by a second bank survives; a present-but- unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty manifest edge cases.
R2 — prune shell + persist wiring (filesystem I/O, thin)
Goal: Enumerate the current project bank folder (M4 project-relative resolution), supply the referenced-set and the owned-file manifest (from B-cap) from the session, feed the pure core, and produce a dry-run manifest. No deletion in this wave — the report path only. CONTEXT.md §Prune (persist / prune shell). Verify (in DAW): Dry-run reports the orphan count + reclaimed size (+ file list for a small set) against the resolved current bank folder; resolves paths the same way the index does (survives a Save-As relocation); deletes nothing. Depends on: R1, B1, B2.
- Prune shell: enumerate the resolved current bank folder; feed the pure core.
- Session supplies the referenced-set (union across the book) and the owned-file manifest (written by B-cap); resolve the bank folder via the M4 project-relative machinery.
- Dry-run manifest: orphan count + reclaimed size (+ files for a small set); no deletion in this wave.
R3 — deletion + action (the destructive step, guarded)
Goal: The confirmed deletion step, the bindable "Prune bank folder" action, and
a bank_panel prune button: dry-run-first, confirm-with-manifest, then reclaim the
orphan set — via OS trash where portably available (fork R-C), else unlink.
CONTEXT.md §Prune (guardrails, API).
Verify (in DAW): "Prune bank folder" (action or panel button) reports first,
deletes only on explicit confirm, and reclaims exactly the orphan set — never a
referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety
holds; deletions route to OS trash where available; non-bank and capture invariants
untouched.
Depends on: R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24.
- "Prune bank folder" action (
command_id/gaccel/hookcommand), dry-run-first, confirm-to-delete. bank_panelprune button (fork R-E) that fires the "Prune bank folder" action through the existing command-id contract — the panel affordance alongside the bindable action; split: button hit-test/layout is pure (mirror ofmode_switch/bank_grid), draw + dispatch is bank_panel shell.- Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash where a portable move-to-trash is verified available, else unlink behind the dry-run/confirm guardrail. Verify the platform move-to-trash surface before use (per platform).
- Orphan attribution (fork R-D, settled owned-file manifest): reclaim only
(owned ∩ present) − referenced— the bank system's own leavings, never a hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.)
Phase R forks — settled 2026-07-24
- Fork R-C — deletion mechanism. Settled: trash-preferred, unlink fallback.
Route to OS trash where a portable move-to-trash is available (recoverable), else
unlink behind strong dry-run/confirm. Per-platform trash surface (SWELL / Win
SHFileOperation·IFileOperation/ macOStrashItemAtURL:/ Linux XDG) is a must-verify before use. Folded into R3. - Fork R-D — orphan attribution. Settled: owned-file manifest,
(owned ∩ present) − referenced; folder-sweep rejected as unsafe. Seam lands early — the manifest is written from capture onward (new B-cap point in Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape (sibling"reasampler"key vs.banksblob) is a small build-time residual. - Fork R-E — trigger. Settled: manual action +
bank_panelbutton, dry-run-first, confirm-to-delete. No background sweep. The earlier optional delete-time "…and prune now" convenience was not selected — out of scope. Folded into R3.
Both docs of record: docs/product/removal-and-prune.md §Fork R-C/R-D/R-E and
CONTEXT.md §Prune (Settled forks).
Phase V — Versioning & release (release-milestone pillar, own lettered namespace)
New pillar, own lettered namespace. Version scheme + beta side-channel — the release-deployment path M11 forward-implies but that had no phase or points. Namespaced
V(Versioning) alongsideM/D/B/Rbecause it is a distinct concern (build identity + channel isolation) that touches CMake,main.cpp's forever-stable command-id contract, and the"reasampler"ext-state — not a capture step. Product framing + the full option analysis:docs/product/versioning- and-release.md. Forks V1–V4 SETTLED (Daniel, 2026-07-26) — semver viaproject(VERSION)+ ext-state version stamp prioritized first-wave; plain-betasuffix; console line + panel readout (about-box deferred); and beta ships as a separate, fully isolated coexisting binary (beta-in-isolation) — a reversal of the note's original one-at-a-time recommendation. Deploy/CD wiring (now two named artifacts per platform) hands off to dev-ops. Build-scoped points to be drawn up.
Settled decisions (Daniel, 2026-07-26 — see docs/product/versioning-and-release.md)
- V1 — version scheme: APPROVED as recommended. Semver, single source of truth in
CMake
project(reaper_reasampler VERSION x.y.z), threaded into the binary. The"reasampler"ext-state writing-version stamp is prioritized to the first wave, not deferred — every project saved without the stamp is harder to migrate later, so the migration seam lands early. - V2 — beta suffix: plain
-beta.project(VERSION)owns the release triple; beta carries a-betasuffix.git describedecoration considered and rejected for legibility. - V3 — user-visible home: recommendation accepted. Startup console line
(
"ReaSampler x.y.z loaded") + a bank-panel version/channel readout; about-box deferred. Panel placement is the residual polish call. - V4 — beta channel shape: BETA-IN-ISOLATION (full coexistence). Reverses the
original recommendation. Beta ships as a separate binary (
reaper_reasampler_ beta) with an isolated ext-state namespace (distinct from stable's"reasampler"— a beta cannot corrupt a stable project's saved state) and an isolated forever-stable command-id prefix (beta/stable keybindings don't collide), so both install and run side-by-side. Built through a compile-time channel flag (-DREASAMPLER_CHANNEL=beta) as the mechanism. Two permanent commitments 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_reasamplerextension 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. NamespacedS(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 toCOMPLETED.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 toSamplenow; 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). SingleComponentEffectskeleton: factory + class registration,initializedeclaring an event-input bus + an audio-output bus (no audio input),setupProcessing,setActive, emptyprocess. 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 thebank_panelLICE/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/EnumProjExtStateby name over the host callback (hostcbopcode0xdeadf00d), fetch host project context (0xdeadf00e), and read a known"reasampler"ext-state value. Verify opcodes + marshalling againstreaper_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) toSample; JSON serialize/deserialize with clean defaults for samples lacking them (additive, backward-compatible — mirror of howprovenancewas added). - Populate the fields on capture where derivable (root note) / settable (loop
points); leave them cleanly empty otherwise. No existing
Samplefield changes. - Tests: full round-trip lossless including the new fields; a legacy
SampleJSON (no new fields) parses with defaults and re-serializes without loss; additive invariant (no change to existing fields, dedup, tier, orBankIndexbehavior).
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
processmarshalling: 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 withpersist, not re-implemented). - Sample selection UI (minimal, in the
IPlugViewLICE 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
IPlugViewLICE 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
IReaperUIEmbedInterfaceon 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, againstreaper_plugin.h/video_processor.h/reaper_plugin_functions.h. IReaperUIEmbedInterface— embed contract + message/lifecycle, againstreaper_plugin_fx_embed.h(needed only at S6).