# 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

**Two questions in this plan await a Daniel decision, and both are in Θ-W3-T1.**
Everything carried forward from `TODO-1.0.md` is still 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 (`live-parameter-delivery`) is the exception, and it is an honest one: it is not
from `TODO-1.0.md` — it arose from Θ-W2-T1's implementation review — and it carries two
genuine **[Daniel]** questions that no amount of code-reading answers. **Which edits stop
triggering an instrument reload** is an edit-model call about the product's shape, and
**what a stage-time change does to a voice already inside that stage** is sound-defining.
Both are stated in that track with candidates laid out and neither pre-picked. The track
should not be dispatched at full scope until they are answered; a reduced-scope fallback
is named there so the track is not hard-blocked if Daniel would rather rule after seeing
the mechanism work.

### 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.
2. **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 in `docs/TODO.md` terms. I am flagging it because the
   consolidation is the natural moment to do it, not because it blocks.
3. **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: **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.

**Two tracks, and they are SERIAL — T1 before T2. This is not a priority hint.** Θ-W4's
two tracks are disjoint and merely ordered; these two are not. T1
(`live-parameter-delivery`) changes how every playback parameter *reaches* a sounding
voice; T2 (`staged-envelope-curves`) changes what *shape* the three envelopes have. Both
land inside `envelopes.h`'s stage math: T1 has to define what happens to a stage's
counters when its duration changes mid-flight, and T2 rewrites the same level
computation to apply a per-segment curve exponent and to grow the Trigger AHD. Two
writers on those forty lines is a merge fight over the phase's hottest code.

The ordering is T1-first for one structural reason: **T2 adds parameters, T1 defines how
parameters are delivered.** Authoring T2's curve exponents, Hold fraction, and Trigger
AHD fields under the latching model means adding them, then re-plumbing every one of them
through the live path afterwards — a backfill cliff, and the exact shape of avoidable
rework. T1 first means every parameter T2 introduces is authored into the delivery
mechanism from the start. The cost of the ordering is honest and worth stating: T2 is the
wave's user-visible payload and T1 delays it by one track.

T1 must specify its mid-stage rule in terms that survive T2 — see that track's open
question 2, whose candidates are deliberately expressed over *normalized stage position*
rather than over output level, because T2's exponent is a pure map of that position and
must compose with the rule rather than invalidate it.

Beyond the ordering, the W3 collision is the phase's densest, which is why neither track
splits further: 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 T2 into model and UI tracks would put
two writers on `play_params.h` + `component_state_io` and two writers on the overlay's
forward/inverse map pair. The serialization is the correct answer.

#### Θ-W3-T1 — `live-parameter-delivery`

**Goal.** Retire note-on latching as the delivery model for continuous playback controls:
a knob moved while a note is sounding changes *that* note, not merely the next one.
Establishes the live-parameter block and its publish path, splits the editor's commit
routing so a parameter edit stops rebuilding the instrument, and makes reachable a
property the filter DSP was deliberately built with and that nothing currently exercises.

**Consolidates:** nothing from `TODO-1.0.md`. This track carries no item number — it
arose from Θ-W2-T1's implementation review. See "Work in this plan that is not one of the
seventeen" below the traceability table.

**Origin — Daniel's ruling, recorded verbatim** (2026-07-30, on the filter's latched
cutoff):

> *"hell no, I was going to bring that up for the other envelopes. We must live compute,
> latching the parameters at note on is not acceptable. long term these will be
> automatable parameters."*

The ruling rejects the **precedent, not one instance of it.** Every playback parameter is
latched into the `Voice` at note-on today — the filter (Θ-W2-T1), the amp AHDSR, and the
pitch envelope alike. It also silently defeats a property that was built on purpose:
`src/core/instrument/engine/filter/CLAUDE.md` records that `prepare()` does **not** clear
state precisely so a live parameter move glides rather than clicks. Nothing reaches it.

**The architectural finding — read this before scoping the track.** A staff-engineer
attempted the live-cutoff fix inside Θ-W2-T1 and correctly stopped. The fix is **not
local to the engine:**

- An editor knob release calls `commitAndReload()` → `reloadInstrument()`, which re-reads
  the bridge, **re-decodes the WAV from disk**, builds a fresh `SampleData` +
  `VoiceEngine`, and atomic-swaps it into `live_`.
- The displaced instrument moves to `draining_`, where already-sounding voices keep
  rendering **their own frozen `SampleData`**. New note-ons route only to `live_`.
