From 9f17df142019c7754ec77df3058bd483c77577d9 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 1 Aug 2026 18:11:46 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20record=20=CE=9E-W2-T1,=20the=20resample?= =?UTF-8?q?=20bake=20chain,=20and=20file=20its=20two=20deferrals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/COMPLETED.md | 85 +++++++++++++++++++++ docs/PLAN.md | 190 ++++------------------------------------------ docs/TODO.md | 54 +++++++++++++ 3 files changed, 155 insertions(+), 174 deletions(-) diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index 533894f..de49d1a 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -669,3 +669,88 @@ both corrected the initial reading of each. All visual outcomes remain **pending Daniel's by-eye sign-off on `dev`** — nothing was verified in a live REAPER window; all measurement was against an offscreen bitmap in a standalone harness. Not recorded as accepted. + +### Ξ-W2-T1 — resample-bake-chain + +The one-click in-sampler resample: dial → bake → dial-again, run without leaving the +sampler. A single click renders the dialed sound through the instrument's own voice +path, banks the result, re-points the instance at it, and hands the parameter set back +neutral — with the recapture's superseded predecessor never deleted, only retired to +prune's reclaim pool. + +**The architecture decision was this track's first deliverable, and Daniel ratified +both halves of it.** Decision 1 (how the click crosses to the extension) is **(1b)**: +the editor invokes the extension's bake action directly over the VST-host bridge — +`NamedCommandLookup` on `"_" + channelCommandId(...)`, then `Main_OnCommandEx` — so +there is no request poller, no nonce, and no cross-process handshake. This dissolves +the S13 DEGRADED verdict rather than re-litigating it. Decision 2 (what renders the +audio) is **(2c)**: the instrument renders in-process and the extension banks the +file — taken **over the plan's leaning toward (2a)** on an engine-version-skew +argument: under (2a) the extension's own copy of the voice engine would render audio +the user heard through the VST3's separately-installed copy, and the format ladders +do not catch a behavioral divergence between the two. (2c) also leaves the +extension's link graph untouched, preserving `component_state_io`'s split-out purpose +of keeping engine object code out of the extension. + +**What shipped:** +- New pure modules: `src/core/instrument/bake/` — a fifth peer of `engine/`/`map/`/ + `note/`/`ui/` under `core/instrument/`, holding `bake_plan`, `bake_render`, and + `bake_reset` — plus `core/model/resample_name` and `core/wire/bake_wire`. +- New shells: `shell/instrument/instrument_bake` (the instrument's half: render, + stage the WAV outside the bank, publish one `rsbake_` request, invoke the + extension's action synchronously, read the outcome back, adopt + reset) and + `shell/capture/bake_land` (the extension's half: scans every open project tab for + pending requests, lands the ones belonging to the loaded project, refuses the rest). +- One new `ActionTableRow`, `RESAMPLE_BAKE`, registered through `main.cpp`'s existing + data-driven table. +- The instrument's guarded ext-state write surface grew from one prefix (`rsusage_`) + to two (`+ rsbake_`); the read-only-**bank** invariant holds because a bake + request key is not bank state, and the structural prefix guard still refuses + `banks`/`view`/`tail`/`assign`. + +**Reset-scope classifications made at review, against Daniel's ratified rule** — these +parameters were absent from both ratified lists, so the classification itself is this +track's durable output: +- **Play mode → RESET, to Trigger.** The bake's product is a finished one-shot + carrying its own attack, span, and release; Gate would re-gate it and re-truncate + the printed tail on every iteration, breaking "iteration composes indefinitely." The + user-visible consequence: after a bake the instance is in Trigger, and a sustained + instrument needs Gate re-dialed by hand. +- **Start point → RESET.** +- **Channel mode and preview velocity → SURVIVE.** +- `resetAfterBake` defaults everything and copies back only the survivors, so a + parameter added later resets by default. + +**Two behaviors worth recording because they are user-visible:** +- A bake fired from an instance in a **background project tab refuses** with + `BakeStatus::WrongProject` rather than risking a write into the wrong project's + bank — landing requires three-way agreement between the request's tab, the + session's loaded project, and the focused tab. +- A crash-stranded bake request is **cleared, not landed**, past a 30-second + staleness window (`kMaxRequestAgeSeconds`). +- Extension presence: the resample affordance reads **cleanly unavailable, not + silently lossy**, when the extension is not loaded — `bakeAvailable` gates the + editor's paint state and `runBake` refuses up front with "resample needs the + ReaSampler extension loaded" if asked anyway. + +**Not demonstrated.** Nothing was verified in a live REAPER session. The +audible-and-faithful, iteration-composes, save/reload, and arrange-untouched +acceptance criteria are structural in the code and untested in a DAW. Three facts +remain DAW-unverifiable and are handled defensively rather than asserted: +`NamedCommandLookup`'s return on an absent command, `Main_OnCommandEx`'s `flag` +semantics, and whether a `WM_TIMER`-issued invoke is honoured. Daniel's manual +verification is still owed. + +**Naming and lineage — proposed, not ratified, and still open jointly with +Ξ-W1-T1's lineage-record question.** This track's proposal: `Kick` → `Kick r2` → +`Kick r3`, incrementing rather than stacking; a replace keeps the source's name; +machine-readable lineage rides `OriginRecord::parentSampleId`, written at birth +(landed by Ξ-W1-T1). The proposal is implemented (`core/model/resample_name`) but not +itself a ratified decision. + +**Deferred, not done — logged to `docs/TODO.md`:** `Sample::sourceMode` has no value +meaning "produced by the instrument" (appending one is a forward-incompatible +bank-format change under the current deserializer, which fails the whole bank blob on +an out-of-range value — it wants its own decision); and `instrument_bake` copies the +interleaved render buffer into a `std::vector` for the WAV build, roughly +doubling peak memory for a large bake. diff --git a/docs/PLAN.md b/docs/PLAN.md index 14afbbb..b2d9ade 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -435,180 +435,22 @@ head governs every step after it. #### Ξ-W2-T1 — `resample-bake-chain` -**Goal.** One click bakes the dialed sound into a bank capture, re-points the instance at -it, and hands the instrument back at neutral — the dial → bake → dial-again loop, run -without leaving the sampler. - -**Consolidates item 15 (the chain; the popup UI is Ξ-W3-T1).** - -**Surface boundary — owns:** whichever seam the architecture decision selects (see below), -plus `shell/capture/capture_orchestrator` (a resample entry point alongside -`captureAndIndexOne`), `core/model/bank_model` + `owned_manifest` at the add boundary, -`shell/actions` (a new action, if the decision needs one), and the instrument-side bake -trigger. **Does not own** the capture-signal popup's UI — that is Ξ-W3-T1. - -##### The architecture decision — this track's first deliverable, before any chain work - -Item 15 requires a click **inside the VST3 editor** to cause a **bank write**, and the -instrument is a read-only bank consumer by invariant. The one prior attempt at this -crossing (S13) closed **DEGRADED** and was deferred. Resolve this before building the -chain. Two orthogonal choices: - -**Decision 1 — how the click crosses to the extension.** -- **(1a) Ext-state request key + extension-side timer poller**, with a claim/clear nonce — - the shape S13 spiked and deferred. It needs a new instrument WRITE seam into ext-state, - though the `rsusage_`-prefixed guarded write already establishes the precedent that such - a seam can exist without weakening the read-only-*bank* invariant. Known cost: the - cross-process handshake race the S17 spec rejected. -- **(1b) Direct action invocation over the VST-host bridge — the lead candidate.** A - REAPER-hosted VST3 already resolves REAPER API functions **by name** over the host - callback (that is how the instrument reads `GetProjExtState`), and can fetch its own - host context — the track/take/project it was instantiated in. If `NamedCommandLookup` - and `Main_OnCommand` resolve the same way, the editor can invoke the extension's bake - action **synchronously**, with no request key, no poller, and no nonce — which dissolves - the S13 problem for this case rather than re-litigating it. **[verify]** against - `vendor/reaper-sdk/sdk/` (`reaper_plugin.h`, `video_processor.h`, - `reaper_plugin_functions.h`) that both resolve over the callback, and that calling them - from the editor's UI thread is safe. This is a verification task, not an assertion — do - not build on it until confirmed. -- **(1c) No crossing — a bindable extension-side action only.** Fallback. It abandons - Daniel's "from directly inside the ReaSampler 9000" framing, so it is the answer only if - (1a) and (1b) both fail. If it is taken, say so explicitly rather than quietly shipping - a panel button. - -**Decision 2 — what actually renders the audio.** -- **(2a) Headless voice-engine render, extension-side.** The extension reads the instance's - component state, reconstructs the engine from the shared pure core, renders the - programmed note, and banks the result through the normal capture add-path. Exact and - fully deterministic. **Trade-off:** the extension currently links `component_state_io` - but deliberately **not** `sampler_core`/`pitch_shift` — `component_state_io` was split out - precisely to avoid pulling the voice engine into the extension. This reverses that. -- **(2b) REAPER offline render of the hosting track**, with the programmed note. Reuses - `OfflineRenderBackend` and with it the whole precision-invariant apparatus — exact - bounds, bit-identical repeats, relative paths, tail control — for free. **Trade-offs:** - it needs a *temporary* arrange mutation to program the note (the realtime backend's - temp-track-created-and-removed-cleanly is the precedent), and the existing scope model - has no "this one FX only" scope, so post-instrument track FX would be baked in unless a - new bypass scope is added. The arrange must be byte-identical afterward. -- **(2c) The instrument renders, the extension banks it.** The instrument produces the - audio (it already owns the engine) into a temp file or shared buffer; the extension - moves it into the bank and indexes it. Splits the work along the existing ownership - line — but adds a handoff artifact and its cleanup. - -**Recommended sequence for the decision:** verify (1b) first — if it holds, the crossing -is nearly free. Then pick between (2a) and (2b) on the strength of the -"only-the-instrument, not the track chain" requirement: Daniel's ask says "send a trigger -or gate **through the sampler** offline," which favours (2a) or (2c) unless (2b) gains a -single-FX bypass scope. Record the verdict in this track's review, and if the verdict is -DEGRADED, escalate rather than improvise — the fallback is (1c) + (2a). - -**Behavior — the chain.** -- **One gesture, whole chain.** A single click performs: offline pass → capture → bank - update (replace or add-distinct) → instance re-point → parameter reset. **One action - from the user's side, not a wizard.** -- **Offline pass through the instrument's own processing.** The audio is produced by - sending a trigger or gate through the sampler **offline** — the instrument's own voice - path, with filtering, pitching, and amp exactly as dialed. The recapture is of that - processed output, not of the raw source. -- **The recapture is a bank capture like any other** — project-relative, indexed, - browsable from any surface that browses the bank, recorded in the owned-file manifest, - and governed by the same safety rules as every file the system creates. -- **The note is the capture's root.** The rendered note is the capture's root note — which - is exactly why **the root-note parameter is not reset by resampling**: capturing at root - is what makes root survivable, and resetting it would detune every subsequent iteration. -- **Gate's hold and tail come from the programmed window.** The programmed note length is - the Gate hold bound — the gate holds for the note length, then releases; a Gate loop - cycles within the held span and the render still terminates. The **end offset** is the - home of the tail policy: captured time past the note's end is where the release rings. -- **Replace, or add distinct.** When nothing else references the source capture, the - recapture **replaces** it as the bank entry; when other references exist, the original - entry stays and the recapture is **added as a new distinct capture**. "Other reference" - means **any usage of the source capture tied to it by the provenance/recapture system** - — Ξ-W1-T1's records. Read plainly: the reference universe is the resample system's own - lineage records, **not** the prune-protection universe. Bank multi-membership, items - placed in the arrange, and a plain hold by another instance outside any recapture - lineage do **not** force add-distinct, and need not for safety — the superseded file - survives until prune, and prune's protection universe is unchanged and broader. -- **Replacement never destroys audio bytes.** "Replace" means the bank entry now denotes - the recapture; the superseded file is not deleted. Prune remains the system's only - file-deletion authority — resample writes a new file and retires the old one to - reclaimable-by-prune status; it never overwrites or deletes it. Until a prune reclaims - it, the pre-bake audio survives on disk — **the iterate loop's built-in recovery floor.** -- **The instance re-points** at the new/corrected bank capture and plays the baked sound. -- **Parameters reinitialize to default** — destructive to the dialed settings **by - design**: the processing now lives in the recaptured audio, and neutral controls are the - starting point for the next iteration. -- **Reset scope: only what the bake baked in.** Ratified by Daniel, not derived: - - **Reset** (their effect is in the audio): the envelope contours — staged and spline - alike — the filter parameters, master gain, the pitch envelope/engine settings, the - velocity transfer curves (their effect at the programmed velocity is in the audio), and - the loop points (they shaped the render, and old positions are meaningless against new - audio). - - **Survive** (mapping facts, not present in the audio): the **root note**, whatever - remains of key mapping (key-tracking; any key-range concept Θ-W1-T1 settled), and the - VOICE group (polyphony behavior leaves no trace in a single rendered note). -- **No timeline item, ever.** Resampling is a capture act: it writes a file to the bank and - updates the index; nothing is placed in the arrange view. Any framing that auto-inserts - the recapture is invalid — and if architecture (2b) is chosen, its temporary programming - item must leave the arrange byte-identical. -- **Undo/recovery: a plus, not a requirement.** Welcome if it falls out cheaply; the - feature ships without it. The guaranteed recovery path is the superseded file surviving - on disk until a prune reclaims it. - -**Acceptance criteria.** -- On a dialed-in instrument, one click yields all of: a recapture in the bank, the instance - holding that recapture, and the baked-in audio parameters at their defaults — with the - root note and the other surviving mapping parameters untouched. -- **The bake is audible and faithful:** after the click, playing the programmed capture - note (root, at the programmed length, offsets, and velocity, in the active mode) through - the now-neutral controls sounds as the dialed instrument sounded just before the click — - the processing has moved from the controls into the audio. -- **A Gate-mode bake terminates on its own:** the gate holds for the programmed note - length, then releases — even with loop-sustain active, the render ends. No indefinite - capture. -- **After the bake the root note is unchanged** — iteration never detunes; the next bake - plays the same root. -- **Sole-reference case:** the bank afterwards shows the recapture where the source - capture's entry was; no other bank entry is disturbed. **Other-references case** - (provenance-tied usage of the original exists, computed by Ξ-W1-T1): the original entry - is untouched, a distinct new entry appears, and **every other holder of the original - sounds exactly as before.** -- **The click deletes no file:** the superseded audio file still exists on disk afterwards, - and only a later prune — under the settled orphan rules, only when nothing references it - — can reclaim it. -- **The arrange timeline is untouched:** no item appears anywhere, on any track, and if a - temporary programming item was used it is gone and the arrange is byte-identical. -- **Iteration composes:** dial → click → dial → click bakes the second pass onto the - first's result, repeatable indefinitely. -- **Save/reload:** an instance holding a recapture reloads and plays it exactly like any - other loaded capture. -- The bake adds nothing to `process()` — the offline pass runs off the audio thread. - -**Open questions.** -- **The architecture decision [propose, with a verification step].** See above. The (1b) - bridge verification is **[verify]**; the choice among the candidates is **[propose]**. -- **Extension presence [propose].** The instrument plays self-contained with the extension - absent, but the bank is the extension's surface and resampling mutates the bank. The - natural answer is that resample **requires the extension present and is cleanly - unavailable — not silently lossy — without it**; propose the exact behavior at review. -- **Reset-scope edge cases [verify].** The rule is settled and the per-parameter - classification is ratified. Only a genuinely new parameter — one arriving with a Θ track - and absent from the ratified lists — is classified **against the rule** and surfaced at - review. Not a new Daniel call. (Θ adds: curve exponents → reset; spline contours → - reset, already named; the filter's velocity/key-tracking mod → reset with the filter; - loop crossfade → reset with the loop points; the Staged/Spline mode flag → classify.) - **Phase Γ's additions are NOT this track's** — this track ran ahead of Γ, so the values Γ - introduces (rate, pitch offset, the limiter flag, the loop enable) are amended in by - **Γ-W3-T2 `bake-reset-amendment`** afterwards, against what this track actually shipped. - Γ-W4-T1 then adds the host-notification obligation over the completed list. Nothing here - needs to anticipate either. -- **Naming and lineage [propose, jointly with Ξ-W1-T1's lineage-record question].** When - add-distinct fires, the new capture needs a display name (derived from the original?), - and the bank some way to read iteration lineage across repeated bakes. One proposal, - both tracks. -- **Provenance of the recapture — homed in item 17**, i.e. Ξ-W1-T1. A resample's recipe is - the instrument's own settings, not a track's chain. Nothing to decide here beyond - consuming that answer. +**Landed** — see `docs/COMPLETED.md` for the full narrative. The architecture decision +(this track's first deliverable) is ratified: **Decision 1 = (1b)**, the editor invokes +the extension's bake action directly over the VST-host bridge +(`NamedCommandLookup`/`Main_OnCommandEx`), no poller/nonce, dissolving the S13 DEGRADED +verdict; **Decision 2 = (2c)**, the instrument renders in-process and the extension +banks the file, taken over the plan's leaning toward (2a) on an engine-version-skew +argument. **Extension presence** resolved to cleanly-unavailable, not silently lossy — +the affordance refuses up front when the extension is not loaded. **Reset-scope edge +cases** classified against the ratified rule: play mode → RESET to Trigger, start point +→ RESET, channel mode and preview velocity → SURVIVE. **Phase Γ's additions remain +NOT this track's** — Γ-W3-T2 `bake-reset-amendment` still amends the reset list for +Γ's own new values, unaffected by this landing. **Naming and lineage** is proposed +(`Kick` → `Kick r2` → `Kick r3`) but not itself ratified — still open jointly with +Ξ-W1-T1's lineage-record question, which Ξ-W1-T1 answered on its own side +(`OriginRecord::parentSampleId`). Nothing was verified in a live REAPER session; +Daniel's manual verification is still owed. --- diff --git a/docs/TODO.md b/docs/TODO.md index 43f19d5..a9c1fdf 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -270,6 +270,60 @@ decode; sounding voices are still destroyed across it, with no ghost-resurrectio a pre-v10 blob still lifts; and `getLatencySamples()` still derives from persisted state rather than from a transient the deactivate cleared. +## `Sample::sourceMode` has no value meaning "produced by the instrument" + +**Context (what shipped — Ξ-W2-T1, resample-bake-chain).** A resample bake's landed +`Sample` entry (`bake_land.cpp`) never sets `sourceMode`; it is left at the struct +default (`SourceMode::MasterMix`) rather than recording that the entry's audio came +from the instrument's own offline render, not from a capture backend. + +**The wart.** A baked capture is indistinguishable, by `sourceMode`, from a master-mix +render — the bank has no way to tell "this file was produced by ReaSampler 9000" from +"this file was rendered off the master bus." + +**Intended fix.** Add a `SourceMode` value for instrument-produced audio and set it at +the one landing site. + +**The constraint the fix MUST handle.** `bank_model.cpp`'s deserializer rejects any +`sourceMode` value outside `MasterMix(0)..Realtime(5)` by failing the whole bank +blob's parse (`parseSample` returns `false`), not just that one field — so appending a +new enumerator is a forward-incompatible bank-format change: an older extension build +reading a newer project's bank would refuse to load it entirely. This needs its own +decision (a version-gated field, or accepting the compatibility cost) rather than a +one-line enum append. + +**Priority / risk.** Low / deferred. Logged at Ξ-W2-T1's review rather than folded in. + +**Done looks like.** A baked capture's `sourceMode` reads as instrument-produced, and +the compatibility question (how an older build reads a bank containing the new value) +is answered rather than left to fail closed by accident. + +## `instrument_bake` doubles peak memory on the WAV build + +**Context (what shipped — Ξ-W2-T1, resample-bake-chain).** `runBake` (`instrument_bake.cpp`) +copies the render's interleaved `float` buffer (`BakeAudio::interleaved`, `AudioSample += float`) into a `std::vector` before handing it to `buildFloat32Wav`, which +takes doubles and narrows back to float for the bank's 32-bit-float WAV contract. + +**The wart.** The copy roughly doubles peak memory for the bake — an 8-byte double +holding a value that started and ends as a 4-byte float — for the duration of the WAV +build on a large bake. + +**Intended fix.** Either give `buildFloat32Wav` (or a sibling entry point) a +`float`-input overload so the bake path narrows nothing it doesn't already own in +`float`, or narrow lazily during the WAV build instead of pre-copying the whole +buffer. + +**The constraint the fix MUST handle.** `buildFloat32Wav`'s `double` parameter is +shared with every other caller in `core/capture/wav_codec`; a fix must not change +those callers' contract or add a second WAV-building code path to maintain. + +**Priority / risk.** Low / deferred. Logged at Ξ-W2-T1's review; correctness is +unaffected, only peak memory on a large bake. + +**Done looks like.** A bake's peak memory no longer includes a full double-precision +copy of the rendered buffer, with `buildFloat32Wav`'s other callers unchanged. + ## The deck layout rework — SPECCED, and the original shape SUPERSEDED **Status (2026-08-01): no longer a deferral. The design notes Daniel owed this entry have