docs: record Ξ-W2-T1, the resample bake chain, and file its two deferrals

This commit is contained in:
2026-08-01 18:11:46 -04:00
parent 7ecb3aa470
commit 9f17df1420
3 changed files with 155 additions and 174 deletions
+16 -174
View File
@@ -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.
---