- Therefore a `Voice` that read `sample_->play` per frame instead of latching would
  **still** not move a sustaining note: that note lives in a different snapshot, and that
  snapshot is by design never updated again.

Two consequences bind the whole track. The live-parameter block's ownership must sit
**above** `LoadedInstrument` so `live_` and `draining_` observe the same one — a block
owned by a snapshot reproduces the defect exactly. And the set of edits that trigger a
reload has to shrink; that is the open product question below.

**Three commit tiers already exist. The work is reassigning edits across them, not
inventing a taxonomy.**

1. **Full reload** — `commitAndReload()` → `reloadInstrument()`: bridge read, WAV
   re-decode, fresh engine, snapshot swap. Today: every parameter edit.
2. **Engine rebuild** — `rebuildVoiceEngine()`: rebuilds around the already-decoded
   `SampleData`, no disk, same drain-slot swap. Today: voice count, voice mode, mono
   trigger.
3. **Live** — `masterGain_`: a lock-free atomic the audio thread reads per block and
   ramps toward per sample (`gainCurrent_` / `gainRampStep_`), no rebuild, no snapshot.
   Today: master gain alone.

Tier 3 is the shape this track generalizes, and it is a **house precedent already
shipped and already zipper-free** — not a pattern imported from elsewhere. Read it before
designing the block.

**Behavior — what must become live.** Every continuous user-facing playback control:

- **Filter** — cutoff, Q, morph, drive, mod amount, key-track.
- **All three envelopes** — every stage time and every stage level, on the amp AHDSR, the
  pitch envelope, and the filter AHDSR alike.

**Behavior — what stays latched per note, where making it live is a bug.** These are
facts about the note event, not controls, and a design that "generalizes" them into the
live block has misread the ruling:

- **Velocity**, and everything derived from it — in particular the velocity-curve
  evaluation result (`velocityGain_`, `filterVelOffset_`), which is evaluated once in
  `Voice::start` on purpose.
- **The note number**, and the pitch ratio derived from it against the root note
  (`baseRatio_`). Moving a root-note or key-track control must not retune a sounding
  note; the legato `retune()` path is the only sanctioned mid-note pitch move.
- **The sample/capture identity and its decoded PCM.** Loading a different capture is a
  new sound, not a parameter change.

**Behavior — the live block.** Requirements, stated as constraints rather than as a
chosen design:

- **Ownership outlives every `LoadedInstrument`.** One block per processor instance,
  observed identically by `live_` and `draining_`. A drain-slot voice therefore keeps
  tracking the knob — that is the *desired* behavior (it is the note the user is
  hearing), and it should be stated so nobody later "fixes" it.
- **`PlayParams` stays a plain copyable value type.** `component_state_io`, `sample_map`,
  and the editor all pass it by value; atomics cannot simply be pushed into it.
- **No lock on the audio path**, and **no torn read**: the editor writes on the UI
  thread while `process()` reads on the audio thread, so observing half of one edit and
  half of another mid-block is a real hazard the design must close, not a theoretical one.
- **No constructor growth, and no dependency grab-bag.** An `IServiceProvider`-shaped
  parameter object threaded into `VoiceEngine`/`Voice` constructors is a smell and is
  rejected. One candidate that satisfies both: a single `const LiveParams*` field on
  `SampleData`, defaulting to `nullptr` so the bare core behaves exactly as today
  (latched, byte-identical, existing tests untouched) and the shell sets it on the path
  `SampleData` already travels. That is a [propose]-class mechanism note, not a mandate.

