Files
reasampler/docs/PLAN.md
T

793 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN
The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas and
sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be
dispatched against directly.
## What this doc is, and how it relates to the others
- **`docs/PLAN.md`** (this file) — the active on-deck specification list. Each track is
written so a specialist brief is writable from this file alone: goal, consolidated
source items, surface boundary, behavior, acceptance criteria, open questions,
prerequisites.
- **`docs/TODO-1.0.md`** — retained as the **verbatim-provenance appendix**. It holds
Daniel's raw asks and every answer round, unedited; where this plan compresses a
behavior bullet, that file is the backing record. Items are cited here by number
(e.g. "consolidates items 1, 8, 14"). It is not a work queue any more; this file is.
- **`docs/TODO.md`** — deferred follow-ups with recorded rationale, unrelated to the
seventeen (with one flagged intersection: see "Flagged for awareness" below).
- **`docs/COMPLETED.md`** / **`docs/ARCHIVE.md`** — doc-keeper's. When a track here
finishes, its point is removed from this file and appended to `COMPLETED.md` with any
deviation between landed code and spec noted.
- **`docs/product/`** — the product-design reasoning behind prior phases. Grep for a
cited section rather than reading a file whole.
**Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
transliterate: **Θ → `th`**, **Ξ → `xi`**. So Θ-W1-T1 dispatches into
`pth-w1-t1-zone-retirement`.
## Decision state
Everything carried forward from `TODO-1.0.md` is classified **[verify]** (answerable by
reading code or running the DAW) or **[propose]** (a design call made at implementation
review with a proposal, not a Daniel call); that classification is preserved per
question, attached to the track that will answer it.
Θ-W3-T1's two genuine **[Daniel]** questions — which no amount of code-reading could
answer — are both ruled on and the track has landed; see `docs/COMPLETED.md` for the
full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cutoff/Q/
morph/drive/mod amount/key-track, every envelope stage time and level; root note, loop
span, and start frame still trigger a full reload). **Mid-stage rule = candidate (iv),
hold normalized stage position** (φ = elapsed/duration held fixed across a duration
change, then advancing at 1/newDuration). No other track in this plan currently carries
an unanswered **[Daniel]**-class question.
### Flagged for awareness — not blocking, but decision-grade
1. **Item 15 has an unresolved cross-artifact seam, and it is the phase's largest
unknown.** The instrument is a read-only bank consumer by invariant
(`src/shell/instrument/CLAUDE.md`), and the one previous attempt at an
instrument→extension relay (S13) closed with a **DEGRADED** spike verdict and was
deferred (`docs/TODO.md`). Resample requires that crossing. Ξ-W2-T1 opens with the
architecture decision and this plan names the candidates rather than assuming the
deferred relay shape; see that track. Consequence to hold: if every candidate fails
verification, item 15's "one click from inside the VST" framing is what gives, not
the read-only invariant — the fallback is a bindable extension-side action.
## Phase-wide acceptance criteria
These bind every track in both phases and are stated once here rather than repeated
per track.
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
- **More directories is a must; more files is good; ~600-line file ceiling.** The
ceiling is the *bar*; a responsibility seam is the *method*. Bisection-to-hit-the-
number is rejected. `sampler_core.cpp` (956 lines) is the standing documented
hot-path exception — Θ-W1-T1 re-seams it, and any surviving over-ceiling TU must
carry the same explicit justification.
- **Templates where earned** — compile-time dedup with zero runtime cost, off the hot
paths. Not for types that differ in name only.
- **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts anywhere;
prefer static polymorphism where the types are compile-time-known.
### Performance guardrails
Root `CLAUDE.md`'s five extension-side guardrails (peaks envelope compute, audition,
realtime-capture tick, JSON, `FxBypassGuard`) are unchanged by this plan; no track here
touches them. The instrument adds a sixth surface that binds every Θ track:
- **`process()` — the per-voice-per-sample path — takes no new indirection.** The
filter tick, the envelope evaluation (staged *and* spline), the pitch-shift read, and
the loop read all sit on it. Concrete, inlineable types only: **no `IEnvelope`, no
`IFilter`, no virtual per-voice `tick()`**. A filter with two modes is a
branch-predictable switch or a compile-time-known dispatch, never a vtable. Spline
evaluation is a binary search over a point array, not a polymorphic curve object.
- **No allocation, no file I/O, no bridge call in `process()`.** The off-audio-thread
`reloadInstrument` + atomic pointer swap stays the only way new state reaches the
audio thread. Every new parameter this plan adds follows that path.
- **A split that would add a hot-path indirection is out of scope — rework it or drop
it.**
### Product invariants
- **Capture and placement are separate acts.** No track here may place a timeline item.
Item 15's bake explicitly must not, and one of its candidate architectures uses a
*temporary* arrange mutation — that candidate must leave the arrange byte-identical.
- **Prune is the single, exclusive file-deletion authority.** Item 15's "replace" never
deletes bytes; item 17's consolidation may not weaken any protection prune has today.
- **The instrument never writes the bank.** Item 15 is the first feature that needs to,
and it resolves that by *asking the extension*, not by breaching the invariant.
- **Migration bar: a project saved before a change reopens sounding identical.** Holds
everywhere except item 16's genuinely-multi-zone case, where Daniel deliberately
relaxed it.
- **Every pure module gets a `<module>_tests` target** that runs without REAPER or a
DAW. New pure modules in this plan are not optional-test.
---
## Phase Θ — ReaSampler 9000: one parameter set, a filter, shapeable envelopes, a legible editor
**Ships:** the instrument with the zone system retired, a resonant HP/LP filter stage in
the voice path, curve-shapeable and spline-drawable envelopes on all three EGs,
Gate-mode loop sustain, and a re-laid, high-DPI-clean editor — plus the two extension
drag/drop defects that block getting captures into it.
**Consolidates items** 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16.
**The organizing constraint.** Three surfaces in the instrument are single-writer by
nature and dictate the wave shape:
1. **The parameter model** (`zone_params.h` + `component_state_io`) — every parameter
addition touches both. Two tracks adding parameters concurrently is a merge fight
and two competing `ComponentState` version bumps.
2. **The voice render path** (`sampler_core.cpp`) — filter insertion, envelope
evaluation, loop read, and the Trigger tail all live there.
3. **The Sample face** (`editor_paint_sample.cpp` 516 lines / `editor_input_sample.cpp`
583 lines) — every UI item repaints it, and both are already at the ceiling.
Parallelism in this phase therefore comes from **splitting the Sample face into bands**
(done once, in Θ-W1-T1) and from **extension-side work being genuinely disjoint** — not
from running two parameter-model tracks at once. Where a wave has one track, the
collision is real and the serialization is the correct answer.
---
### Θ-W1 — Collapse and re-seam
**All three tracks have landed** — Θ-W1-T1 (`zone-retirement`), Θ-W1-T2
(`capture-handoff-bugs`), and Θ-W1-T3 (`filter-dsp-port`) — see `docs/COMPLETED.md` for
the full narrative of each. Between them: the zone subsystem is retired, the wave's two
responsibility seams (the `sampler_core` split, the Sample-face band split) are in
place, the two extension-side capture-handoff defects are fixed, and the filter DSP has
landed as a standalone pure module — Θ-W2-T1 has since wired it into the voice path (see
below).
---
### Θ-W2 — Filter in the voice path; waveform and chrome bands
**Depends on W1 for:** the one-parameter-set model and the `sampler_core` seam that T1
writes the filter into; the `filter` module T1 wires up; the Sample-face band split and
band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which decides
what T3's piano strip displays. Authoring any of this against the per-zone model means
writing storage plumbing W1 deletes.
**All three tracks have landed** — Θ-W2-T1 (`filter-voice-path`), Θ-W2-T2
(`stereo-waveform-lanes`), and Θ-W2-T3 (`toolbar-and-piano-strip`) — see
`docs/COMPLETED.md` for the full narrative of each. Between them: the filter sits in the
per-voice signal path with its own deck, the waveform band shows both channels in
stereo mode behind a type-enforced full-height overlay contract, and the chrome band's
toolbar and piano strip are cleaned up per spec. The three tracks were disjoint by
band — T1 owned the parameter model and the deck band, T2 owned the waveform band, T3
the chrome band — and none re-allocated the band stack.
---
### Θ-W3 — Live parameters, then the staged envelope system
**Depends on W2 for:** the filter envelope's existence — items 1, 8, and 14 govern
*three* envelopes, and the filter is the third; and for the Filter deck, which must
exist before it can receive a corner radio switch and inner curve dials. T1 additionally
depends on W2-T1 for the filter itself: the filter is the first control set where the
latched-at-note-on delivery model fails audibly, and it is what made the defect visible.
**Both tracks have landed** — Θ-W3-T1 (`live-parameter-delivery`) and Θ-W3-T2
(`staged-envelope-curves`) — see `docs/COMPLETED.md` for the full narrative of each.
Between them: every continuous playback control (filter cutoff/Q/morph/drive/mod
amount/key-track, every stage time and level on all three envelopes) now reaches a
sounding voice live instead of latching at note-on, and the envelope-overlay editor
grew from an amp-only fixture into the shared graphical surface for all three
envelopes — a corner radio switch per deck (none active by default), curve-shapeable
segments on every sloped stage (0.110 exponent, an inner dial paired with an overlay
knot), the release-right-anchored AHDSR layout against the pitch envelope's 1:1 AHD,
and the Trigger amp/filter fade pair folded into a Trigger AHD, consolidating what
were two staged-shape mechanisms into one. The Trigger × Preserve end-of-sample click
is fixed; the landed fix is wider than scoped, also ringing out Gate × Preserve ×
source-exhaustion, previously a hard cut. The ordering was deliberately serial — T1's
live-delivery mechanism landed first so T2's new curve exponents and Trigger AHD
fields were authored directly into it rather than backfilled afterward.
---
### Θ-W4 — Loop sustain and the velocity deck
**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain
is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves
modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for
the filter's existence and on W2-T3 for the preview button's toolbar position.
**Both tracks have landed** — Θ-W4-T1 (`gate-loop-sustain`) and Θ-W4-T2
(`velocity-deck-and-bipolar-curves`) — see `docs/COMPLETED.md` for the full narrative of
each. Between them: loop points are now a usable feature, with a Gate-mode loop acting
as the sustain and a parameterized crossfade at the seam; and the three velocity-curve
popups (amp, pitch, filter) now live together in a new VELOCITY deck group, with the
pitch and filter curves bipolar and flat-by-default so their modulation is off until
drawn, while the amp curve stays unipolar and unchanged, and the preview button's text
is replaced by a drawn play-triangle glyph. Params payload reached v11 with T1's loop
block and v12 with T2's velocity→pitch curve appended after it.
---
### Θ-W5 — Spline EGs
**Depends on W4 for:** the bipolar velocity-curve domain (W4-T2) — the spline algorithm
is **singly implemented and multi-referenced**, so its enhancement must land against the
final consumer set, and the last consumer to change domain is the pitch/filter velocity
curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline mode" is
only a real, testable rule once Gate has something to be unavailable *for*. It also
depends on W3's radio switch, which is how a spline contour reaches the overlay at all.
**One track.** The spline algorithm is one implementation with several consumers; a
second concurrent writer to it collides by construction.
#### Θ-W5-T1 — `spline-egs`
**Goal.** Offer a free-drawn alternative to every staged envelope: switch any EG from
Staged to Spline and draw the contour directly — with the monotone-spline machinery
already proven by the velocity curve, enhanced so sharp corners are possible rather than
everything smoothed.
**Consolidates item 3.**
**Surface boundary — owns:** `core/instrument/engine/velocity_curve` (the shared spline
implementation — hard points), the spline evaluator on the voice-render path,
`core/instrument/ui/curve_popup` + the overlay's spline mode
(`envelope_overlay`/`envelope_edit`), `core/instrument/map/component_state_io` (dual
state persistence), and the deck band's **disabled-knob rendering only**.
**Behavior.**
- **Mode toggle per EG.** Every processor that has an envelope (pitch, filter, amp) can
switch its Staged EG to a **Spline EG**.
- **Dual state — save but inactive.** Both the Staged and the Spline state are persisted;
switching modes keeps the inactive one **saved but inactive**. No conversion, no
discard — round-tripping Staged↔Spline restores the other mode's shape untouched.
- **Gate unavailable in Spline mode.** Gate mode is **not available while a Spline EG is
active**; the spline **always covers the full sample length** — a pure time function
over the sample, i.e. the Trigger/one-shot playback model.
- **Time axis: normalized, visually 1:1.** The contour is normalized to the full sample
length and the overlay represents **the time axis of the actual sample visually 1:1**.
Consequently a different-length capture rescales the stored contour to its own length,
shape preserved proportionally.
- **Point-editing grammar. Left-click adds a point; right-click deletes a point;
control-click toggles a point hard/smooth.** (The source doc's second follow-up said
alt-click delete; the third **supersedes** it with right-click — matching the
velocity-curve popup's already-shipped right-click node delete, giving **one
point-editing grammar across both spline consumers, with no migration on either side**.)
- **Point-count ceiling: 128.** Floor: the two endpoints implied by full-length coverage.
Daniel floated 64 and raised it to 128 explicitly so the cap does not limit **long
rhythmic phrases, which require high resolution to be interesting** — at roughly two
points per articulation event, 64 is about two bars of 16ths and 128 about four. **The
ceiling is a musical bound, not a performance one** (segment lookup is logarithmic; the
editor's 8 px minimum node separation is the practical density limit anyway). **An
engineer tempted to lower this number must read that motivation first.**
- **Staged controls disabled while Spline is active.** That envelope's staged segment
knobs are **disabled — inert, not merely inaudible** — including their inner curve dials
(the dial is part of the knob). The dormant staged state is edited only by switching
back to Staged.
- **Not globally monotone.** Contours don't have to rise and fall and are not globally
monotone; the monotone guarantee is **per-segment** — all soft points between any hard
points are smooth/monotone, with no overshoot between adjacent points.
- **Default contour: a smooth y = 1 x** — a smooth downward slope over the full sample
length. (The Spline-EG default; not a change to the velocity→amp curve's existing
default.)
- **Hard points — the algorithm enhancement.** The one spline implementation is enhanced
to support **multiple segments that don't minimally smooth**, so hard points are
possible: the contour is defined by **one or more monotone spline functions** which
together form the full time function — e.g. the first three points form a curved segment
connecting **at a sharp angle** to the next three, finishing the contour over the full
sample length. A hard point does no curve smoothing on either adjacent segment; the
natural sharp angle stands instead of a continuous derivative.
- **Shared across consumers.** The enhancement applies to the one implementation and flows
to every consumer: the **velocity→amp transfer curve** gains hard-point support by the
same change, as do the pitch, filter, and amp EGs in spline mode.
- **Hot-path constraint.** Spline evaluation runs per-voice per-sample. It is an indexed
binary search over a point array plus a polynomial evaluation — **not** a polymorphic
curve object, and not a per-sample allocation. With a 128-point ceiling the search is
≤7 steps; cache the segment index across samples where the read position is monotone.
**Acceptance criteria.**
- Each of the pitch, filter, and amp EGs offers a Staged/Spline mode switch; in Spline
mode the overlay (via the W3 radio switch) shows and edits the drawn contour, and played
notes audibly follow it.
- Left-click on the contour adds a point; right-click on a point deletes it; control-click
toggles it hard/smooth. Points are smooth by default; a hard point renders a **visible
sharp angle with no smoothing on either adjacent segment**, and the discontinuous slope
is audible where the modulation target makes it so (e.g. a pitch EG corner).
- A contour accepts points up to the **128-point ceiling**; attempting to add beyond it is
refused **without disturbing the existing contour**. The two endpoints cannot be deleted.
- A contour of several segments joined at hard points plays back over the full sample
length exactly as drawn — including contours that rise and fall freely (no
globally-monotone restriction), with **no overshoot between any adjacent pair of
points**.
- A freshly created Spline EG shows the smooth y = 1 x default contour.
- While a Spline EG is active, **Gate mode is not selectable**; the spline plays as a pure
time function over the full sample length.
- The overlay contour spans the full displayed sample width, 1:1 with the sample's time
axis; loading a different-length capture rescales the contour to the new length with the
drawn shape preserved proportionally.
- While a Spline EG is active, that envelope's staged segment knobs **and their inner
curve dials** render disabled and reject edits; switching back to Staged re-enables them
with values exactly as left.
- **Staged↔Spline round-trip preserves both states**: switch to Spline, draw, switch back
— staged values exactly as left; switch forward again — the contour exactly as drawn.
Both survive save/reload.
- The velocity→amp transfer-curve editor supports the same control-click hard/smooth
toggle with identical rendering behavior, and its existing right-click node delete
matches the Spline EG's delete gesture unchanged — **one point-editing grammar across
both consumers.**
- Spline evaluation adds no allocation and no virtual dispatch to `process()`.
**Open questions.** None — item 3's two remaining questions (point-count bounds; gesture
convergence) closed in the source doc's third follow-up round.
---
### Θ-W6 — Editor legibility pass
**Depends on W5 for:** the last change to a drawn surface. Item 13 is an audit whose
output is a disposition list over "every class of drawn surface," and item 10's sizing
pass is judged by eye over the finished layout — running either while the spline contour,
the disabled-knob state, or the deck inventory is still moving means auditing and then
re-auditing. The source doc names this sequencing as an observation (item 13 after the
layout/knob work); this plan adopts it as the boundary.
**One track.** Both items repaint essentially every surface in the editor; concurrent
tracks would collide everywhere.
#### Θ-W6-T1 — `legibility-and-antialiasing`
**Goal.** Make the editor legible — bigger knobs and labels, time constants in ms,
double-click reset per ring — then audit every drawn surface for high-DPI clean rendering.
**Consolidates items 10, 13.** Sequence internally: sizing first, audit second.
**Surface boundary — owns:** `core/instrument/ui/knob_deck` (sizing geometry),
`shell/panel/draw_kit` (the shared LICE draw shell — arcs, lines, slopes), and all three
Sample-face bands for repaint-level changes. **Note the blast radius:** `draw_kit` is
shared with the docked bank panel, so an antialiasing change there lands on the extension
UI too. That is desirable, but it puts the panel in this track's verification scope.
**Behavior — sizing and ergonomics (item 10).**
- **Radial knobs and their text labels grow** — both are currently too small. No target
size was given; this is a visual-judgment change accepted by eye.
- **Time-constant labels display in ms, not seconds.** A display-unit change; this makes
no claim about internal representation.
- **Double-click on any radial knob resets it to its default value.**
- **On dual-ring knobs, each ring is its own reset target**: double-click the outer ring
resets the time/level value; double-click the inner curve dial resets the exponent to
**1.0** (the settled linear neutral) — each independently, without touching the other.
**Behavior — antialiasing audit (item 13).**
- **A review pass, not a point fix.** Audit each class of drawn surface and confirm it
renders with antialiasing (or an equivalent) suitable for high-DPI, high-resolution
displays. Where one renders visibly aliased, bring it to the smooth standard.
- Daniel named the visibly pixely surfaces: **radial arcs, waveform lines, envelope
segment slopes.** By the time this runs, those include the inner dials (W3), the stereo
lanes (W2-T2), the staged *and* spline segment slopes (W3, W5), the loop markers
(W4-T1), and the piano key edges (W2-T3).
- The outcome is observable, not procedural.
**Acceptance criteria.**
- Knobs and labels are legibly larger; **Daniel signs off on the result by eye.**
- **Every** time-constant label reads in ms.
- Double-click resets any radial knob to its default; on dual-ring knobs, double-clicking
the inner dial resets **only** the exponent (to 1.0) and double-clicking the outer ring
resets **only** the value.
- Radial arcs (including the inner dials), waveform lines (including the stereo lanes),
and envelope segment slopes (staged **and** spline) render smooth — **no visible jaggies
at 100% scale or on a high-DPI display.**
- **The audit produces a short disposition list**: surfaces checked, which needed work,
which were already clean. A required deliverable, not a side effect — it is what makes
the audit re-runnable.
- Daniel signs off by eye on the named surfaces.
- The docked bank panel is re-verified for regression, since `draw_kit` is shared.
**Open questions.**
- **Target sizes [verify by eye].** No numeric target was given; item 10's own note is
that sizing is judged by eye. Bring a build, not a spec.
- **Whether the piano-key width defect was aliasing [verify].** W2-T3 fixes uniform width
regardless of cause; this audit confirms whether aliasing was the cause and whether the
fix survives DPI scaling.
---
## Phase Ξ — The resample loop
**Ships:** one consolidated, fully robust provenance/usage tracking system, and on top of
it the one-click in-sampler resample — dial → bake → dial again, with the bank as the
medium each iteration passes through.
**Consolidates items 15, 17.**
**Why these two and not more.** Item 17 is a **prerequisite of meaning** for item 15's
bank-side half: the replace-vs-add rule is "does provenance-tied usage exist," which
denotes nothing until the consolidated lineage records exist. Item 17 is otherwise
independent of the editor chain — which is what makes Ξ-W1 concurrency-safe with Θ.
**Ξ-W2 onward requires Phase Θ complete.** Item 15 presupposes the processing surface it
bakes — "filtering, pitching, amp all set up nice" is the instrument items 1, 2, 3, and 14
build — and its settled reset scope enumerates the filter parameters, the spline contours,
and the loop points by name. Baking a processing chain that does not exist yet is not a
schedule preference; the feature is not expressible.
---
### Ξ-W1 — Consolidated tracking, and the programmed-note model
#### Ξ-W1-T1 — `tracking-consolidation`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The provenance/usage
territory is now one system: a new `src/core/tracking/` directory holds `origin_ledger`
(the record family — `OriginRecord`/`OriginKind`, the insertion-ordered `OriginLedger`,
its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion` load
classification) and `tracking_authority` (the one decision surface: `pruneProtection`
and `tiedUsageExists`), retiring `core/model/owned_manifest`. Both prune's protected set
and the resample's replace-vs-add decision are computed from one borrowed
`TrackingState`, so the two safety-critical consumers cannot drift apart.
`sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
decisions, not the codecs. The deferred persisted-instance-identity fix was **not**
folded in — the deferral is restated in `docs/TODO.md`, its one home. This track
landed `OriginRecord`'s birth-time `parentSampleId` chain as the lineage mechanism, but
**Ξ-W2-T1's "naming and lineage" open question (jointly held with this track) is
unaffected and still theirs to close** — display naming and user-readable iteration
lineage were not decided here.
#### Ξ-W1-T2 — `note-program-model`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The programmed-capture-
signal model is a new pure module directory, `src/core/instrument/note/` — a fourth
peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_division`,
`tempo`, and `note_program` (`Velocity`, the denominated `OffsetAmount`, the anchored
`StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below
are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign
uniform, positive is later in time; only an inverted window is refused, reported via
`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed — note length
stays musical-division-only, and an offset stores the denomination it was entered in.
---
### Ξ-W2 — The bake chain
**Depends on Ξ-W1 for:** the consolidated lineage records that make replace-vs-add
computable (T1) and the programmed-note record the offline pass renders (T2). **Also
depends on all of Phase Θ** — see the phase note above.
**One track.** The bake is one gesture and one chain; the architecture decision at its
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.)
- **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.
---
### Ξ-W3 — The capture-signal popup
**Depends on Ξ-W2 for:** the bake chain that the popup programs and its preview must not
diverge from. The acceptance criterion "preview and bake cannot diverge" requires one
render path, which W2 owns — building the preview against a second path is exactly the
defect the criterion exists to catch.
**One track.**
#### Ξ-W3-T1 — `capture-signal-popup`
**Goal.** Let the user program the capture signal — note length, offsets, velocity — and
hear it before committing the bake.
**Consolidates item 15 (the popup sub-feature).**
**Surface boundary — owns:** `core/instrument/ui/curve_popup`'s sibling — a new pure popup
geometry module for the capture-signal sheet (mirror the `curve_popup` precedent: centered
sheet, width/height clamps, title row, Close button rect, outside-sheet dismissal test) —
plus the editor's popup paint/input wiring in the deck or chrome band, and the preview
trigger's call into W2's render path. Reads Ξ-W1-T2's note-program record; does not
re-own it.
**Behavior.**
- **A popup menu programs the capture signal:** **note length**, **start and end offsets —
in ms AND in beats** — and **velocity**.
- **A preview trigger button** auditions the capture note exactly as currently programmed
— the user hears the bake before committing it — and the offline pass renders that same
programmed performance.
- **Note length is a musical-division picker** spanning 1/64th to 64/1 with dotted and
triplet multipliers.
- **Offsets are anchored** — start to note-on, end to note-off — and each is readable and
editable in **both** ms and beats, the two views of one stored value.
- **Beat-denominated values resolve against the project tempo under the cursor.** The
shell reads the tempo; the arithmetic is Ξ-W1-T2's.
- **The programmed velocity is the render velocity** — material because the velocity
transfer curves modulate amp, pitch, and filter at that velocity.
- House-consistent: drawn through the shared kit by palette role, geometry pure and
CTest-covered, no decorative animation.
**Acceptance criteria.**
- The popup exposes: note length as a musical-division picker spanning **1/64th to 64/1
with dotted and triplet multipliers**; start and end offsets, **each readable and
editable in both ms and beats**, anchored to note-on and note-off respectively; and
velocity.
- **Its preview trigger auditions the capture note exactly as programmed, and the bake
renders that same programmed performance — preview and bake cannot diverge.** Verify
structurally (one render path), not just by ear.
- **Beat-denominated values resolve against the project tempo under the cursor:** the same
programmed division yields a correspondingly different rendered duration when the tempo
at the cursor differs.
- The programmed signal persists with the instance and round-trips save/reload.
- The popup's geometry and dismissal test are pure and unit-tested; no hit-test math lands
in shell code.
**Open questions.** Both of Ξ-W1-T2's residuals (negative offsets; the note-length
denomination seam) surface here as UI consequences — if T2 answered them, this track
implements the answer; if T2 deferred either, this track is where it becomes visible and
must be closed.
---
## Traceability — all seventeen items
The check that nothing was dropped. Every row points at a track that exists above.
| # | Item (short) | Phase-Wave-Track | Worktree slug |
|---|---|---|---|
| 1 | Envelope editor: radio switch, curve dials, overlay recolor | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 2 | MM preamp Filter — resonant HP/LP stage | Θ-W1-T3 (DSP) **+** Θ-W2-T1 (integration) | `pth-w1-t3-filter-dsp-port`, `pth-w2-t1-filter-voice-path` |
| 3 | Alternative Spline EGs | Θ-W5-T1 | `pth-w5-t1-spline-egs` |
| 4 | Bug: end-of-sample click, Trigger × Preserve | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 5 | Bug: drag-out sometimes lands without audio | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
| 6 | Bug: FX-container drop loses the capture | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
| 7 | Stereo waveform shows both channels | Θ-W2-T2 | `pth-w2-t2-stereo-waveform-lanes` |
| 8 | Release anchoring; the Pitch AD becomes AHD | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 9 | Loop points — regression + Gate loop-sustain | Θ-W4-T1 | `pth-w4-t1-gate-loop-sustain` |
| 10 | Knob/label sizing, ms units, double-click reset | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
| 11 | Preview glyph; VELOCITY deck; bipolar curves | Θ-W4-T2 | `pth-w4-t2-velocity-deck-and-bipolar-curves` |
| 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` |
| 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
| 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` |
| 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` |
| 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` |
### Work in this plan that is not one of the seventeen
The table above is a completeness proof over `TODO-1.0.md` — every row points at a track,
so nothing from the source was dropped. It is deliberately **not** an index of the plan:
work that did not come from the source doc has no row, and inventing one would weaken the
proof it exists to give.
- **Θ-W3-T1 — `live-parameter-delivery`** (`pth-w3-t1-live-parameter-delivery`). Arose
from Θ-W2-T1's implementation review, not from `TODO-1.0.md`. It is the only such track
in this plan today; if others appear, they belong on this list rather than in the table.
### Deliberate compressions
Recorded so a reader of `TODO-1.0.md` can see what this plan did to the source, rather
than discovering it later:
- **Item 2 is split across two waves.** The DSP port (Θ-W1-T3) is deliberately separated
from the integration (Θ-W2-T1) so the external-input dependency on Daniel's Cortex-M4
code sits on a standalone, disjoint track instead of blocking a wave. Item 2's
acceptance criteria are split accordingly — the range/resonance assertions land in
W1-T3's tests, the audible/pipeline/deck criteria in W2-T1.
- **Item 2's filter envelope ships twice.** W2-T1 ships it in the existing staged AHDSR
shape; W3-T2 gives it the curve treatment and the mode-driven Gate→AHDSR /
Trigger→AHD shape. This is deliberate: waiting would put the filter behind the whole
envelope system.
- **Item 4 lands in Θ-W3, not Θ-W1.** Its fix region is the region item 14 retires, and
the only earlier instrument-side track owns the entire engine. Its acceptance gate is
stated as the post-consolidation gate. It is not gated behind the *whole* editor chain
— three waves of six — and the source doc itself requires re-verification under the
surviving mechanism either way.
- **Item 11's filter velocity curve is split from item 2's filter velocity
modulation.** W2-T1 wires the modulation path (following the amp/pitch precedents);
W4-T2 sets the curve's bipolar domain and default and homes its button. Neither track
can do the other's half.
- **Item 15's undo/recovery is carried as a note, not a criterion** — Daniel set it at
exactly "a plus." The guaranteed recovery floor (the superseded file surviving until
prune) is a criterion.
- **Nothing else was compressed.** Every other item's behavior bullets and acceptance
criteria are carried at full strength into the track that owns it.
---
## Outline at a glance
```
Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor
W1 Collapse and re-seam
T1 zone-retirement ......................... 16
T2 capture-handoff-bugs .................... 5, 6
T3 filter-dsp-port ......................... 2 (DSP) [needs Daniel's M4 code]
W2 Filter in the voice path; waveform and chrome bands
T1 filter-voice-path ....................... 2 (integration)
T2 stereo-waveform-lanes ................... 7
T3 toolbar-and-piano-strip ................. 12
W3 Live parameters, then the staged envelope system [T1 before T2 — serial]
T1 live-parameter-delivery ................. (not one of the seventeen)
T2 staged-envelope-curves .................. 1, 8, 14, 4
W4 Loop sustain and the velocity deck
T1 gate-loop-sustain ....................... 9
T2 velocity-deck-and-bipolar-curves ........ 11
W5 Spline EGs
T1 spline-egs .............................. 3
W6 Editor legibility pass
T1 legibility-and-antialiasing ............. 10, 13
Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward)
W1 Consolidated tracking, and the programmed-note model
T1 tracking-consolidation .................. 17
T2 note-program-model ...................... 15 (model)
W2 The bake chain [requires all of Phase Θ]
T1 resample-bake-chain ..................... 15 (chain)
W3 The capture-signal popup
T1 capture-signal-popup .................... 15 (popup)
```