diff --git a/CONTEXT.md b/CONTEXT.md index d853d54..aee6601 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1311,6 +1311,134 @@ and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embe `IPlugView`↔LICE window/bitmap bridge at the spike (window creation, sizing, event routing) — the least-trodden edge of the phase. +## Drop-and-load — drag a capture onto a track's FX button (S7 spec) + +**The gesture.** While a capture is dragged out of the `bank_panel`, a track's TCP **FX +button** becomes a drop zone. Dropping the capture there **instantiates a ReaSampler 9000 +on that track with the dragged capture already loaded and selected for playback** — one +gesture from bank to playable instrument. This is the *third* integration gesture: capture +(extension), placement-into-arrange (extension), and now **placement-of-the-player** +(this wave). It is drop-and-load, not drop-to-arrange — no media item touches the timeline. + +**Why it needs a new drag mode (the CF_HDROP path can't carry it).** Today's drag-out +(M11) becomes an **OS file drag** (`CF_HDROP` via `drag_out` + `drag_out_win`) the moment +the pointer leaves the panel client rect. REAPER's TCP FX button is **not** a native drop +target that instantiates a plugin-with-a-file, so this feature cannot ride the OS-drag +path: an OS drop of a WAV onto the FX area does not create "an instrument preloaded with +that WAV." It requires an **internal drag** where the extension itself tracks the pointer +over REAPER's own UI, detects the FX-button hover, and on release **drives the insert +itself**. The extension is the actor for the whole gesture. + +**The two-part mechanism.** + +1. **Internal-drag hover detection (extension-side, pure + shell).** The `drag_out` pure + module gains a **third `DragGesture`** beyond `Internal` (bank-to-bank) and `OsDrag` + (M11) — `InstrumentDrop`. The gesture decision is refined: leaving the panel client + rect no longer *immediately* means OS-bound. Instead: + - Pointer **inside** the panel client rect → `Internal` (unchanged bank-to-bank drag). + - Pointer **outside the panel but still over REAPER's own window/UI** → + `InstrumentDrop` (new — the shell hover-tracks the TCP FX button and highlights it). + - Pointer **left REAPER entirely** (Explorer / another app) → `OsDrag` (unchanged M11). + + The pure module stays REAPER-free: it decides `InstrumentDrop` vs. `OsDrag` from + position **plus an "over-REAPER's-own-UI" predicate the shell supplies** (the shell owns + the REAPER window/hit query; the pure layer owns the set/boundary algebra). Mirror of how + M11 kept `decideGesture` pure over a rect the shell supplied. The shell then resolves the + pointer to a track + FX-button hotspot, highlights it, and on release drives the drop. + +2. **FX-button drop → add-VST + load-capture (extension-side shell, then instrument + seam).** On release over an FX button the shell: + - Adds a fresh instance: `TrackFX_AddByName(track, "VST3:ReaSampler 9000", /*recFX*/ + false, /*instantiate*/ )`. **Verified present** in + `reaper_plugin_functions.h`: + `int TrackFX_AddByName(MediaTrack* track, const char* fxname, bool recFX, int + instantiate)` — a **negative** `instantiate` always creates a new effect (per the + header comment); the `"VST3:"` prefix selects the format. Captures the returned FX + index (or `-1` on failure). + - **Loads the dragged capture into that instance via the load-capture seam** (below). + - Wraps the whole thing in one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the + gesture is one Ctrl-Z — the same discipline the bank verbs use. + +**The ReaSampler 9000 load-capture seam (the hard coupling — MUST be added; does not yet +exist).** The Phase S spec today gives the instrument a **live-state *read* seam** (it +reads bank index + mapping from `"reasampler"` ext-state via the bridge — §The two seams) +but **no entry point for an external actor to say "this fresh instance should play *this +specific* capture."** Reading the bank is not the same as being *pointed at one sample*. +This wave is the reason to add that seam, and the seam lands **inside the instrument** +(the `phase-s` artifact), not the extension. Two candidate mechanisms — **the choice is a +Phase S open question:** +- **(A) Fresh-instance ext-state handshake.** The extension writes a small "pending load" + hint into `"reasampler"` ext-state keyed to the target track/FX (e.g. the capture id + + a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via the same + bridge it already uses, claims + clears the hint, and self-selects that capture. **Pro:** + reuses the existing bridge; no new VST3 surface. **Con:** a cross-process handshake with + a claim/clear race to get right; "which instance claims which hint" needs a stable key. +- **(B) VST3 `setState` / preset injection.** The extension builds the instance's + component state (the same blob `getState`/`setState` round-trips) with the capture + pre-selected and injects it right after `TrackFX_AddByName`. **Pro:** deterministic, no + shared-state race, uses the instrument's own persistence format. **Con:** the extension + must know and construct the instrument's state blob format — a tighter cross-artifact + coupling to a format that is itself still being built in Phase S; verify whether the + extension can set an added FX's state through the REAPER API (candidate: + `TrackFX_SetNamedConfigParm` / a state-set path — **not yet confirmed against + `reaper_plugin_functions.h`**). + + Lean: **(A)** keeps the artifacts loosely coupled through the one ext-state seam they + already share and avoids the extension hard-coding the instrument's state format — but + it inherits the claim/clear race. Daniel to decide (open question below). + +**Coexistence with the OS drag-out (disambiguation contract).** The two OS-vs-internal +modes are disambiguated **by pointer location, not a mode toggle** — the user never picks +"OS drag" vs. "instrument drop"; the extension infers it from where the pointer is when +released. The M11 boundary (left the client rect) is *refined*, not replaced: leaving the +rect now asks "over REAPER's UI → InstrumentDrop, else → OsDrag." Both M11 OS drag-out and +the internal bank-to-bank drag must remain **byte-for-byte unchanged** in their own +regions — this wave only inserts a new middle case. Multi-capture payloads are a +disambiguation input too (see open question — instrument drop is naturally single-capture; +a multi-capture drag over the FX button is either rejected or loads the first). + +**Precision / invariant implications (drop-and-load).** +- **Explicit user-driven placement — consistent with capture↔placement separation.** This + is a *deliberate placement gesture*: the user chooses to put a playing instrument on a + track, exactly as inserting an item into the arrange is a deliberate act. It does **not** + auto-capture (the file already exists in the bank) and does **not** insert a media item + into the timeline. It instantiates a *reader* of the bank on a track and points it at one + already-captured sample. Capture, placement, and playback stay three distinct acts; this + is placement-of-the-player, not a capture and not a timeline insert. +- **No private sample copy.** The instantiated instrument consumes the one authoritative + bank (it resolves the WAV via the shared M4 project-relative machinery like any + ReaSampler 9000 instance); the seam hands it a *reference* (a capture identity), never a + copied file. Any path that copies bytes into the instance is a bug. +- **The internal drag stays pure-decidable and testable.** The new `InstrumentDrop` + gesture is decided in the `drag_out` pure module (REAPER-free) over a shell-supplied + predicate; the M11 `drag_out` unit tests must not regress. + +**Open questions (Daniel / Phase S team to decide).** +- **Load-capture seam mechanism (A vs. B above).** The single load-bearing Phase S design + choice this wave forces. Lean is (A) for loose coupling; needs Daniel's call. +- **Multi-capture drag over an FX button** — reject (only single-capture drags arm + `InstrumentDrop`), or load the first / a keymap of all? Tier-0 leans reject-or-first; + a multi-capture keymap load is a Tier-1 stretch. +- **FX-button hotspot vs. whole TCP.** Does the drop zone have to be the FX button + specifically, or is dropping anywhere on the target track's TCP enough (simpler hit + resolution, arguably clearer target)? Depends on what the SDK exposes (see must-verify). +- **Numbering vs. the `phase-s` worktree.** Drop-and-load is a sibling of the worktree's + ingest-relay / drop-to-load work (not on dev). Reconcile the S7 label when `phase-s` + merges (see PLAN.md §S7 reconciliation note) — possibly fold into the ingest-relay family. + +**Must-verify before build (drop-and-load).** +- `TrackFX_AddByName` — **verified present** (`reaper_plugin_functions.h`): signature and + the `"VST3:"`-prefix + negative-`instantiate` semantics confirmed from the header. +- **Pointer→track / FX-button hit resolution during a drag** — **not yet confirmed.** + Candidates: `GetTrackFromPoint` / `GetThingFromPoint` (verify names + signatures against + `reaper_plugin_functions.h`); whether the FX button specifically is addressable vs. the + TCP as a whole is an open verification that also decides the "hotspot vs. whole TCP" + question. +- **Instance state injection (only if seam mechanism (B) is chosen)** — whether the + extension can set a just-added FX's state via the REAPER API + (`TrackFX_SetNamedConfigParm` or similar) — **not yet confirmed against + `reaper_plugin_functions.h`.** Moot if (A) is chosen. + ## Non-goals / guardrails - **The instrument never captures and never inserts into the arrange.** Playback is a @@ -1330,6 +1458,11 @@ and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embe 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. +- **Drop-and-load must not regress the two existing drags.** S7 inserts a new middle case + (`InstrumentDrop`) between the M11 OS drag-out and the internal bank-to-bank drag; both + existing gestures stay byte-for-byte unchanged in their own regions. Drop-and-load never + inserts a media item into the arrange and never copies sample bytes into the instance — + it hands the new instance a *reference* to an already-captured bank sample. - **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 0e32623..4ac41bc 100644 --- a/PLAN.md +++ b/PLAN.md @@ -248,6 +248,103 @@ build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle agains - [ ] Embed lifecycle (open/close/resize/hit-test inline) handled cleanly; reflects the live keymap/levels. +## S7 — drop-and-load: drag a capture onto a track's FX button → instantiate ReaSampler 9000 with the capture loaded +**Goal:** Turn a bank capture into a playable instrument in one gesture. Today a drag +out of the `bank_panel` becomes an OS file drag once it leaves the panel (M11 — +`drag_out` + `drag_out_win`, CF_HDROP). This wave adds a **second, internal drag mode**: +while a capture is dragged, a track's TCP **FX button** lights as a drop zone, and +dropping there instantiates a **ReaSampler 9000** (the Phase S VST3 sampler) on that +track with the dragged capture **already loaded and selected** for playback. The +extension drives the whole gesture itself — REAPER's FX button is not a native +plugin-with-file drop target, so this cannot ride the CF_HDROP path. CONTEXT.md §Phase S +(drop-and-load — internal-drag hover mode + the FX-button drop → add-VST + load-capture +seam). Product framing: `docs/product/midi-playback.md` (drop-and-load — the third +integration gesture). +**Consistent with the load-bearing principle (make the reasoning explicit):** this is an +**explicit, user-driven placement gesture** — the user is deliberately choosing to place +a playing instrument on a track, exactly as inserting an item into the arrange is a +deliberate placement act. It does **not** auto-capture (the file already exists in the +bank) and does **not** insert a media item into the timeline; it instantiates a *reader* +of the bank on a track and points it at one already-captured sample. Capture, placement, +and playback stay distinct acts; drop-and-load is a placement-of-the-player gesture, not +a capture and not a timeline insert. +**Two-part mechanism:** +- **(a) Internal-drag hover mode.** A drag armed with a *single* capture that stays + *inside* REAPER's own UI (does not cross to Explorer / another app) is tracked by the + extension: it detects the pointer hovering a track's TCP FX button, highlights it as a + drop target, and on release drives the insert. This is a *third* `DragGesture` beyond + the existing `Internal` (bank-to-bank) and `OsDrag` (M11) — call it `InstrumentDrop`. +- **(b) FX-button drop → add-VST + load-capture.** On drop, the extension adds a + ReaSampler 9000 instance to the target track via `TrackFX_AddByName` (verified present + in `reaper_plugin_functions.h`; signature + `int TrackFX_AddByName(MediaTrack*, const char* fxname, bool recFX, int instantiate)` — + use `"VST3:ReaSampler 9000"` and a negative `instantiate` to always create a new + instance), then pushes the dragged capture's identity into that instance so it plays + that sample — via the **load-capture seam** (below). +**The ReaSampler 9000 load-capture seam (the hard Phase S coupling — MUST be added):** +The current Phase S spec gives the instrument a *live-state read* seam (it reads the bank +index + mapping from `"reasampler"` ext-state via the bridge) but **no entry point for an +external actor to say "instantiate playing *this specific* capture."** This wave is the +reason to add that seam. It is a Phase S dependency, not extension-side, and must land in +the instrument before drop-and-load's drop half can work end-to-end. See CONTEXT.md +§Phase S for the two candidate seam mechanisms (fresh-instance ext-state handshake vs. +VST3 `setState` preset injection) and the open question on which is chosen. +**Coexistence with the M11 OS drag-out (disambiguation, load-bearing):** the two drag +modes are disambiguated by **where the pointer goes**, not by a mode toggle. Inside the +panel client rect → `Internal` (unchanged). Left the panel but still over REAPER's own +window/UI → `InstrumentDrop` (new — hover-tracks the FX button). Left REAPER entirely +(Explorer / another app) → `OsDrag` (unchanged M11). The M11 `decideGesture` boundary +(pointer left the client rect) is **refined**, not replaced: leaving the client rect no +longer immediately means OS-bound; it means "resolve which of InstrumentDrop / OsDrag by +whether the pointer is over REAPER's UI." Single-capture vs. multi-capture also +disambiguates — see open questions. +**Verify (in DAW):** dragging a single capture from the dock over a track's FX button +highlights it; dropping instantiates ReaSampler 9000 on that track with the dragged +capture loaded, selected, and MIDI-playable immediately (no manual pick step); the OS +drag-out to Explorer / another DAW still works unchanged; the internal bank-to-bank drag +still works unchanged; no media item is ever inserted into the arrange; the instrument +holds no private copy (it reads the one authoritative bank). +**Depends on:** M11 (`drag_out` gesture machinery — the mode it extends); **Phase S S4** +(a loadable, playing ReaSampler 9000 instance must exist) **AND the new load-capture seam +added inside ReaSampler 9000**. Gated on **Phase S landing on dev** — the instrument and +its seam are on the `phase-s` worktree, not dev; this wave cannot be built on dev until +they merge (same gate posture as L3). + +- [ ] Extend the `drag_out` pure module with the third gesture: `decideGesture` (or a + successor) returns `InstrumentDrop` when a drag armed with a single capture is over + REAPER's UI outside the panel client rect, `OsDrag` only when it has left REAPER + entirely, `Internal`/`OsDrag`/`None` otherwise unchanged. Pure over (drag state + + pointer + panel rect + an "over-own-UI" predicate the shell supplies). Unit-tested — + the existing `drag_out` invariants (M11) must not regress. +- [ ] Shell (extension): hover-track the pointer over REAPER's UI during the drag, + resolve the hovered track + its FX button (verify the TCP/FX-button hit surface against + the SDK — see must-verify), highlight it as a drop target, and on release drive the + drop. Extends the `bank_panel` drag hook alongside the M11 `drag_out_win` path. +- [ ] Shell (extension): on drop, `TrackFX_AddByName(track, "VST3:ReaSampler 9000", + false, /*instantiate*/ negative)` to always add a fresh instance; capture the returned + FX index; then invoke the load-capture seam to point the new instance at the dragged + capture. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the + whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline). +- [ ] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry + point that lets the just-added instance be told which capture to play (mechanism chosen + per the CONTEXT.md open question). This is the cross-artifact half; it lands in the + instrument, not the extension. +- [ ] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across + single- and multi-capture payloads; FX-button hit resolution (pure geometry where it + can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged. + +> **Numbering reconciliation (load-bearing for doc-keeper).** On **dev**, Phase S runs +> S1–S6; the next free label is **S7**, assigned here. **However**, the `phase-s` +> worktree carries additional, not-yet-merged Phase S points (a stereo point, an +> **ingest-relay** point, and an S10–S13 UX overhaul — the "S10–S13" the L3 coordination +> contract already references but which are not on dev). Drop-and-load is a **close +> sibling of the worktree's ingest-relay / drop-to-load work** (both are cross-artifact +> "get a capture into an instance" gestures). When `phase-s` merges to dev, **reconcile +> this S7 against the worktree's S7+ numbering** — likely by renumbering this wave and/or +> folding it into the ingest-relay family rather than shipping two overlapping +> drop-into-instrument specs. This label is provisional-on-dev, not a claim that the +> worktree's S7+ do not exist. + ## 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 @@ -266,6 +363,16 @@ build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle agains `video_processor.h` / `reaper_plugin_functions.h`. - **`IReaperUIEmbedInterface`** — embed contract + message/lifecycle, against `reaper_plugin_fx_embed.h` (needed only at S6). +- **Drop-and-load (S7) — three surfaces.** (1) `TrackFX_AddByName` — **verified present** + in `reaper_plugin_functions.h` (signature confirmed; the `"VST3:"` name prefix and the + negative-`instantiate`-always-adds semantics are documented in the header comment). (2) + **TCP / FX-button hit resolution** — how the extension resolves the pointer-under-cursor + to a track and its FX-button hotspot during a drag: **not yet confirmed against the + SDK/SWELL** — `GetTrackFromPoint` / `GetThingFromPoint` are candidates to verify against + `reaper_plugin_functions.h`; whether the FX button specifically is addressable (vs. the + TCP as a whole) is an open verification. (3) The **ReaSampler 9000 load-capture seam** — + a *new* interface added inside the Phase S instrument; its mechanism is a Phase S design + choice (see CONTEXT.md §Phase S open question), not a pre-existing SDK surface. ---