docs: spec Phase S stereo (S7), bank-ingest (S8), change-detection (S9)
Settled 2026-07-26: per-instance mono|stereo mode with automatic bus negotiation (S3-core extension); ingest routes through the bank, extension- owned, instrument stays read-only; bank-generation counter for hands-free recapture refresh. ME + drop-target SDK limits verified; spikes flagged.
This commit is contained in:
+264
-1
@@ -1289,6 +1289,109 @@ someday-note. It is polish, not a Tier-0 need, so it sequences last in the phase
|
||||
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`.
|
||||
|
||||
## Channel mode — mono | stereo (D-E, decided 2026-07-26; PLAN.md S7)
|
||||
|
||||
**Decided direction: the instrument gets a per-instance channel-mode toggle — 1 (mono) or
|
||||
2 (stereo) — that negotiates the REAPER audio bus automatically.** Captures are often
|
||||
stereo; the current mono downmix is a Tier-0 simplification, not a permanent shape.
|
||||
|
||||
- **Mono mode keeps today's path.** The decode-side downmix stands: a stereo source in
|
||||
mono mode downmixes (the existing policy), a mono source plays as-is. No engine change
|
||||
for mono.
|
||||
- **Stereo mode is an S3-core extension, not a shell hack (honest).** The S3 core is
|
||||
**mono-per-sample by design today** — `SampleData::frames` is one mono stream,
|
||||
`Voice::renderFrame` returns a single value, `VoiceEngine::render` writes one channel.
|
||||
Stereo mode grows the core a **channel dimension**: 2-channel decoded PCM, per-voice
|
||||
**stereo** render (per-channel fractional read + linear interpolation + loop), and a
|
||||
per-channel mix in the engine. Mono stays the degenerate (single-channel) case, so
|
||||
existing mono behavior is unchanged. This is why S7 sequences first after the editor/embed
|
||||
work: it touches the engine Daniel smoke-tests.
|
||||
- **Where the toggle lives.** Per-instance component state (setState/getState), alongside
|
||||
the selected sample — a **performance choice the instrument owns**, never written to the
|
||||
bank (D-B: not a file fact). Default preserves current behavior (mono).
|
||||
- **Cross-mode policy (settled).** Mono source + stereo mode → **dual-mono** (same signal
|
||||
both channels, centered). Stereo source + mono mode → **downmix** (the existing
|
||||
decode-side policy). The bank's per-sample channel-count intrinsic (already on `Sample`)
|
||||
tells the shell how many channels to decode into `SampleData`.
|
||||
- **Bus negotiation (the "works with the REAPER bus automatically" requirement).** The VST3
|
||||
implements `setBusArrangements` so the output bus reports mono or stereo per the
|
||||
instance's channel mode, and REAPER's routing follows without manual channel wiring.
|
||||
**Must-verify before build:** the `setBusArrangements` / `getBusArrangement` contract and
|
||||
REAPER's mono/stereo instrument-bus expectations against the vendored Steinberg SDK +
|
||||
`reaper_vst3_interfaces.h`.
|
||||
|
||||
## Ingest through the bank — the extension owns ingest (decided "option 1", 2026-07-26; PLAN.md S8)
|
||||
|
||||
**Decided: loading a sample into the sampler is ONE gesture — capture/import-into-bank AND
|
||||
auto-assign to the active sampler instance — and the *extension* owns it.** The instrument
|
||||
stays a **read-only bank consumer**; it never captures and never imports. The extension is
|
||||
the right owner: it has arrange access, Media-Explorer access, and the drop-target surface
|
||||
on its own docked panels. Ingest lives in the *extension* codebase (actions + `bank_panel` +
|
||||
capture/import add-path), routing through the existing capture add-path and the live
|
||||
`"reasampler"` seam the instrument already reads.
|
||||
|
||||
**The three ingest surfaces, with the honest SDK reality (verified against the vendored
|
||||
headers):**
|
||||
|
||||
- **Arrange capture → bank → assign.** A one-click action captures the selected item /
|
||||
time-selection into the bank (reusing the existing capture request path —
|
||||
`CountSelectedMediaItems` / `GetSelectedMediaItem` + `GetSet_LoopTimeRange` are already the
|
||||
capture inputs) and assigns the resulting `Sample` id to the target instance. **It never
|
||||
inserts a timeline item** — the capture/placement separation is load-bearing; assignment
|
||||
is a bank-index + instance-selection act, not a placement.
|
||||
- **Media Explorer import → bank → assign.** The Media-Explorer surface is **thin**:
|
||||
`OpenMediaExplorer` (open/select a file) and `MediaExplorerGetLastPlayedFileInfo` (read the
|
||||
*one* last-played/selected file path + its selection range/pitch/vol/rate) are the whole
|
||||
contract. There is **no** enumerate-selected-files and **no** register-a-drop-handler-on-
|
||||
the-Media-Explorer API. So ME import is **single-file, pull-on-action** — an action fired
|
||||
while a file is selected in the ME — not a push/drop from inside the ME. **Spike:** confirm
|
||||
`MediaExplorerGetLastPlayedFileInfo` returns a usable path+range for a merely-*selected*
|
||||
(not-yet-played) file, or whether a play is required first.
|
||||
- **Drag-and-drop onto ReaSampler surfaces.** REAPER exposes **no** drag-drop registration
|
||||
API. Drop handling is on ReaSampler's *own* HWNDs via SWELL/Win32 (`WM_DROPFILES` /
|
||||
`IDropTarget` on the docked `bank_panel` HWND — the surface the panel already owns) → ingest
|
||||
→ assign. **Assess-and-flag (spike, not promised):** a drop *onto the VST3 editor window* —
|
||||
whether the `IPlugView` HWND can accept an OS file drop and **relay it to the extension as
|
||||
a bank-ingest request** (the instrument does not ingest; it forwards a request over an
|
||||
agreed seam). This crosses the two-artifact boundary and the relay is unproven; if gnarly,
|
||||
drop-onto-panel is the shipped path and drop-onto-editor is deferred.
|
||||
|
||||
**The assign seam.** The ingest action names the target instance (lean: the active/
|
||||
last-focused instance, discovered via the host context the bridge already resolves) and hands
|
||||
it the new sample id — the same instance-owned selection state S4 already persists, so a
|
||||
reload picks it up. With the change-detection seam (below) the assignment refreshes
|
||||
hands-free; without it the ingest action pokes the target instance's reload directly.
|
||||
|
||||
**Guardrail (load-bearing, restated):** ingest is an *extension* act. Any instrument code
|
||||
path that captures, imports, inserts a timeline item, or writes back into the bank is a bug —
|
||||
the instrument reads and plays only.
|
||||
|
||||
## Bank-generation change-detection — hands-free refresh (decided 2026-07-26; PLAN.md S9)
|
||||
|
||||
Instances reference sample **ids**. So a **recapture** (M10) landing under the same id — or
|
||||
an **ingest** (S8) touching the active bank — should refresh playing instances **hands-free**,
|
||||
without re-opening each editor. The missing trigger: a **bank-generation counter** in
|
||||
`"reasampler"` ext-state.
|
||||
|
||||
- **Writer (extension).** A monotonic **bank-generation counter**, stamped into
|
||||
`"reasampler"` ext-state under a new forever-stable `ext_keys.h` constant, bumped on every
|
||||
bank-content mutation that changes what an instance would play (capture add, recapture-in-
|
||||
place, sample-remove, move/copy affecting the active bank). Additive to the persist blob;
|
||||
defaults to 0 for projects saved before the stamp exists.
|
||||
- **Reader (instrument).** Poll the generation over the bridge on a safe **off-audio-thread
|
||||
cadence** (a UI/timer tick, **never** `process`), compare to the last-seen value, and call
|
||||
the existing off-thread `reloadFromBank()` on change — reusing S4's atomic pointer-swap
|
||||
handoff (graveyard-reclaim) so a mid-play refresh does not glitch. No new audio-thread work;
|
||||
no allocation in `process`.
|
||||
- **Cadence + safety.** A low-frequency UI timer, coalescing multiple bumps between polls into
|
||||
one reload (build-time residual). The read already tolerates a stale value by design (it
|
||||
reloads on the *next* poll). **Must-verify before build:** no torn-read hazard on the single
|
||||
integer generation key for a bridge read on the instrument's UI/timer thread concurrent with
|
||||
an extension write.
|
||||
|
||||
This seam serves **both** S8 ingest and M10 recapture; the writer side is extension-only and
|
||||
independent of S8, so it can land alongside either.
|
||||
|
||||
## REAPER / Steinberg API surface (verify all signatures)
|
||||
|
||||
- **VST3 SDK (a new vendored dependency — vendor it at the spike).** `FUnknown` and the
|
||||
@@ -1307,10 +1410,154 @@ and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embe
|
||||
- **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.
|
||||
- **VST3 bus arrangement (S7 channel mode).** `setBusArrangements` /
|
||||
`getBusArrangement` and REAPER's mono/stereo instrument-bus expectations — verify against
|
||||
the vendored Steinberg SDK + `reaper_vst3_interfaces.h`.
|
||||
- **Ingest surfaces (S8).** `InsertMedia` is the placement path (untouched by ingest);
|
||||
`CountSelectedMediaItems` / `GetSelectedMediaItem` + `GetSet_LoopTimeRange` are the
|
||||
arrange-capture inputs (already the capture path's); `OpenMediaExplorer` +
|
||||
`MediaExplorerGetLastPlayedFileInfo` are the *whole* Media-Explorer contract (thin — no
|
||||
enumerate-selected, no ME-drop-handler). Drop handling is SWELL/Win32 on ReaSampler's own
|
||||
panel HWNDs — REAPER exposes **no** drag-drop registration API. All verified against
|
||||
`reaper_plugin_functions.h`.
|
||||
- **Bank-generation seam (S9).** New forever-stable `ext_keys.h` key for the generation
|
||||
counter; read over the same bridge `GetProjExtState` path S4 already uses. No new API —
|
||||
confirm no torn-read hazard on the integer key.
|
||||
- **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.
|
||||
|
||||
## Drop-and-load — drag a capture onto a track's FX button (S17 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*/ <negative>)`. **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 (reconciled on merge to dev).** Authored on dev
|
||||
as a provisional S7; renumbered to **S17** on merge, since the worktree's authoritative
|
||||
Phase S set (S7 stereo, S8 ingest, S9 change-detection, S10–S16) took the lower labels.
|
||||
Drop-and-load stays a distinct integration gesture (drop onto a track's FX button) —
|
||||
a sibling of but not the same as S8 (ingest into the bank) and S13 (drop-to-load inside
|
||||
the editor). See PLAN.md §S17.
|
||||
|
||||
**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
|
||||
@@ -1319,6 +1566,13 @@ and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embe
|
||||
- **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).
|
||||
- **The instrument never ingests (S8).** Capture, import, and drop-ingest are *extension*
|
||||
acts; the instrument only reads and plays. A drop onto the editor window (if the spike
|
||||
proves it viable) is *relayed to the extension* as an ingest request — the instrument
|
||||
never writes the bank itself.
|
||||
- **Channel mode is a performance choice, not a bank fact (S7).** The mono/stereo toggle is
|
||||
per-instance component state, never written to `Sample` or the bank (D-B). The bank's
|
||||
per-sample channel-count intrinsic is a *file fact*; the play mode is the instrument's.
|
||||
- **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 /
|
||||
@@ -1329,7 +1583,16 @@ and embed message/lifecycle against `vendor/reaper-sdk/sdk/reaper_plugin_fx_embe
|
||||
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.
|
||||
optional-forever. Do not let their feature lists drive Tier 0–1's build shape. **Note:**
|
||||
S7 stereo is *not* a Tier-2 feature — it is a channel-count dimension on the existing
|
||||
Tier 0–1 engine, orthogonal to Tier 2's velocity-layers / round-robin / per-sample trim.
|
||||
(S7's stereo loop read is the same loop the core already has, extended per-channel — not
|
||||
the Tier-2 "sustain loops" feature.)
|
||||
- **Drop-and-load must not regress the two existing drags.** S17 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.
|
||||
|
||||
@@ -254,6 +254,230 @@ build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle agains
|
||||
- [ ] Embed lifecycle (open/close/resize/hit-test inline) handled cleanly; reflects
|
||||
the live keymap/levels.
|
||||
|
||||
## S7 — stereo channel mode (mono | stereo; core channel dimension + bus negotiation)
|
||||
**Goal:** Give the instrument a per-instance **channel-mode toggle — 1 (mono) or 2
|
||||
(stereo)** — that "works with the REAPER audio bus automatically." Mono keeps today's
|
||||
downmix path; stereo grows the S3 core a **channel dimension** (2-channel sample data,
|
||||
per-voice stereo render, stereo interp/loop) and negotiates the VST3 output bus so
|
||||
mono/stereo just works in REAPER's routing. **This is an S3-core extension, not a shell
|
||||
hack** — it touches the engine Daniel smoke-tests, so it sequences first after the
|
||||
editor/embed work. CONTEXT.md §Phase S (channel mode, D-E). **Decided direction
|
||||
(2026-07-26); leans below are build-time residuals, not open forks.**
|
||||
**Verify (in DAW):** an instance set to stereo plays a stereo capture in true stereo,
|
||||
its VST3 output bus negotiated to 2 channels via `setBusArrangements` so REAPER routes it
|
||||
without manual channel wiring; an instance set to mono plays the existing downmix path; a
|
||||
mono source in stereo mode plays dual-mono (centered); a stereo source in mono mode
|
||||
downmixes (existing policy); the mode is per-instance state that survives project
|
||||
save/reopen (component state, like the selected sample); the pure core's stereo render is
|
||||
asserted against a known two-channel signal (mirror of `peaks`), and mono behavior is
|
||||
unchanged (regression).
|
||||
**Depends on:** S3 (extends the core), S4 (extends the process/bus shell). Independent of
|
||||
S8/S9.
|
||||
|
||||
- [ ] Core channel dimension (pure, S3 extension): `SampleData` carries N-channel
|
||||
(1 or 2) decoded PCM; `Voice::renderFrame` and `VoiceEngine::render` produce a
|
||||
per-channel frame; stereo linear interpolation + loop read per channel. Mono stays the
|
||||
degenerate case (single channel) — no behavior change for existing mono play. Tests:
|
||||
stereo render asserted against a known 2-channel signal; mono render unchanged.
|
||||
- [ ] Channel-mode toggle as per-instance state: `mono | stereo` in the instrument's own
|
||||
component state (setState/getState, alongside the selected sample); default preserves
|
||||
current behavior (mono). Cross-mode policy: **mono source + stereo mode → dual-mono**
|
||||
(same signal both channels, centered); **stereo source + mono mode → downmix** (the
|
||||
existing decode-side policy). The toggle lives in the instrument, never written to the
|
||||
bank (a performance choice, not a file fact — D-B).
|
||||
- [ ] Shell: decode fills 1- or 2-channel `SampleData` per the source's channel count
|
||||
(the S2/bank channel-count intrinsic already exists); the process path renders the
|
||||
active mode's channel count into the output bus.
|
||||
- [ ] VST3 bus negotiation: implement `setBusArrangements` so the output bus reports
|
||||
mono or stereo per the instance's channel mode, and REAPER's routing follows
|
||||
automatically (no manual channel wiring). **Must-verify before build:** the
|
||||
`setBusArrangements` / `getBusArrangement` contract and REAPER's mono/stereo instrument
|
||||
bus expectations against the vendored Steinberg SDK + `reaper_vst3_interfaces.h`.
|
||||
|
||||
## S8 — ingest through the bank (one gesture: capture/import into bank + assign to instance)
|
||||
**Goal:** Loading a sample into the sampler is **one gesture** — capture/import-into-bank
|
||||
**and** auto-assign to the active sampler instance. **The extension owns ingest** (it has
|
||||
arrange access, media-explorer access, and drop-target surface on its own panels); the
|
||||
instrument stays a **read-only bank consumer**. This lives in the *extension* codebase
|
||||
(actions + bank_panel + capture/insert), routing through the existing capture add-path and
|
||||
the live `"reasampler"` seam the instrument already reads. CONTEXT.md §Phase S
|
||||
(ingest-through-bank contract). **Decided direction "option 1" (2026-07-26).**
|
||||
**Verify (in DAW):** a one-click "capture selected item / time-selection into the bank and
|
||||
assign to the active instance" action captures via the existing capture path (never
|
||||
auto-inserting into the arrange — load-bearing principle intact) and the target instance
|
||||
plays the new sample on its next reload; a Media Explorer file imports into the bank and
|
||||
assigns the same way; a file dropped onto a ReaSampler panel surface ingests into the bank
|
||||
and assigns; the instrument never captures or imports (read-only over the bank throughout).
|
||||
**Depends on:** S4 (an instance to assign to), M7 capture add-path, B2 (active-bank add
|
||||
target). Best paired with S9 so assignment refreshes hands-free; functional without it
|
||||
(assign triggers a reload on the target instance directly).
|
||||
|
||||
- [ ] "Capture selected item / time-selection into bank + assign to active instance"
|
||||
action (`command_id`/`gaccel`/`hookcommand`, MIDI-bindable): reuse the existing capture
|
||||
request path (`CountSelectedMediaItems`/`GetSelectedMediaItem` + `GetSet_LoopTimeRange`
|
||||
as the capture inputs), add the resulting `Sample` to the active bank, then assign its
|
||||
id to the target instance. **Never inserts a timeline item** (capture/placement stay
|
||||
separate — the assignment is a bank-index + instance-selection act, not a placement).
|
||||
- [ ] Media Explorer import → bank → assign: read the Media Explorer's current selection
|
||||
via `MediaExplorerGetLastPlayedFileInfo` (path + selection range), import the file into
|
||||
the bank (existing import/capture add-path), assign to the target instance. **Honest SDK
|
||||
limit (verified against the vendored headers):** the Media-Explorer surface is thin —
|
||||
`OpenMediaExplorer` (open/select) + `MediaExplorerGetLastPlayedFileInfo` (read the *one*
|
||||
last-played/selected file + its range) are the whole contract; there is **no**
|
||||
enumerate-selected-files and **no** register-a-drop-handler-on-the-Media-Explorer API.
|
||||
So ME import is *single-file, pull-on-action* (an action the user fires while a file is
|
||||
selected in the ME), not a push/drop from inside the Media Explorer. **Spike:** confirm
|
||||
`MediaExplorerGetLastPlayedFileInfo` returns a usable path+range for a merely-*selected*
|
||||
(not-yet-played) file, or whether a play is required first.
|
||||
- [ ] Drag-and-drop onto ReaSampler surfaces: accept an OS file drop onto the docked
|
||||
`bank_panel` (and its bank/tab regions) → ingest into the bank → assign. **Honest SDK
|
||||
limit (verified):** REAPER exposes **no** drag-drop registration API; drop handling is on
|
||||
ReaSampler's *own* HWNDs via SWELL/Win32 (`WM_DROPFILES` / an `IDropTarget` on the panel
|
||||
HWND), the same surface the panel already owns. **Assess-and-flag (spike, do not promise
|
||||
here):** a drop *onto the VST3 editor window* — whether the `IPlugView` HWND can accept an
|
||||
OS file drop and relay it to the extension as a bank-ingest request (the instrument does
|
||||
**not** ingest; it forwards a request to the extension over an agreed seam). Reported
|
||||
honestly as a spike because it crosses the two-artifact boundary and the relay mechanism
|
||||
is unproven; if it proves gnarly, drop-onto-panel is the shipped path and drop-onto-editor
|
||||
is deferred.
|
||||
- [ ] "Assign to instance" seam: how the ingest action names the target instance and hands
|
||||
it the new sample id. Lean (build-time residual, not a fork): the active/last-focused
|
||||
instance is the target, discovered via the host context the bridge already resolves; the
|
||||
assignment is the same instance-owned selection state S4 already persists, so a reload
|
||||
picks it up. If the change-detection seam (S9) exists, assignment refreshes hands-free;
|
||||
without it, the ingest action pokes the target instance's reload directly.
|
||||
|
||||
## S9 — bank-generation change-detection (recapture / ingest refreshes instances hands-free)
|
||||
**Goal:** Because instances reference sample **ids**, a **recapture** (M10) landing under
|
||||
the same id — or an **ingest** (S8) touching the active bank — should refresh playing
|
||||
instances **hands-free**, without the user re-opening each editor. Add a **bank-generation
|
||||
counter** to `"reasampler"` ext-state that the extension bumps on any bank-content
|
||||
mutation, and that the instrument polls off the audio thread on a safe cadence, calling its
|
||||
existing `reloadFromBank()` when the generation changes. CONTEXT.md §Phase S
|
||||
(bank-generation seam). **Closes the missing change-detection trigger the recapture
|
||||
auto-update story needs.**
|
||||
**Verify (in DAW):** a recapture that regenerates a sample already assigned to a live
|
||||
instance refreshes that instance's playback within a bounded cadence, no editor re-open; an
|
||||
ingest (S8) that updates the active bank likewise refreshes assigned instances; the poll
|
||||
runs off the audio thread (never in `process`) and triggers the existing off-thread reload
|
||||
path; instances not referencing a changed sample do not audibly glitch (reload is atomic —
|
||||
the S4 graveyard-reclaim handoff); a project with no generation stamp (pre-S9) defaults
|
||||
cleanly (treated as generation 0; first bump refreshes).
|
||||
**Depends on:** S4 (the off-thread `reloadFromBank` + atomic handoff this drives). Writer
|
||||
side is extension-only and independent of S8; consumed by S8 and M10 recapture. Best landed
|
||||
alongside S8.
|
||||
|
||||
- [ ] Writer (extension): a monotonic **bank-generation counter** stamped into
|
||||
`"reasampler"` ext-state (new `ext_keys.h` constant — forever-stable spelling), bumped
|
||||
on every bank-content mutation that changes what an instance would play (capture add,
|
||||
recapture-in-place, sample-remove, move/copy affecting the active bank). Additive to the
|
||||
persist blob; defaults to 0 for projects saved before the stamp exists.
|
||||
- [ ] Reader (instrument): poll the generation over the bridge on a safe **off-audio-thread
|
||||
cadence** (a UI/timer tick, not `process`), compare to the last-seen value, and call the
|
||||
existing `reloadFromBank()` on change — reusing S4's atomic pointer-swap handoff so a
|
||||
refresh mid-play does not glitch. No new audio-thread work; no allocation in `process`.
|
||||
- [ ] Cadence + coalescing: pick a poll interval that is responsive but cheap (build-time
|
||||
residual — a low-frequency UI timer, coalescing multiple bumps between polls into one
|
||||
reload). **Must-verify before build:** that a bridge ext-state read on the instrument's
|
||||
UI/timer thread is safe against a concurrent extension write (the read already tolerates a
|
||||
stale value by design — it reloads on the *next* poll; confirm no torn-read hazard for the
|
||||
single integer generation key).
|
||||
|
||||
## S17 — 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**. Composes with — but is distinct from — **S8** (ingest
|
||||
through the bank: capture/import/drop *into* the bank) and **S13** (drop-to-load *inside*
|
||||
the editor). S17 is the third integration gesture: drop *onto a track's FX button* to
|
||||
instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S
|
||||
artifact, not extension-only.
|
||||
|
||||
- [ ] 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 (resolved on merge to dev).** This wave was authored on
|
||||
> **dev** as a provisional **S7** while dev's Phase S ran only S1–S6. On merge with the
|
||||
> `phase-s` worktree, the worktree's authoritative numbering (S7 stereo, S8 ingest, S9
|
||||
> change-detection, S10–S16) took the lower labels, so drop-and-load was renumbered to
|
||||
> **S17** — the next free label past the worktree's Phase S set. It stays a distinct
|
||||
> integration gesture (drop onto a track's FX button), a sibling of but not the same as
|
||||
> S8 (ingest into the bank) and S13 (drop-to-load inside the editor).
|
||||
|
||||
## 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
|
||||
@@ -273,6 +497,32 @@ build:** the `IReaperUIEmbedInterface` contract + embed message/lifecycle agains
|
||||
`0xdeadf00d`/`0xdeadf00e` opcodes are VST2-only and do not apply.
|
||||
- **`IReaperUIEmbedInterface`** — embed contract + message/lifecycle, against
|
||||
`reaper_plugin_fx_embed.h` (needed only at S6).
|
||||
- **VST3 bus arrangement (S7)** — `setBusArrangements` / `getBusArrangement` and REAPER's
|
||||
mono/stereo instrument-bus expectations, against the vendored Steinberg SDK +
|
||||
`reaper_vst3_interfaces.h`. The channel-mode toggle depends on the output bus
|
||||
re-negotiating cleanly.
|
||||
- **Media Explorer surface (S8)** — confirmed thin against the vendored headers:
|
||||
`OpenMediaExplorer` (open/select) + `MediaExplorerGetLastPlayedFileInfo` (read the one
|
||||
last-played/selected file + range) are the whole contract; **no** enumerate-selected and
|
||||
**no** ME-drop-handler API. Spike: does `MediaExplorerGetLastPlayedFileInfo` return a
|
||||
usable path+range for a merely-selected (not-yet-played) file?
|
||||
- **Drop targets (S8)** — REAPER exposes **no** drag-drop registration API (verified); drop
|
||||
handling is on ReaSampler's own panel HWNDs via SWELL/Win32 (`WM_DROPFILES` / `IDropTarget`).
|
||||
Drop-onto-VST3-editor relayed as a bank-ingest request is an **unproven cross-artifact
|
||||
spike**, not a promise.
|
||||
- **Bank-generation ext-state read (S9)** — confirm no torn-read hazard on the single
|
||||
integer generation key for a bridge read on the instrument's UI/timer thread concurrent
|
||||
with an extension write.
|
||||
- **Drop-and-load (S17) — 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -609,6 +609,55 @@ in-phase later point on the Phase S roadmap (**S6**), sequenced *after* the main
|
||||
`IPlugView` editor exists (it composes with that LICE path), not a someday-note. It is
|
||||
polish rather than a Tier-0 need, so it sequences last — but it is on the roadmap.
|
||||
|
||||
### Addendum — two directions set post-scoping (Daniel, 2026-07-26)
|
||||
|
||||
After Phase S was scoped (D-A..D-D), Daniel set two further directions. These are
|
||||
**settled directions**, not open forks — specced as new Phase S points (S7–S9), not
|
||||
re-litigated. Recorded here per the doc's settled-decisions convention.
|
||||
|
||||
**D-E — Channel mode: mono | stereo, per-instance, bus-negotiated (→ PLAN.md S7).**
|
||||
Captures are often stereo; the current mono downmix is a Tier-0 simplification. The
|
||||
engine gets a **per-instance channel-mode toggle (1 mono / 2 stereo)** that "works with
|
||||
the REAPER audio bus automatically" — the VST3 declares/negotiates its output bus
|
||||
arrangement (`setBusArrangements`) so mono/stereo just works in REAPER's routing. Honest
|
||||
scope: **this is an S3-core extension, not a shell hack** — the core is mono-per-sample by
|
||||
design today, so stereo mode grows a channel dimension (2-channel sample data, per-voice
|
||||
stereo render, per-channel loop/interp). Mono mode keeps the existing downmix path.
|
||||
Cross-mode policy: mono-source-in-stereo → dual-mono; stereo-source-in-mono → downmix
|
||||
(existing). The toggle is instrument-owned per-instance state (D-B: a performance choice,
|
||||
never a bank fact). Sequenced **first after the editor/embed work** because it touches the
|
||||
engine Daniel smoke-tests.
|
||||
|
||||
**Ingest routes through the bank — "option 1"; the extension owns ingest (→ PLAN.md
|
||||
S8 + S9).** Loading a sample into the sampler is **one gesture**: capture/import-into-bank
|
||||
+ auto-assign to the active instance. The **extension owns ingest** (it has arrange
|
||||
access, Media-Explorer access, and the drop-target surface on its own panels); the
|
||||
**instrument stays a read-only bank consumer** — it never captures or imports. Sub-parts,
|
||||
with the honest SDK reality verified against the vendored headers:
|
||||
|
||||
- *(a) Arrange capture → bank → assign* — a one-click action reusing the existing capture
|
||||
path; **never inserts a timeline item** (capture/placement separation intact).
|
||||
- *(b) Media Explorer import → bank → assign* — the ME surface is **thin**
|
||||
(`OpenMediaExplorer` + `MediaExplorerGetLastPlayedFileInfo` are the whole contract; no
|
||||
enumerate-selected, no ME-drop-handler), so ME import is **single-file, pull-on-action**,
|
||||
not a push/drop from inside the ME. Spike: does the last-played-file read work for a
|
||||
merely-*selected* file?
|
||||
- *(c) Drag-and-drop* — REAPER exposes **no** drag-drop registration API; drop handling is
|
||||
SWELL/Win32 on ReaSampler's *own* panel HWNDs. Drop *onto the VST3 editor window* relayed
|
||||
to the extension as a bank-ingest request is a genuine **cross-artifact spike**, not a
|
||||
promise (drop-onto-panel is the shipped path if it proves gnarly).
|
||||
- *(d) Recapture / ingest auto-refresh (→ S9)* — because instances reference sample **ids**,
|
||||
a recapture landing under the same id (M10) or an ingest touching the active bank should
|
||||
refresh live instances **hands-free**. The missing trigger is a **bank-generation counter**
|
||||
in `"reasampler"` ext-state: the extension bumps it on any bank-content mutation; the
|
||||
instrument polls it **off the audio thread** on a safe cadence and calls its existing
|
||||
`reloadFromBank()` on change (reusing S4's atomic handoff). This seam serves both S8 ingest
|
||||
and M10 recapture.
|
||||
|
||||
*The genuine spikes flagged (not decisions Daniel owes, just build-time unknowns):* the
|
||||
ME merely-selected-file read (b), and the drop-onto-editor cross-artifact relay (c). Both
|
||||
are honestly-flagged as spikes in PLAN.md S8, not promised.
|
||||
|
||||
---
|
||||
|
||||
## Where this landed
|
||||
@@ -626,10 +675,19 @@ into **Phase S** — a native VST3 sampler as a **second build artifact** alongs
|
||||
4. **D-D → embedded TCP/MCP UI scheduled** (**S6**), after the main editor exists — on the
|
||||
roadmap, not deferred.
|
||||
|
||||
**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
|
||||
Two further directions set post-scoping (2026-07-26; see the Addendum in §4):
|
||||
|
||||
5. **D-E → channel mode (mono | stereo), per-instance, bus-negotiated** (**S7**) — an
|
||||
S3-core channel-dimension extension, sequenced first after the editor/embed work.
|
||||
6. **Ingest through the bank ("option 1"), extension-owned** (**S8**) + **bank-generation
|
||||
hands-free refresh** (**S9**) — one-gesture capture/import + assign; the instrument stays
|
||||
a read-only consumer.
|
||||
|
||||
**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1–S6 the original
|
||||
dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded
|
||||
UI; then **S7** stereo, **S8** ingest, **S9** change-detection); **CONTEXT.md §Phase S** is
|
||||
the spec (seam-field semantics, scope contracts, the channel-mode / ingest / bank-generation
|
||||
contracts, the pure/shell split, the must-verify SDK/bridge surfaces). This doc is the
|
||||
framing/decision record they point back to. The "no PLAN.md footprint" era is over.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user