Two Greek-lettered phases (Θ instrument overhaul, Ξ resample loop): sequenced waves, parallel tracks on disjoint surfaces, per-track acceptance criteria, carried-forward open questions, traceability table. TODO-1.0.md retained as verbatim-provenance appendix.
92 KiB
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 toCOMPLETED.mdwith 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
Nothing in this plan awaits a Daniel decision. Every question 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.
Flagged for awareness — not blocking, but decision-grade
- 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. - Item 17's "100% robust" and
docs/TODO.md's deferred persisted-instance-identity fix sit one day apart and point opposite ways. On 2026-07-28 Daniel accepted shipping the safe-but-incomplete usage tracking and deferring the persisted-nonce fix; on 2026-07-29 he required the tracking be "consolidated and made 100% robust." My reading: those do not conflict — "robust" is a safety strength claim (no destructive act follows from ambiguity), while the deferred wart is a completeness one (prune stops reclaiming after a reopen; the bank folder grows, nothing is lost). Ξ-W1-T1 carries this as a [propose] question: fold the deferred fix in, or restate the deferral explicitly indocs/TODO.mdterms. I am flagging it because the consolidation is the natural moment to do it, not because it blocks. - Phase Ξ Wave 1 is concurrency-safe with Phase Θ (from Θ-W2 onward). See "Running Θ and Ξ concurrently" below. Whether to spend a specialist that way is a scheduling call, not a plan decision.
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: noIEnvelope, noIFilter, no virtual per-voicetick(). 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-threadreloadInstrument+ 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>_teststarget 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:
- 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 competingComponentStateversion bumps. - The voice render path (
sampler_core.cpp) — filter insertion, envelope evaluation, loop read, and the Trigger tail all live there. - The Sample face (
editor_paint_sample.cpp516 lines /editor_input_sample.cpp583 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
Three tracks, in priority order. This wave removes a whole subsystem, re-seams the two files everything downstream must edit, and lands the filter DSP as a standalone pure module — so that from W2 on, tracks fill bands and call modules rather than fighting over god-TUs.
Θ-W1-T1 — zone-retirement
Goal. Retire the zone mapping system: ReaSampler 9000 becomes one capture = one parameter set — and, in the space that deletion clears, land the two responsibility seams the rest of the phase depends on.
Consolidates item 16. (Also discharges the superseded per-zone storage language in items 2, 9, 11, 12 before those items are built.)
Surface boundary — owns:
src/core/instrument/map/—sample_map(zone payload → one parameter set),component_state_io(zones-payload version bump + migration).src/core/instrument/engine/—zone_params.h,sampler_core.{h,cpp}(the responsibility seam).src/core/instrument/ui/—editor_geometry(Zone-face layout removal + the band seam),keyboard_strip(zone-highlight geometry removal).src/shell/instrument/— all eight editor face-axis TUs,editor_internal.h,processor_state.cpp,reasampler_processor.{h,cpp}.CMakeLists.txttarget list (shared, low-conflict).
Behavior.
- One capture = one parameter set. The instrument holds one loaded capture and one set of playback parameters governing it. No zones, no per-zone divergence, no keymap of captures. Every playback parameter edits in exactly one place; no gesture can express per-zone divergence.
- The zone-mapping surface goes away — the dedicated zone-editing face and its authoring affordances (add/delete zone, the per-zone parameter panel, the Low/High/ Root zone legend). The root note survives as a first-class parameter of the one set — only its zone-legend housing goes.
- The per-zone/per-instance storage distinction collapses. There is nothing for VOICE and MASTER to be exceptions to and no second surface to hold in parity.
- Migration: adopt the first zone's capture and parameters. A saved multi-zone instance lifts by adopting its first zone's capture and that zone's parameters; the remaining zones drop from the instance. Dropping a zone touches no file and no bank entry. The sounds-identical bar is deliberately relaxed for a genuinely multi-zone instance — Daniel has no projects with zones in use, so it has no real referents. Single-zone instances lift losslessly.
- The single-capture experience is unchanged — a loaded capture still plays across the keyboard repitched from its root, key-tracking still applies.
- No effect on the bank or the extension. Captures, banks, capture/placement separation, and the prune safety model are untouched.
Structural deliverables (this wave's real output for everything downstream).
- The
sampler_coreresponsibility seam. With zone resolution gone, what remains divides cleanly: note routing / allocation / stealing / mono stack / panic (per-note, cold) versus the per-voice per-sample render (hot). Split along that seam. Envelope evaluation comes out as a concrete, header-inline evaluator — never a virtualtick()on the per-voice path (structural heuristic 3 names this blowout by name). The exact seam is [propose]; the constraint is not. - The Sample-face band seam. Split
editor_paint_sample/editor_input_sample/ the Sample-face half ofeditor_geometryinto three bands — chrome, waveform, decks — with one pure band-stack allocator owning the vertical inventory. Land the band inventory the phase needs (chrome band, waveform band sized for two stacked lanes, deck row) so W2's three tracks fill bands rather than re-allocate them. The band allocator is a shared read-only surface from W2 on; no later track re-owns it without saying so. - Both TUs are at the ~600 ceiling before this work; neither may exit it over.
Acceptance criteria.
- The editor exposes no zone-mapping surface anywhere: no zone view or button, no add/delete-zone affordance, no per-zone parameter panel, no Low/High zone legend. The root-note control survives.
- Every playback parameter edits in exactly one place and governs the instrument as a whole; no gesture can express per-zone divergence.
- A single-capture instance saved before this change reopens sounding identical — same keyboard response, same parameters, same root.
- A multi-zone instance saved before this change reopens holding its first zone's capture with that zone's parameters — no error, no file touched, no bank entry disturbed.
- The one parameter set round-trips save/reload intact.
- Bank, capture, placement, and prune behavior are unchanged.
sampler_coreand the Sample-face TUs are split by responsibility; every resulting TU is under ~600 lines or carries an explicit hot-path justification. No virtual dispatch is introduced on the per-voice per-sample path — verify by inspection of the split.
Open questions.
- Does any key-range concept survive? [propose] With zones gone, does the capture respond across the entire keyboard (repitched from root), or does a user-settable low/high playable range survive as a plain parameter of the one set? Lean, consistent with the simplification motive: no range concept — full-keyboard response — with a low/high pair re-addable later as ordinary parameters if missed. Θ-W2-T3 consumes this answer for the piano strip's contents; answer it here.
- Which zone is "first"? [verify] The instance's existing zone ordering. No real-world stakes given the migration rationale.
- The two responsibility seams [propose] — see Structural deliverables.
Θ-W1-T2 — capture-handoff-bugs
Goal. Fix the two extension-side defects that break getting a capture out of the bank and into a target — one intermittent, one deterministic.
Consolidates items 5, 6.
Surface boundary — owns:
src/shell/actions/drag_out_win.{h,cpp},src/shell/actions/instrument_drop_win.{h,cpp}src/core/ui/drag_out,src/core/wire/instrument_dropsrc/shell/panel/panel_drag.cppsrc/ingest.{h,cpp}(physically atsrc/root — see rider below)
Fully disjoint from the instrument; runs concurrently with T1 and T3 without contact.
Behavior.
- Item 5 — drag-out sometimes lands without audio. Extension side, not the VST. Dragging a capture out of the pool or a named bank sometimes produces a drop that does not contain the audio; retrying works. Both details are load-bearing: the failure is intermittent and a retry succeeds. Expected: every completed drag-out delivers the capture's audio at the drop target, first try, every time.
- Item 6 — drop onto FX container loads the instrument without the capture. Dropping a capture onto an FX container loads a ReaSampler 9000 instance but without the capture; dropping the same capture onto the FX button works as expected. The FX-button path is the reference; the container path must match it.
Acceptance criteria.
- Item 5 (soak gate — the failure is intermittent, so a single pass is not a gate): across a sustained session of varied drag-outs — pool and bank sources, including the first drag after other bank activity — every drop yields a playable file containing the audio, with no retry ever needed.
- Item 6: a capture dropped onto an FX container yields an instrument instance with that capture loaded and immediately playable, indistinguishable (apart from where the FX sits) from the FX-button drop. The FX-button path remains unregressed.
instrument_drop's all-or-nothing contract holds on the container path too: any failure rolls back viaTrackFX_Delete, leaving no orphaned instance.
Open questions. None carried from the source items (both are bug reports recorded without root-cause analysis, per the no-code-reads constraint they were written under).
Opportunistic rider (not required; propose at review). ingest.cpp / ingest.h,
ext_keys.h, and resource.h physically live at src/ root rather than under
shell/actions/ — a documented structural wart Phase Q's reorg did not close. This
track is the only one in the phase that opens ingest; re-homing it here is cheap and
otherwise needs its own dispatch. Take it or explicitly decline it.
Θ-W1-T3 — filter-dsp-port
Goal. Land Daniel's Cortex-M4 resonant high/low-pass filter as a pure, tested module with the settled parameter ranges — no call site, so it is disjoint from every other track by construction.
Consolidates item 2 (DSP half). The integration half is Θ-W2-T1.
Surface boundary — owns: a new src/core/instrument/engine/filter.{h,cpp} (name
[propose]) plus its filter_tests target and the CMake row. No existing file is edited
except CMakeLists.txt.
EXTERNAL PREREQUISITE — this track cannot start without it. The filter processor is
based on Daniel's own Cortex-M4 resonant high/lowpass filter code. That code is an
input Daniel supplies at implementation time — it is not in this repo, and neither
this plan nor TODO-1.0.md characterizes it beyond "resonant high and lowpass."
Dispatching this track before the source is in hand is a stall, not a start.
Behavior.
- Modes: high-pass and low-pass now. Modes beyond the pass filters are explicitly deferred to later — do not build a mode-extension framework for them.
- Cutoff: the full audio spectrum, log scaled, from fully open to fully closed.
- Q: 0.1 to 10 (the same range as the curve exponent), scaled so √2 sits at the center of the control.
- State is per-voice. The module carries no shared/instance-wide state; each sounding voice runs its own instance with its own coefficients and history.
- Concrete type, no interface. Two modes select by branch or compile-time-known
dispatch. No
IFilter, no virtualtick()— this lands directly on the per-voice per-sample path. - Sample-rate-aware by parameter, not by hardcoded constant (the standing no-hardcoded-sample-rates ruling).
Acceptance criteria.
- The module builds and tests without REAPER, VST3, or a DAW.
- Cutoff sweeps the full audio spectrum on a log scale, from fully open to fully closed, with the mapping asserted in tests at the endpoints and the decade midpoints.
- Q spans 0.1 → 10 with √2 at the control's center — asserted, not eyeballed.
- High Q measurably emphasizes the cutoff region (resonance) in both HP and LP modes; the test asserts the resonant peak, not just monotone rolloff.
- Coefficient updates are stable across a full-range cutoff sweep at audio rate — no blow-up, no NaN, no denormal stall.
- No virtual dispatch and no allocation in the per-sample entry point.
Open questions.
- Module name and file placement [propose] —
core/instrument/engine/filteris the obvious home; confirm at review. - Whether mode is a runtime branch or a compile-time-known dispatch [propose] — decided by what the ported DSP actually costs per sample; the constraint is "no vtable," not "no branch."
Θ-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.
Three tracks, in priority order. They are disjoint by band: T1 owns the parameter model and the deck band, T2 the waveform band, T3 the chrome band. None re-allocates the band stack.
Θ-W2-T1 — filter-voice-path
Goal. Put the filter into the per-voice signal path as a new fixed processing point, give it its deck, and relay the deck row in signal-flow order.
Consolidates item 2 (integration half). The DSP module is Θ-W1-T3. The filter envelope's curve treatment and mode-driven shape arrive in Θ-W3 — this track ships the filter envelope in the existing staged AHDSR shape.
Surface boundary — owns: core/instrument/engine/ voice-render filter call site +
zone_params.h (filter parameters), core/instrument/map/component_state_io (version
bump for the filter parameters), core/instrument/ui/knob_deck + the deck band of
the Sample face (geometry, paint, input). Reads the band allocator; does not change it.
Behavior.
- Pipeline position. A new processing point in the sampler audio pipeline: after the pitch envelope, before the amp stage. The amp envelope still shapes the filtered result.
- Per-voice. Each sounding voice runs its own filter with its own envelope state — not a shared instance-wide filter.
- Parameters: mode, cutoff, Q, mod amt — then the AHDSR controls.
- Mod amt: bipolar, −100% to +100%, targeting cutoff — covering the full range from either end.
- Cutoff / Q / mode ranges are the module's, from Θ-W1-T3.
- Velocity and key-tracking modulation ship with this item — not deferred. The filter gains velocity modulation following the amp's velocity-transfer-curve pattern and key-tracking following the pitch key-tracking pattern. The follow-up established the parallel, not new ranges or control layout — follow the cited precedents. (The bipolar domain and default of the filter velocity curve, and its button's home, are item 11 — Θ-W4-T2. This track wires the modulation path; that track shapes the curve.)
- Off by default. Pre-existing saved instances and freshly loaded captures sound unchanged until the user engages it.
- Parameters live in the instrument's one parameter set (post-item-16) — no per-zone storage, no Sample/Zone parity.
- Label. The user-facing deck-group label is "Filter". "MM preamp" is working shorthand for the DSP lineage, not UI text.
- Deck reorder. The knob deck row is relaid out in signal-flow order: pitch → filter → amp.
Acceptance criteria.
- With the filter engaged, played notes are audibly filtered at the specified pipeline point: the filter acts on pitched (post-pitch-envelope) signal and the amp envelope still shapes the filtered result — audible ordering pitch → filter → amp.
- Mode, cutoff, Q, and mod-amt controls appear in a deck group labeled "Filter".
- Cutoff sweeps the full audio spectrum on a log scale, fully open to fully closed; Q spans 0.1 → 10 with √2 at the control's center; mod amt at −100% and at +100% each drive cutoff across the full range, from opposite ends.
- High Q audibly emphasizes the cutoff region in both HP and LP modes.
- Two simultaneously sounding voices at different envelope phases are filtered independently — per-voice processing is audible, not a shared filter.
- Filter parameters live in the one parameter set: they edit in one place and govern the instrument as a whole.
- Velocity and key-tracking modulation of the filter are audible — velocity following the amp-velocity-transfer-curve pattern, key-tracking following the pitch-key-tracking pattern.
- The deck row reads pitch → filter → amp left-to-right.
- The filter is off by default: a project saved before this change reopens sounding identical, and a freshly loaded capture sounds unchanged until the filter is engaged.
- The filter tick adds no allocation and no virtual dispatch to
process().
Open questions. None carried from item 2 — all three of its prior questions (other mod sources, parameter storage side, neutral default) closed in the source doc.
Θ-W2-T2 — stereo-waveform-lanes
Goal. Show both channels in the waveform visual when the instrument is in stereo mode, without duplicating any overlay.
Consolidates item 7.
Surface boundary — owns: core/instrument/ui/waveform_view and the waveform
band of the Sample face (geometry, paint, input). Reads channel mode; does not own
the mono/stereo control (that widget relocates in T3 — coordinate on state, not on
layout).
Behavior.
- In stereo mode, the waveform visual shows both L and R channels, left on top — two stacked lanes.
- In mono mode, a single channel shows — no redundant duplicate lane.
- The display keys off the active channel mode.
- Overlays draw once, at full height. Overlays that ride the waveform — the envelope overlay, markers, and item 9's loop region when it lands — draw once at full height across both stacked lanes, not per lane. Land the full-height overlay contract now even though the envelope overlay is reworked in W3 and the loop region arrives in W4; both consume this contract.
- Stereo processing is linked. One editor, one set of controls governing both channels — no per-channel parameter divergence, no per-channel editing surface.
Acceptance criteria.
- A stereo capture in stereo mode shows two stacked lanes, L above R, each a true view of its channel's content — an asymmetric-channel capture visibly differs between lanes.
- Mono mode shows exactly one lane. Switching modes updates the display accordingly.
- In stereo mode, waveform-riding overlays render once at full stacked height — no duplicated per-lane copies — and stay legible across both lanes.
- No per-channel controls appear; every edit applies identically to both channels.
Open questions. None — item 7's overlay-layout question closed in the source doc ("one full height; stereo linked processing, one editor").
Θ-W2-T3 — toolbar-and-piano-strip
Goal. Clean up the top toolbar, free the note-range piano strip to full width, and make the key pattern uniform and self-describing.
Consolidates item 12.
Surface boundary — owns: core/instrument/ui/keyboard_strip, core/ui/tooltip (as
consumer), and the chrome band of the Sample face (toolbar layout, title, piano
strip — geometry, paint, input). Reads the band allocator; does not change it.
Behavior.
- Font cleanup. One consistent font treatment across the top toolbar text.
- Zone count label removed from the title. (Doubly settled — item 16 removed its referent in W1.)
- Preview and mono/stereo controls relocate next to the browse and zoom buttons, freeing the note-range piano strip to take the full width.
- Uniform piano keys. Some keys currently render skinnier than others — Daniel suspects pixel aliasing, but the requirement stands regardless of cause: keys of the same class render at uniform width. (If aliasing turns out to be the cause, the general audit is item 13 — Θ-W6; the uniform-width requirement is this track's regardless.)
- Note-name tooltips. Hovering a piano key shows its note value (C4 etc., DAW convention), following the instrument's existing tooltip conventions.
- What the strip means with no zones. With zone bars gone, the strip's surviving jobs are the root display/affordance (item 15's answer round makes root explicitly resample-stable, so it must remain visible and settable here) and the note-name tooltips — plus a range display only if W1-T1's key-range answer kept one.
Acceptance criteria.
- Top toolbar text renders in one consistent font treatment; the title carries no zone count.
- Preview and mono/stereo sit adjacent to browse/zoom; the piano strip spans the full editor width.
- Same-class keys are equal pixel width at any window width and at any DPI scale.
- Hovering any piano key shows its note name in a tooltip.
- The root note is displayed on and settable from the strip.
Open questions.
- The strip's exact contents [propose] — jointly owned with item 16's key-range answer, which W1-T1 delivers. Propose the final inventory (root affordance + tooltips, ± range display) at this track's review.
- The preview button's glyph is not this track's — item 11 (Θ-W4-T2) replaces its inner text with a glyph in the position this track gives it. Ship the button with its current text; do not pre-empt the glyph.
Θ-W3 — 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.
One track. This is the phase's densest collision: the envelope parameter model, the
per-sample envelope evaluation, the deck-band inner dials, and the waveform-band overlay
are one system with one source of truth (the settled S-VIEW rule: node-drag and
slider-edit read/write the same model, structurally, not via a listener chain). Splitting
it into model and UI tracks would put two writers on zone_params.h +
component_state_io and two writers on the overlay's forward/inverse map pair. The
serialization is the correct answer.
Θ-W3-T1 — staged-envelope-curves
Goal. Grow the envelope-overlay editor from an amp-only fixture into the shared graphical surface for every envelope, give every envelope shapeable segments, fix the overlay's layout and contrast failures, consolidate the Trigger amp fade pair into the AHD — and, in the same region, kill the Trigger×Preserve end-of-sample click.
Consolidates items 1, 8, 14, 4.
Surface boundary — owns:
core/instrument/engine/— the envelope evaluator (from W1's seam),zone_params.h,pitch_shift.{h,cpp}(item 4's tail), the voice-render envelope and tail path.core/instrument/map/—trigger_seam,component_state_io(curve exponents, the pitch Hold stage, the Trigger AHD, migration).core/instrument/ui/—envelope_overlay,envelope_edit,knob_deck(the inner dial).src/shell/instrument/— the deck band (inner dials, radio switches) and the waveform band's overlay layer only (T2's lane geometry from W2 is consumed, not re-owned).
Behavior — the overlay surface (item 1).
- Radio switch per envelope deck. Each envelope knob-deck group (AMP ENVELOPE, PITCH ENV, and now Filter) gains a radio switch in the corner of its deck. Selecting a deck's radio makes that envelope the one displayed and editable in the graphic waveform overlay — replacing today's always-on Amp AHDSR. The switch is exclusive: one overlay-active envelope at a time, or none — no-envelope-shown is a valid state, not an error.
- Default overlay: none. The editor opens with no envelope selected.
- Segment curve values on all envelopes. Amp AHDSR, Pitch AHD, and Filter AHDSR all gain an editable curve value per sloped segment. The curve is exponential; the per-segment parameter is the exponent scalar, range 0.1 to 10. 1.0 is the linear neutral (y = x^1.0 is linear).
- Which segments are sloped: every stage except Hold and Sustain. For an AHDSR that is Attack, Decay, Release; for the Pitch AHD, Attack and Decay. Hold is flat everywhere and carries no curve dial.
- Inner dial on curvable-segment knobs. Every knob controlling a sloped, curvable segment gains an inner dial: its own inner arc, its own hover accent (tertiary purple), its own needle, its own numerical label. The inner dial controls that segment's curve exponent; the outer knob keeps controlling the segment's time/level value as today.
- Curve editing in the overlay. Dragging on a segment adds a round mid-segment knot whose drag adjusts that segment's curve — the overlay is a curve-edit surface in its own right, alongside (not instead of) the inner dial. Same one-model rule as the existing draggable nodes: knot-drag and inner dial cannot diverge.
- Overlay recolor. Envelope segments change from secondary blue to tertiary
purple — secondary blue contrasts poorly against the primary green waveform behind
it. Re-verify the tertiary-purple-on-green pair against
theme's contrast-floor assertions. - Pre-existing instances load at exponent 1.0 — the linear neutral — so their audible envelope behavior is unchanged.
Behavior — layout policy and the pitch AHD (item 8).
- The release segment anchors to the right edge of the overlay. Release is dragged from its top node (the node joining sustain to release) instead of the bottom corner; the bottom corner — the envelope's end point — becomes fixed/anchored, not draggable. Today's failure is that with little or no release the sustain portion occupies only a small stretch and the whole figure reads off-center.
- Spec change: the pitch envelope becomes AHD — Attack → Hold → Decay. Hold ranges
0 to 100% of the time remaining after Attack and Decay (100% of
sample length − (attack + decay)). At 100% Hold fills all remaining time; at 0% it takes none. - The A + H + D ≤ sample-length bound holds by construction, not by a clamp. Hold is expressed as a fraction of what is left, so the sum cannot overflow. Do not add a separate clamp; none is needed.
- Scope rule — split on the sustain stage. The 1:1-overlay property and the combined-time bound apply to all envelopes that do NOT have a sustain stage; envelopes with a sustain stage get the right-anchored-release policy instead. The two policies coexist rather than merge. Daniel's rationale: the 1:1 mapping only makes sense for trigger, not gated envelopes. The pitch envelope's D is not a release, so it is not right-anchored.
Behavior — mode-driven envelope shape (item 14).
- Trigger mode: fades out, AHD in. In Trigger mode the amp envelope becomes an AHD per item 8's definition. The fade-in-length and fade-out-length controls go away in Trigger mode; Attack and Decay carry those roles. Daniel's motive is stated: reducing code for the same job — one staged-envelope design covering what is currently two mechanisms.
- The Trigger AHD's sloped segments get the full curve treatment — exponent 0.1–10, inner dials, mid-segment overlay knots, tertiary-purple rendering.
- Gate mode unchanged — the amp envelope stays the AHDSR with the right-anchored release.
- The filter envelope follows the same rule. Stated once, governing all three: pitch is always AHD (1:1 overlay); amp and filter are AHDSR in Gate (right-anchored release) and AHD in Trigger (1:1 overlay). The rule selects by the envelope's current shape under the active playback mode, not by which processor it modulates. No new rule is needed — item 8's rule already decides every case.
- Pre-existing instances reopen sounding identical. A Trigger instance's prior fade-in/fade-out contour must be reproduced by the loaded AHD. The evident mapping (Attack ← fade-in, Decay ← fade-out, Hold ← the full remainder, exponents at whatever reproduces the prior fade shape) is a verify-at-implementation detail. A prior zero fade-out is Decay = 0 — the abrupt end stays representable, so nothing the old controls could express is lost.
Behavior — the Trigger×Preserve click (item 4).
- In Trigger mode — polyphonic or monophonic alike — with the pitch engine in Preserve mode only, the end of the sample produces an audible click. The scoping is the useful part of the report and is recorded as given: Trigger × Preserve × end-of-sample; Varispeed is not implicated.
- Expected: a Trigger one-shot in Preserve ends silently — no click or discontinuity at the sample end, in either voice mode.
- Why it is in this track and not earlier: the defect sits in the region the fade-out currently governs, and this track retires that mechanism. Fixing it in a prior wave means fixing it, then re-verifying it under the surviving mechanism anyway — and the only other instrument-side track before this one owns the whole engine. The gate below is the post-consolidation gate.
Acceptance criteria.
- Each envelope deck shows a corner radio switch; activating one puts that envelope in the overlay, editable there, and the overlay tracks the switch immediately. At most one envelope is overlay-active; with none active the overlay draws no envelope.
- The editor opens with no envelope overlay-active.
- Every curvable-segment knob shows the inner dial (inner arc, tertiary-purple hover accent, needle, numeric label); sweeping it 0.1 → 10 visibly reshapes the overlay segment and audibly reshapes the envelope on played notes. Hold and Sustain knobs carry no inner dial.
- Dragging on an overlay segment adds a round mid-segment knot; dragging the knot adjusts that segment's curve, and the segment's inner dial reflects the same value.
- Overlay envelope segments render in tertiary purple and are clearly legible against the primary green waveform.
- With release at zero or minimum, the sustain segment extends to (near) the right edge — the overlay reads full-width, not bunched left. Dragging the sustain→release top node adjusts release; the bottom-right corner is fixed and not draggable.
- Envelopes with a sustain stage follow that anchoring policy; the pitch envelope's Decay is not right-anchored.
- The pitch envelope plays and displays three stages — Attack, Hold, Decay — with Hold spanning 0–100% of the time remaining after Attack and Decay. No combination of A, H, D exceeds the sample length, and no clamp fires, because none exists.
- The overlay of any sustain-less envelope is 1:1 with the waveform's time axis: a stage boundary at N seconds sits over the waveform at N seconds.
- In Trigger mode the amp deck shows Attack / Hold / Decay — no fade-in or fade-out control anywhere in Trigger mode — with curve inner dials on Attack and Decay and none on Hold.
- Switching between Gate and Trigger switches the amp and filter envelope surfaces (deck and overlay) between AHDSR and AHD accordingly.
- The Trigger amp AHD behaves stage-for-stage like the pitch AHD — Hold semantics, curve treatment, and overlay mapping match. (The observable proxy for the consolidation motive: one staged-envelope design, several consumers.)
- Migration: a project saved before this change reopens sounding identical — every curve at exponent 1.0, and a Trigger instance's prior fade contour (including a zero fade-out's abrupt end) audibly reproduced by the loaded AHD.
- Item 4's gate, under the surviving mechanism: a Trigger-mode note played in Preserve, in both Poly and Mono, ends with no audible click at the sample end — verified by ear and by inspecting the rendered output for a terminal discontinuity. Varispeed playback is unchanged.
Open questions.
- Stage-value state across the Gate/Trigger switch — shared or per-mode? [propose] The Gate AHDSR and Trigger AHD share stage names (A, H, D); whether they share values (one envelope whose S and R fall away in Trigger) or keep per-mode state (item 3's "save but inactive" dual-state precedent) is unspecified — and the question covers the filter envelope too. Migration leans per-mode for the amp: an old instance carries both its AHDSR values and its Trigger fade values, and a shared-value model cannot preserve both modes' prior sound at once. The filter is new and carries no migration weight either way; matching the amp's answer is the natural default. Resolve with a proposal at review — the sound-identical gate must hold for whichever mode a saved instance plays in.
Θ-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.
Two tracks, in priority order. Disjoint: T1 owns the engine loop path and the waveform band's marker layer; T2 owns the deck band, the curve popup, and the preview button in the chrome band. Neither touches the other's band.
Θ-W4-T1 — gate-loop-sustain
Goal. Establish (or restore) loop points, and make a Gate-mode loop function as the sustain — indefinite playback until note-off, with a crossfaded seam.
Consolidates item 9 (both halves: the suspected regression and the feature spec).
Surface boundary — owns: the voice-render loop read path (post-W1 seam),
core/instrument/map/component_state_io (loop parameters), core/instrument/ui/ waveform_view markers, and the waveform band's marker layer. Reads W2-T2's lane
geometry and W3's overlay contract; re-owns neither.
Behavior.
- Regression half. Daniel's observation: loop points appear to have been lost. Whether they were genuinely removed from playback or are merely unexposed in the current UI must be verified as the first act of implementation — it was not guessable in the source doc, which was written under a no-code-reads constraint.
- Feature half. In Gate mode: a loopable section with parameterized start and end points and a parameterized crossfade on loop reset. The loop functions as the sustain — indefinite playback cycling the loop until note-off, then release.
- Loop-sustain is a Staged/Gate-mode feature. Gate mode is unavailable while a Spline EG is active (item 3, Θ-W5), so Spline mode remains full-sample-length playback with no loop. This track must not assume Gate is always available.
- Storage: loop parameters belong to the instrument's one parameter set. There is no per-zone side to confirm.
- Preserve-mode contract (existing, do not break): loop the source, shift the output — loop points stay source-frame facts.
- Reset-scope note for Ξ: item 15 classifies loop points as baked-in — they shaped the render and old positions are meaningless against new audio — so they reset on a resample. Nothing to build here; recorded so Ξ-W2-T1 does not re-derive it.
Acceptance criteria.
- In Gate mode with a loop defined, a held note sustains indefinitely, audibly cycling the loop section; note-off exits into the release stage.
- With a nonzero crossfade the loop seam is smooth — no click at the loop reset — and crossfade length audibly follows its parameter.
- Loop start, end, and crossfade are user-parameterized, editable, and persisted across save/reload.
- Whatever the regression finding, the end state is loop points exposed and functional per this spec.
- Looping adds no allocation to
process(); the crossfade is computed on the per-sample path without a branch-heavy or virtual detour.
Open questions.
- Regression verification — removed vs. unexposed [verify]. The first act of implementation. Report the finding; the end state is the same either way.
- Crossfade units and range [propose]. Unspecified. Surface a proposed default at implementation review (ms is the likely denomination given item 10 puts time constants in ms).
- Editing surface for loop start/end [propose]. Waveform markers, knobs, or both, is
unspecified. The waveform display is the natural home for range markers — and
waveform_viewalready owns generic named draggable markers with clamp and zero-crossing snap, which is a strong lean — but Daniel has not said. Propose at review.
Θ-W4-T2 — velocity-deck-and-bipolar-curves
Goal. Give the three velocity-curve popups one home, make the pitch and filter curves bipolar and off-by-default, and replace the preview button's text with a glyph.
Consolidates item 11.
Surface boundary — owns: core/instrument/engine/velocity_curve (bipolar y-domain),
core/instrument/ui/curve_popup, the deck band (the new VELOCITY group), and the
preview button in the chrome band at the toolbar position W2-T3 gave it. Does not touch
the waveform band.
Behavior.
- The VELOCITY deck. All three velocity-curve popup buttons (amp, pitch, filter) live together in a new control deck group labelled "VELOCITY", placed to the left of the VOICE group. This supersedes the original ask's per-section placement — Daniel confirmed the MASTER placement was a real contention point: MASTER is reserved for other, post-voice-mixer concerns he will add to later, so the velocity curves do not belong there. No velocity-curve button appears in MASTER, PITCH, or Filter.
- Bipolar pitch/filter transfer functions. Pitch and filter velocity transfer functions are bipolar: y range [−1, 1], default y = 0 — flat at zero, meaning velocity modulation of pitch and filter is off until the user draws a curve.
- Amp stays unipolar at [0, 1], and its existing flat-unity default is unchanged.
- Storage: the curves live in the instrument's one parameter set.
- Preview glyph. The preview button's inner text is replaced with a glyph. Proposed at product level: a right-pointing play triangle — the universal audition read. Daniel's constraint: no new dependencies — a statically embedded bitmap or equivalent that plays nicely with the existing LICE drawing path.
Acceptance criteria.
- The three velocity-curve buttons sit together in a deck group labelled VELOCITY, immediately to the left of the VOICE group; no velocity-curve button appears in MASTER, PITCH, or Filter.
- Opening the pitch or filter curve shows a bipolar editor ([−1, 1]) defaulted flat at y = 0; played velocities produce no pitch/filter modulation until a curve is drawn, then audibly follow it.
- The amp curve's domain ([0, 1]) and flat-unity default are unchanged.
- The velocity curves persist in the one parameter set and round-trip save/reload.
- The preview button shows the glyph (no text) and stays legible in all interaction states; no new build or runtime dependency is introduced.
Open questions.
- Does a user-facing pitch velocity transfer curve already exist, or does this item introduce it? [verify] Unverifiable in the source doc under its no-code-reads constraint. If absent, this track introduces it. Check before scoping.
- Bipolar rendering in the shared spline editor [propose]. Item 3 (Θ-W5) enhances the one spline implementation for hard points; this track must render and edit a bipolar y-domain alongside the amp curve's unipolar one. Land the bipolar domain here in the existing editor; Θ-W5 then adds hard points to the same editor without re-deciding the domain.
Θ-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_kitis 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
Depends on: nothing in this phase. Concurrency-safe with Phase Θ from Θ-W2 onward — see the note at the end of this phase.
Two tracks, in priority order. Disjoint: T1 is entirely extension-side record-keeping; T2 is a new pure module with no existing call site.
Ξ-W1-T1 — tracking-consolidation
Goal. Make the provenance/usage territory one system, 100% robust — recipe provenance, live-instance usage, and recapture lineage as facets of the same record-keeping, answering both safety-critical consumers from one place.
Consolidates item 17.
Surface boundary — owns: core/model/provenance, core/model/owned_manifest,
core/wire/sample_usage, core/reclaim/prune_reconcile, shell/persist/
(usage_scan, prune_fs, ext_state_io's manifest/provenance keys),
shell/actions/prune_action. Instrument-side touch is limited to the usage-publish block
at the tail of reloadInstrument in shell/instrument/processor_reload.cpp — this is
the one file shared with Phase Θ; if run concurrently, this track owns that block and
Θ-W1-T1 does not touch it.
Behavior.
- One system, not three mechanisms. Today the territory holds two separately-grown
mechanisms plus one new demand:
- the capture-recipe fingerprint — a thin reproducibility record of how a capture was made, deliberately not a restorable chain, conservatively recording nothing when the situation is ambiguous;
- the instance-usage tracking — each live instance declares the captures it holds, so prune can never delete a capture a live instance is using, with a fail-safe stance that unreadable usage state halts prune entirely;
- item 15's demand for recapture lineage — records tying usages of a capture to it "by the provenance/recaptureing system," deciding replace-vs-add at bake time. These stop being separate ad-hoc mechanisms. Every consumer — the prune's protection decision, the resample's replace-vs-add decision, and any future lineage reader — is answered from the one system.
- What "100% robust" observably means. Daniel stated the strength, not the mechanics.
Three observable implications follow from the stakes, and no further specifics are
invented:
- No silent gaps. Every capture the system itself creates is tracked from the moment of its creation; a recapture carries its lineage from birth, never backfilled. There is no window in which a system-created file exists untracked.
- Fail-safe on unreadable or ambiguous state. Tracking state that cannot be read never yields the destructive answer: the prune deletes nothing (today's settled stance — preserved and generalized, not relaxed), and the resample never takes the replace branch on unreadable lineage.
- Consumers cannot disagree. The prune's protection answer and the resample's tied-usage answer are different questions with different universes — item 15 settles that the replace-vs-add universe is narrower than the prune-protection universe — but both are computed from the same records, so they cannot drift apart.
- Existing guarantees are the floor. Consolidation must not weaken anything settled: prune still deletes only the system's own orphans and never a referenced or live-held capture; the fail-safe abort on unreadable usage state survives; the recipe fingerprint's record-nothing-when-ambiguous conservatism survives for the recipe half. The lineage half is the one place that conservatism is foreclosed — item 15's replace-vs-add must be computable, so a recapture's lineage record is mandatory.
- Why this is safety-critical, stated once: this territory gates the system's only file-deletion authority (prune) and its only capture-replacement act (resample) — the two places where a tracking error loses a user's audio or sound.
Acceptance criteria.
- One consolidated tracking system answers both safety-critical consumers: the prune's protected set and the resample's replace-vs-add decision are each computed from it, per their own settled rules; no separate ad-hoc tracking mechanism remains in the territory.
- No silent gaps: a recapture created by item 15's bake is tracked from the instant it exists — a bake followed immediately by a prune, or by a second bake, behaves correctly with no window in which the recapture is untracked or its lineage absent. (Testable ahead of item 15 by simulating a system-created file through the same path.)
- Fail-safe throughout: with tracking state made unreadable, the prune deletes nothing (and reports what blocked it, per today's behavior) and the resample never takes the replace branch; no destructive act follows from ambiguity, anywhere in the territory.
- Every protection settled today holds undiminished after consolidation: a capture held by a live instance cannot be pruned; files the system did not create are untouchable; unreadable usage state still halts the prune.
- A pre-existing bank lifts into the consolidated system with no loss of protection and no spurious lineage, and never-recorded remains distinguishable from unreadable.
- Item 15's other-references case is decidable: for any capture, "does provenance-tied usage exist" has a definite yes/no answer.
- The pure fold decisions stay pure and unit-tested without a DAW; the REAPER/filesystem
half stays in
shell/persist.
Open questions. None awaits a Daniel decision — the requirement and its strength are his; the shape is review work.
- The consolidated shape [propose]. What "one system" concretely is — one record family, one authority, how the three facets relate — is design work proposed at review.
- The lineage record [propose, jointly with item 15's naming-and-lineage question]. What constitutes a "usage tie," when it is written, whether it is ever severed, and whether iteration lineage is user-readable from the bank.
- The recipe-fingerprint half of a recapture [propose]. A resample's recipe is the instrument's own settings, not a track chain; whether the fingerprint records a resample-shaped recipe — and what its conservatism means there — is proposed at review. (The lineage half has no record-nothing option; the recipe half may keep one.)
- Never-recorded vs. unreadable [propose]. Pre-existing captures predate lineage records, and the two absences demand opposite treatment: never-recorded means no tied usage exists (replace is legitimate); unreadable means fail-safe. How the consolidated system distinguishes them — and how pre-existing banks lift in without weakening any protection they enjoy today — is proposed at review.
- NEW — does this absorb the deferred persisted-instance-identity fix? [propose].
docs/TODO.mdcarries "Persist ReaSampler 9000 instance identity to let prune reclaim de-referenced captures after reopen," deferred 2026-07-28 as low-risk (safe, but the bank folder grows unbounded after a reopen). "Consolidated and made 100% robust" arrived the next day. My reading is that they do not conflict — robustness is a safety claim, the wart is a completeness one — but the consolidation is the natural moment to revisit it, and the constraint it must handle is unchanged (a persisted identity is inherited by a Ctrl+D in-place duplicate; a divergent clone must still be detected and protected fail-safe without reintroducing the sibling-drop bug). Fold it in or explicitly restate the deferral at review; do not leave it ambiguous.
Ξ-W1-T2 — note-program-model
Goal. Land the programmed-capture-signal model as a pure, tested module: musical divisions, tempo resolution, and offset anchoring — the arithmetic both the bake and the popup will read.
Consolidates item 15 (the capture-signal model; the bake is Ξ-W2-T1 and the popup UI is Ξ-W3-T1).
Surface boundary — owns: a new pure module (core/instrument/map/note_program or
similar, name [propose]) plus its tests target and the CMake row. New files only; no
existing file edited except CMakeLists.txt. Disjoint from T1 by construction.
Behavior.
- Note length is a musical division, not a free duration. Chosen from divisions
spanning 1/64th through 64/1, with dotted and triplet multipliers (Daniel's
examples:
1/8.,1/4t,1/16,4/1). - Beats resolve against the project tempo under the cursor. A beat-denominated value — the note-length division always, the offsets when expressed in beats — resolves to time against "the project tempo under the item cursor" (Daniel's phrase; read plainly: the tempo in effect at the project's cursor position when the preview or bake runs). The module takes the tempo as a parameter; reading it from REAPER is the shell's job, not this module's.
- Offsets anchor to note-on and note-off. The start offset is relative to the programmed note's note-on; the end offset is relative to its note-off.
- Offsets are expressed in ms AND in beats — the two denominations are two views of one stored value, and the conversion is this module's. Note length is musical-division-only.
- Velocity is explicit — a plain 1–127 value carried in the same record. Material because the velocity transfer curves modulate amp (and, post-Θ, pitch and filter) at that velocity.
- The record is one struct, round-trippable, that the bake renders from and the popup edits. One source of truth: preview and bake cannot diverge because they read the same record through the same resolver.
Acceptance criteria.
- The division set spans 1/64 through 64/1 inclusive, with dotted and triplet variants, and each resolves to the correct duration at a given tempo — asserted at the extremes and at Daniel's four named examples.
- The same division yields a correspondingly different duration when the supplied tempo differs (tests assert the proportionality, not a hardcoded rate).
- Offsets round-trip losslessly between ms and beats at a given tempo.
- Anchoring is explicit in the type: a start offset is note-on-relative and an end offset is note-off-relative, and the resolved window is computed from both plus the note length.
- No hardcoded sample rate and no hardcoded tempo anywhere in the module (the standing rate-free-seconds ruling).
- Builds and tests without REAPER, VST3, or a DAW.
Open questions.
- Are negative offsets meaningful? [propose] Unchanged from the source doc. A negative start offset (capture beginning before note-on) is plausible; a negative end offset (truncating before note-off) is also plausible. Propose the answer with the type.
- The denomination seam [propose]. The first answer round expressed the offsets "in ms AND in beats" while note length is musical-division-only. The plain reading is that the ms/beats duality applies to the offsets only. If an ms display or entry for note length seems wanted at implementation, propose it at review rather than assuming either way.
Ξ-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. IfNamedCommandLookupandMain_OnCommandresolve 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] againstvendor/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_iobut deliberately notsampler_core/pitch_shift—component_state_iowas 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
OfflineRenderBackendand 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.
Running Θ and Ξ concurrently
Ξ-W1 is concurrency-safe with Phase Θ from Θ-W2 onward. Ξ-W1-T1 is extension-side
record-keeping (core/model, core/wire, core/reclaim, shell/persist,
shell/actions); Ξ-W1-T2 is a new pure module. Neither is on Θ's critical path.
Two conditions.
- Not during Θ-W1. Θ-W1-T1 re-seams the whole instrument, including
processor_reload.cpp, which is the one file Ξ-W1-T1 also touches (the usage-publish block). Wait for Θ-W1 to land. - Ξ-W1-T1 owns the usage-publish block for the duration; no Θ track edits it.
Recommendation: run Ξ-W1 concurrently if a specialist is spare — it ships prune robustness early and removes the largest dependency from Ξ-W2's critical path. Otherwise sequence it after Θ. This is a scheduling call, not a plan decision.
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-T1 | pth-w3-t1-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-T1 | pth-w3-t1-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-T1 | pth-w3-t1-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-T1 | pth-w3-t1-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 |
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-T1 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 The staged envelope system
T1 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)