**Behavior — the per-frame constraint (Daniel's standing non-negotiable).** Root
`CLAUDE.md`'s performance guardrails and structural heuristic 3 both bind here:

- **No allocation and no virtual dispatch on the per-voice-per-sample path.** A per-frame
  copy of a params struct is a violation; so is an added header→TU indirection.
  `envelopes.h`'s evaluators and `Voice::advanceFrame` are header-inline by RT
  constraint (no LTO configured) — concrete, no common base, no virtual `tick()`.
- **Observation happens at block boundaries, not per frame.** The voice already caches
  its filter parameters in members (`filterCutoffNorm_`, `filterModAmount_`,
  `filterKeyTrack_`, …); the minimal live implementation refreshes those cached members
  at a block boundary instead of only at note-on, leaving the per-sample shape unchanged.
  At 512 frames / 48 kHz that is ~93 Hz of control resolution — ample for a hand on a
  knob, and the coarse floor for automation later.
- **Smoothing is required, and it is what makes the block-rate step inaudible.** A
  block-rate jump in a base value is a step; `masterGain_`'s per-sample linear ramp
  toward the target is the in-house answer. **The ramp must terminate exactly**, not
  asymptotically: `tickFilterCutoff`'s two exact skips compare the value itself, so a
  never-quite-arriving one-pole would pin the filter on the always-re-solve path forever.
  Cost while a cutoff ramp is running is known and affordable — 15.5 ns/frame/voice
  measured in `engine/filter/CLAUDE.md`, ~1.2% of one core at 16 voices — and it returns
  to the skip path when the ramp completes.

**Acceptance criteria.**

- With a note held, sweeping filter cutoff, Q, morph, drive, mod amount, or key-track
  audibly moves *that* note. Same for every stage time and stage level on all three
  envelopes.
- A knob move on a live parameter performs **no** WAV re-decode and **no** snapshot
  rebuild — verified against `reloadInstrument`, not by ear alone.
- A note sounding out of the **drain slot** responds to a live parameter move identically
  to a `live_` voice.
- **No click, no step, no zipper** on any live parameter move, at any block size —
  including a full-range cutoff sweep at maximum Q, and a sustain-level change on a note
  held in Sustain.
- **The per-sample path costs nothing at rest.** With no live value changed, the render
  is unchanged in call/inline shape from today; all live observation is at block
  boundaries. No allocation, no lock, no virtual call is added on the audio thread.
- The audio thread never observes a partially-applied edit within one block.
- `PlayParams` is still a plain copyable value type, still passed by value by
  `component_state_io`, `sample_map`, and the editor.
- **Velocity, the velocity-curve result, the note number, the pitch ratio, and the
  decoded PCM are still latched at note-on.** Dragging a velocity-curve point does not
  retune or re-gain a sounding note.
- **With no live block attached the core is byte-identical to today** — `sampler_core`'s
  existing regression tests pass unchanged, including the bare-engine baselines.
- **Migration:** a project saved before this change reopens sounding identical. Persisted
  values are unchanged; `ComponentState` gains no version bump unless the chosen
  representation forces one, and if it does, the bump is additive and pre-existing blobs
  lift with no audible change.
- The filter DSP's glide property is finally exercised by a test: a cutoff move across a
  `prepare()` without a `reset()` produces no output discontinuity.

**Open questions.**

1. **Which edits stop triggering a reload? [Daniel]** Sample/capture selection genuinely
   needs a reload; filter cutoff plainly does not. The line between them is an edit-model
   decision, not an implementation one. Three candidate groupings, in widening order:
   - **Grouping C — continuous controls only.** Only knob-valued continuous controls go
     live (filter cutoff/Q/morph/drive/mod/key-track, envelope times and levels). Every
     discrete toggle — play mode, pitch engine, filter law, filter enable, channel mode,
     velocity curve — keeps reloading. Narrowest blast radius, smallest verification
     surface, and it fully satisfies the ruling as stated.
   - **Grouping B — capture-anchored edits reload.** C, plus: the three capture-anchored
     overrides (root note, loop span, start frame) also reload, because they name
     positions in the decoded PCM and `loadSelection` already treats them as one family.
     Everything else goes live or drops to the tier-2 rebuild.
   - **Grouping A — only the sound source reloads.** Reload if and only if the identity
     of the decoded audio changes: capture selection and channel mode (a decode policy).
     Everything else is live or a rebuild. Widest live surface, largest verification
     surface, and the one that most nearly matches "these will be automatable
     parameters."
   No pick is made here. **Proposed fallback if Daniel would rather rule after seeing the
   mechanism run:** build the mechanism and ship Grouping C, leaving B and A reachable as
   later reassignments rather than rework — the tier a given edit sits in is a routing
   decision at the editor's commit site, not a property of the block. That is a scope
   proposal, not a decision taken.
2. **What does a stage-time change do to a voice already inside that stage? [Daniel]**
   Sound-defining, and there is no single right answer. Bounded by one non-negotiable:
   **it must not click.** A discontinuity in output level on a parameter move is a
   defect, and a sustain level changed while a voice is held must glide, not step. The
   candidates, with the click property of each named:
   - **(i) Jump / recompute from absolute elapsed.** Keep `framesInStage_`, divide by the
     new duration. This is what today's code would do if params were simply swapped —
     `level_ = framesInStage_ / attackFrames`. Steps the level discontinuously. Fails the
     non-negotiable unaided.
   - **(ii) Clamp.** As (i), but a stage whose new duration is already exceeded completes
     immediately. Still steps.
   - **(iii) Re-derive rate from the current level.** Hold the level, recompute the
     per-frame advance so the stage completes at the new duration measured *from now*.
     Continuous by construction. Costs: the stage's total time becomes elapsed +
     remaining rather than the dialed value, and it needs a rate representation. This is
     what a hardware EG with a rate DAC does.
   - **(iv) Hold normalized stage position.** Keep phase φ = elapsed / duration fixed
     across the change, then advance at 1/newDuration. Continuous in level (φ unchanged →
     level unchanged) and the remaining stage takes its share of the dialed duration.
   - **(v) Track absolute elapsed against the new duration.** Identical to (i); listed so
     it is visibly not a distinct third option.
   - **(vi) Keep (i) and smooth the envelope output.** Accept the recompute and put a
     short declick ramp on the envelope's output level — the codebase already owns a
     bounded-blend declick primitive (`kDeclickDecay`, `seedDeclick`) built for exactly
     this class of step.
   Only **(iii)** and **(iv)** are continuous without added machinery, and both compose
   with T2's curve exponent because both are expressed over normalized position, which
   the exponent is a pure map of. **(vi)** is the option that makes the cheap rule
   acceptable, at the cost of a second smoother. Sustain is a separate sub-case under
   every candidate: it is a *level*, not a timed stage, so a live sustain edit is a direct
   level step and needs the ramp regardless of which rule wins.
3. **Representation: atomics in the value type, or a second live representation?
   [propose]** Two forks, both with real costs:
   - **F1 — atomics in the value type.** Push atomics into the live-relevant fields of
     `PlayParams` / `FilterParams` / `AdsrParams`. One model, no mapping to drift.
     Blast radius is wide and probably disqualifying: atomics are non-copyable, and
     `PlayParams` is passed by value by `component_state_io`, `sample_map`, and the
     editor — the plain-copyable-value-type constraint above is a hard bound on how far
     this fork can go.
   - **F2 — a second live-parameter representation.** A parallel block the audio thread
     reads, published by the UI thread. Keeps the value type plain; the cost is that the
     model exists twice and the mapping between them can drift. Mitigation to require if
     this fork wins: derive the live block from `PlayParams` through exactly **one**
     explicit fold function, so there is a single writer and a single site to keep in
     step. Three publication mechanisms sit under F2, and the choice among them is the
     torn-read answer: a **seqlock** (odd/even generation counter, audio thread copies
     the block once per block and retries on a torn read — the standard single-writer
     lock-free pattern); **per-parameter atomics** (simplest, exactly what `masterGain_`
     does today, but offers no coherence *across* parameters — a set like an envelope's
     A/H/D/S/R can be observed mid-edit); or a **double-buffered block published by
     atomic pointer swap** (coherent, no retry, needs a reclaim rule — and the processor
     already owns that muscle in `live_`/`draining_`/`graveyard_`).
4. **Does the block need sample-accurate (sub-block) resolution now? [propose]** Daniel's
   ruling names automation as the long-term destination, and VST3 delivers parameter
   changes on the audio thread with sample offsets inside `ProcessData` — so per-block is
   the coarse floor, not the ceiling. Building sub-block splitting now is speculative;
   *foreclosing* it is the failure mode. The requirement this track must carry either
   way: **the block's writer interface must not assume a UI thread**, so the audio
   thread's own parameter-change queue can drive it later without a redesign.
5. **Does a live edit still persist immediately? [verify]** Today `commitAndReload` also
   writes the edited set into `params_`, which is what `getState` serializes. A live path
   must keep making that UI-thread write so a saved project carries the edit — but that
   write is no longer the audio thread's source. Confirm by reading that nothing else
   depends on the reload as its persistence trigger.

#### Θ-W3-T2 — `staged-envelope-curves`

**Prerequisite: Θ-W3-T1 must land first** — see the wave preamble. Both tracks write
`envelopes.h`'s stage math, and this track's curve exponent must be authored as a map of
whatever normalized stage position T1's mid-stage rule establishes.

**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_view` already 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_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

**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:
  1. 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;
  2. 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;
  3. 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.md` carries "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. 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.

---

### 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.**
1. **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.
2. **Ξ-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-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)
```
