1387 lines
84 KiB
Markdown
1387 lines
84 KiB
Markdown
# 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
|
||
|
||
**Nothing in this plan awaits a Daniel decision.** Every question carried forward from
|
||
`TODO-1.0.md` is classified **[verify]** (answerable by reading code or running the DAW)
|
||
or **[propose]** (a design call made at implementation review with a proposal, not a
|
||
Daniel call). That classification is preserved per question, attached to the track that
|
||
will answer it.
|
||
|
||
### Flagged for awareness — not blocking, but decision-grade
|
||
|
||
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
|
||
|
||
**Θ-W1-T1 (`zone-retirement`) and Θ-W1-T2 (`capture-handoff-bugs`) have landed** — see
|
||
`docs/COMPLETED.md`. Between them: the zone subsystem is retired and 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. **One track remains:**
|
||
Θ-W1-T3 (`filter-dsp-port`), which lands the filter DSP as a standalone pure module —
|
||
so that from W2 on, tracks fill bands and call modules rather than fighting over
|
||
god-TUs.
|
||
|
||
---
|
||
|
||
#### Θ-W1-T3 — `filter-dsp-port`
|
||
|
||
**Goal.** Land Daniel's Cortex-M4 resonant high/low-pass filter as a pure, tested
|
||
module with the settled parameter ranges — **no call site**, so it is disjoint from
|
||
every other track by construction.
|
||
|
||
**Consolidates item 2 (DSP half).** The integration half is Θ-W2-T1.
|
||
|
||
**Surface boundary — owns:** a new `src/core/instrument/engine/filter.{h,cpp}` (name
|
||
[propose]) plus its `filter_tests` target and the CMake row. No existing file is edited
|
||
except `CMakeLists.txt`.
|
||
|
||
**EXTERNAL PREREQUISITE — this track cannot start without it.** The filter processor is
|
||
based on Daniel's own Cortex-M4 resonant high/lowpass filter code. **That code is an
|
||
input Daniel supplies at implementation time** — it is not in this repo, and neither
|
||
this plan nor `TODO-1.0.md` characterizes it beyond "resonant high and lowpass."
|
||
Dispatching this track before the source is in hand is a stall, not a start.
|
||
|
||
**Behavior.**
|
||
- **Modes:** high-pass and low-pass now. **Modes beyond the pass filters are explicitly
|
||
deferred to later** — do not build a mode-extension framework for them.
|
||
- **Cutoff:** the full audio spectrum, **log scaled**, from fully open to fully closed.
|
||
- **Q:** **0.1 to 10** (the same range as the curve exponent), scaled so **√2 sits at
|
||
the center** of the control.
|
||
- **State is per-voice.** The module carries no shared/instance-wide state; each
|
||
sounding voice runs its own instance with its own coefficients and history.
|
||
- **Concrete type, no interface.** Two modes select by branch or compile-time-known
|
||
dispatch. No `IFilter`, no virtual `tick()` — this lands directly on the per-voice
|
||
per-sample path.
|
||
- Sample-rate-aware by parameter, not by hardcoded constant (the standing
|
||
no-hardcoded-sample-rates ruling).
|
||
|
||
**Acceptance criteria.**
|
||
- The module builds and tests without REAPER, VST3, or a DAW.
|
||
- Cutoff sweeps the full audio spectrum on a log scale, from fully open to fully closed,
|
||
with the mapping asserted in tests at the endpoints and the decade midpoints.
|
||
- Q spans 0.1 → 10 with √2 at the control's center — asserted, not eyeballed.
|
||
- High Q measurably emphasizes the cutoff region (resonance) in **both** HP and LP
|
||
modes; the test asserts the resonant peak, not just monotone rolloff.
|
||
- Coefficient updates are stable across a full-range cutoff sweep at audio rate — no
|
||
blow-up, no NaN, no denormal stall.
|
||
- No virtual dispatch and no allocation in the per-sample entry point.
|
||
|
||
**Open questions.**
|
||
- **Module name and file placement [propose]** — `core/instrument/engine/filter` is the
|
||
obvious home; confirm at review.
|
||
- **Whether mode is a runtime branch or a compile-time-known dispatch [propose]** —
|
||
decided by what the ported DSP actually costs per sample; the constraint is
|
||
"no vtable," not "no branch."
|
||
|
||
---
|
||
|
||
### Θ-W2 — Filter in the voice path; waveform and chrome bands
|
||
|
||
**Depends on W1 for:** the one-parameter-set model and the `sampler_core` seam that T1
|
||
writes the filter into; the `filter` module T1 wires up; the Sample-face band split and
|
||
band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which decides
|
||
what T3's piano strip displays. Authoring any of this against the per-zone model means
|
||
writing storage plumbing W1 deletes.
|
||
|
||
**Three tracks, in priority order.** They are disjoint by band: T1 owns the parameter
|
||
model and the deck band, T2 the waveform band, T3 the chrome band. None re-allocates the
|
||
band stack.
|
||
|
||
#### Θ-W2-T1 — `filter-voice-path`
|
||
|
||
**Goal.** Put the filter into the per-voice signal path as a new fixed processing point,
|
||
give it its deck, and relay the deck row in signal-flow order.
|
||
|
||
**Consolidates item 2 (integration half).** The DSP module is Θ-W1-T3. The filter
|
||
envelope's *curve treatment and mode-driven shape* arrive in Θ-W3 — this track ships the
|
||
filter envelope in the existing staged AHDSR shape.
|
||
|
||
**Surface boundary — owns:** `core/instrument/engine/` voice-render filter call site +
|
||
`zone_params.h` (filter parameters), `core/instrument/map/component_state_io` (version
|
||
bump for the filter parameters), `core/instrument/ui/knob_deck` + the **deck band** of
|
||
the Sample face (geometry, paint, input). Reads the band allocator; does not change it.
|
||
|
||
**Behavior.**
|
||
- **Pipeline position.** A new processing point in the sampler audio pipeline: **after
|
||
the pitch envelope, before the amp stage.** The amp envelope still shapes the filtered
|
||
result.
|
||
- **Per-voice.** Each sounding voice runs its own filter with its own envelope state —
|
||
not a shared instance-wide filter.
|
||
- **Parameters:** mode, cutoff, Q, mod amt — then the AHDSR controls.
|
||
- **Mod amt: bipolar, −100% to +100%, targeting cutoff** — covering the full range
|
||
from either end.
|
||
- Cutoff / Q / mode ranges are the module's, from Θ-W1-T3.
|
||
- **Velocity and key-tracking modulation ship with this item — not deferred.** The
|
||
filter gains velocity modulation following the amp's velocity-transfer-curve pattern
|
||
and key-tracking following the pitch key-tracking pattern. The follow-up established
|
||
the parallel, not new ranges or control layout — follow the cited precedents. (The
|
||
*bipolar domain and default* of the filter velocity curve, and its button's home, are
|
||
item 11 — Θ-W4-T2. This track wires the modulation path; that track shapes the curve.)
|
||
- **Off by default.** Pre-existing saved instances and freshly loaded captures sound
|
||
unchanged until the user engages it.
|
||
- **Parameters live in the instrument's one parameter set** (post-item-16) — no per-zone
|
||
storage, no Sample/Zone parity.
|
||
- **Label.** The user-facing deck-group label is **"Filter"**. "MM preamp" is working
|
||
shorthand for the DSP lineage, not UI text.
|
||
- **Deck reorder.** The knob deck row is relaid out in signal-flow order:
|
||
**pitch → filter → amp**.
|
||
|
||
**Acceptance criteria.**
|
||
- With the filter engaged, played notes are audibly filtered at the specified pipeline
|
||
point: the filter acts on pitched (post-pitch-envelope) signal and the amp envelope
|
||
still shapes the filtered result — audible ordering pitch → filter → amp.
|
||
- Mode, cutoff, Q, and mod-amt controls appear in a deck group labeled **"Filter"**.
|
||
- Cutoff sweeps the full audio spectrum on a log scale, fully open to fully closed; Q
|
||
spans 0.1 → 10 with √2 at the control's center; mod amt at −100% and at +100% each
|
||
drive cutoff across the full range, from opposite ends.
|
||
- High Q audibly emphasizes the cutoff region in both HP and LP modes.
|
||
- **Two simultaneously sounding voices at different envelope phases are filtered
|
||
independently** — per-voice processing is audible, not a shared filter.
|
||
- Filter parameters live in the one parameter set: they edit in one place and govern the
|
||
instrument as a whole.
|
||
- Velocity and key-tracking modulation of the filter are audible — velocity following
|
||
the amp-velocity-transfer-curve pattern, key-tracking following the
|
||
pitch-key-tracking pattern.
|
||
- The deck row reads pitch → filter → amp left-to-right.
|
||
- **The filter is off by default:** a project saved before this change reopens sounding
|
||
identical, and a freshly loaded capture sounds unchanged until the filter is engaged.
|
||
- The filter tick adds no allocation and no virtual dispatch to `process()`.
|
||
|
||
**Open questions.** None carried from item 2 — all three of its prior questions (other
|
||
mod sources, parameter storage side, neutral default) closed in the source doc.
|
||
|
||
---
|
||
|
||
#### Θ-W2-T2 — `stereo-waveform-lanes`
|
||
|
||
**Goal.** Show both channels in the waveform visual when the instrument is in stereo
|
||
mode, without duplicating any overlay.
|
||
|
||
**Consolidates item 7.**
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/waveform_view` and the **waveform
|
||
band** of the Sample face (geometry, paint, input). Reads channel mode; does not own
|
||
the mono/stereo control (that widget relocates in T3 — coordinate on state, not on
|
||
layout).
|
||
|
||
**Behavior.**
|
||
- In **stereo mode**, the waveform visual shows **both L and R channels, left on top**
|
||
— two stacked lanes.
|
||
- In **mono mode**, a single channel shows — no redundant duplicate lane.
|
||
- The display keys off the active channel mode.
|
||
- **Overlays draw once, at full height.** Overlays that ride the waveform — the envelope
|
||
overlay, markers, and item 9's loop region when it lands — draw **once at full height
|
||
across both stacked lanes**, not per lane. Land the full-height overlay contract now
|
||
even though the envelope overlay is reworked in W3 and the loop region arrives in W4;
|
||
both consume this contract.
|
||
- **Stereo processing is linked.** One editor, one set of controls governing both
|
||
channels — no per-channel parameter divergence, no per-channel editing surface.
|
||
|
||
**Acceptance criteria.**
|
||
- A stereo capture in stereo mode shows two stacked lanes, L above R, each a true view
|
||
of its channel's content — an asymmetric-channel capture visibly differs between lanes.
|
||
- Mono mode shows exactly one lane. Switching modes updates the display accordingly.
|
||
- In stereo mode, waveform-riding overlays render **once** at full stacked height — no
|
||
duplicated per-lane copies — and stay legible across both lanes.
|
||
- No per-channel controls appear; every edit applies identically to both channels.
|
||
|
||
**Open questions.** None — item 7's overlay-layout question closed in the source doc
|
||
("one full height; stereo linked processing, one editor").
|
||
|
||
---
|
||
|
||
#### Θ-W2-T3 — `toolbar-and-piano-strip`
|
||
|
||
**Goal.** Clean up the top toolbar, free the note-range piano strip to full width, and
|
||
make the key pattern uniform and self-describing.
|
||
|
||
**Consolidates item 12.**
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/keyboard_strip`, `core/ui/tooltip` (as
|
||
consumer), and the **chrome band** of the Sample face (toolbar layout, title, piano
|
||
strip — geometry, paint, input). Reads the band allocator; does not change it.
|
||
|
||
**Behavior.**
|
||
- **Font cleanup.** One consistent font treatment across the top toolbar text.
|
||
- **Zone count label removed** from the title. (Doubly settled — item 16 removed its
|
||
referent in W1.)
|
||
- **Preview and mono/stereo controls relocate** next to the browse and zoom buttons,
|
||
freeing the **note-range piano strip to take the full width**.
|
||
- **Uniform piano keys.** Some keys currently render skinnier than others — Daniel
|
||
suspects pixel aliasing, but the requirement stands regardless of cause: keys of the
|
||
same class render at uniform width. (If aliasing turns out to be the cause, the
|
||
*general* audit is item 13 — Θ-W6; the uniform-width requirement is this track's
|
||
regardless.)
|
||
- **Note-name tooltips.** Hovering a piano key shows its note value (C4 etc., DAW
|
||
convention), following the instrument's existing tooltip conventions.
|
||
- **What the strip means with no zones.** With zone bars gone, the strip's surviving
|
||
jobs are the **root display/affordance** (item 15's answer round makes root explicitly
|
||
resample-stable, so it must remain visible and settable here) and the note-name
|
||
tooltips — plus a range display only if W1-T1's key-range answer kept one.
|
||
|
||
**Acceptance criteria.**
|
||
- Top toolbar text renders in one consistent font treatment; the title carries no zone
|
||
count.
|
||
- Preview and mono/stereo sit adjacent to browse/zoom; the piano strip spans the full
|
||
editor width.
|
||
- **Same-class keys are equal pixel width at any window width and at any DPI scale.**
|
||
- Hovering any piano key shows its note name in a tooltip.
|
||
- The root note is displayed on and settable from the strip.
|
||
|
||
**Open questions.**
|
||
- **The strip's exact contents [propose]** — jointly owned with item 16's key-range
|
||
answer, which W1-T1 delivers. Propose the final inventory (root affordance +
|
||
tooltips, ± range display) at this track's review.
|
||
- **The preview button's glyph is not this track's** — item 11 (Θ-W4-T2) replaces its
|
||
inner text with a glyph in the position this track gives it. Ship the button with its
|
||
current text; do not pre-empt the glyph.
|
||
|
||
---
|
||
|
||
### Θ-W3 — The staged envelope system
|
||
|
||
**Depends on W2 for:** the filter envelope's existence — items 1, 8, and 14 govern
|
||
*three* envelopes, and the filter is the third; and for the Filter deck, which must
|
||
exist before it can receive a corner radio switch and inner curve dials.
|
||
|
||
**One track.** This is the phase's densest collision: the envelope parameter model, the
|
||
per-sample envelope evaluation, the deck-band inner dials, and the waveform-band overlay
|
||
are one system with one source of truth (the settled S-VIEW rule: node-drag and
|
||
slider-edit read/write the same model, structurally, not via a listener chain). Splitting
|
||
it into model and UI tracks would put two writers on `zone_params.h` +
|
||
`component_state_io` and two writers on the overlay's forward/inverse map pair. The
|
||
serialization is the correct answer.
|
||
|
||
#### Θ-W3-T1 — `staged-envelope-curves`
|
||
|
||
**Goal.** Grow the envelope-overlay editor from an amp-only fixture into the shared
|
||
graphical surface for every envelope, give every envelope shapeable segments, fix the
|
||
overlay's layout and contrast failures, consolidate the Trigger amp fade pair into the
|
||
AHD — and, in the same region, kill the Trigger×Preserve end-of-sample click.
|
||
|
||
**Consolidates items 1, 8, 14, 4.**
|
||
|
||
**Surface boundary — owns:**
|
||
- `core/instrument/engine/` — the envelope evaluator (from W1's seam), `zone_params.h`,
|
||
`pitch_shift.{h,cpp}` (item 4's tail), the voice-render envelope and tail path.
|
||
- `core/instrument/map/` — `trigger_seam`, `component_state_io` (curve exponents, the
|
||
pitch Hold stage, the Trigger AHD, migration).
|
||
- `core/instrument/ui/` — `envelope_overlay`, `envelope_edit`, `knob_deck` (the inner
|
||
dial).
|
||
- `src/shell/instrument/` — the deck band (inner dials, radio switches) and the
|
||
waveform band's **overlay layer only** (T2's lane geometry from W2 is consumed, not
|
||
re-owned).
|
||
|
||
**Behavior — the overlay surface (item 1).**
|
||
- **Radio switch per envelope deck.** Each envelope knob-deck group (AMP ENVELOPE,
|
||
PITCH ENV, and now Filter) gains a radio switch in the corner of its deck. Selecting a
|
||
deck's radio makes *that* envelope the one displayed and editable in the graphic
|
||
waveform overlay — replacing today's always-on Amp AHDSR. The switch is exclusive:
|
||
**one overlay-active envelope at a time, or none** — no-envelope-shown is a valid
|
||
state, not an error.
|
||
- **Default overlay: none.** The editor opens with **no envelope selected**.
|
||
- **Segment curve values on all envelopes.** Amp AHDSR, Pitch AHD, and Filter AHDSR all
|
||
gain an editable curve value per *sloped* segment. The curve is exponential; the
|
||
per-segment parameter is the exponent scalar, range **0.1 to 10**. **1.0 is the linear
|
||
neutral** (y = x^1.0 is linear).
|
||
- **Which segments are sloped: every stage except Hold and Sustain.** For an AHDSR that
|
||
is Attack, Decay, Release; for the Pitch AHD, Attack and Decay. Hold is flat
|
||
everywhere and carries no curve dial.
|
||
- **Inner dial on curvable-segment knobs.** Every knob controlling a sloped, curvable
|
||
segment gains an **inner dial**: its own inner arc, its own hover accent (tertiary
|
||
purple), its own needle, its own numerical label. The inner dial controls that
|
||
segment's curve exponent; the outer knob keeps controlling the segment's time/level
|
||
value as today.
|
||
- **Curve editing in the overlay.** Dragging on a segment **adds a round mid-segment
|
||
knot** whose drag adjusts that segment's curve — the overlay is a curve-edit surface
|
||
in its own right, alongside (not instead of) the inner dial. Same one-model rule as
|
||
the existing draggable nodes: knot-drag and inner dial cannot diverge.
|
||
- **Overlay recolor.** Envelope segments change from secondary blue to **tertiary
|
||
purple** — secondary blue contrasts poorly against the primary green waveform behind
|
||
it. Re-verify the tertiary-purple-on-green pair against `theme`'s contrast-floor
|
||
assertions.
|
||
- **Pre-existing instances load at exponent 1.0** — the linear neutral — so their
|
||
audible envelope behavior is unchanged.
|
||
|
||
**Behavior — layout policy and the pitch AHD (item 8).**
|
||
- **The release segment anchors to the right edge** of the overlay. Release is dragged
|
||
from its **top node** (the node joining sustain to release) instead of the bottom
|
||
corner; the bottom corner — the envelope's end point — becomes **fixed/anchored**, not
|
||
draggable. Today's failure is that with little or no release the sustain portion
|
||
occupies only a small stretch and the whole figure reads off-center.
|
||
- **Spec change: the pitch envelope becomes AHD** — Attack → Hold → Decay. **Hold ranges
|
||
0 to 100% of the time remaining after Attack and Decay** (100% of
|
||
`sample length − (attack + decay)`). At 100% Hold fills all remaining time; at 0% it
|
||
takes none.
|
||
- **The A + H + D ≤ sample-length bound holds by construction, not by a clamp.** Hold is
|
||
expressed as a fraction of what is left, so the sum cannot overflow. Do not add a
|
||
separate clamp; none is needed.
|
||
- **Scope rule — split on the sustain stage.** The 1:1-overlay property and the
|
||
combined-time bound apply to **all envelopes that do NOT have a sustain stage**;
|
||
envelopes **with** a sustain stage get the right-anchored-release policy instead. The
|
||
two policies **coexist rather than merge**. Daniel's rationale: the 1:1 mapping only
|
||
makes sense for trigger, not gated envelopes. **The pitch envelope's D is not a
|
||
release, so it is not right-anchored.**
|
||
|
||
**Behavior — mode-driven envelope shape (item 14).**
|
||
- **Trigger mode: fades out, AHD in.** In Trigger mode the amp envelope becomes an AHD
|
||
per item 8's definition. The fade-in-length and fade-out-length controls **go away in
|
||
Trigger mode**; Attack and Decay carry those roles. Daniel's motive is stated:
|
||
reducing code for the same job — one staged-envelope design covering what is currently
|
||
two mechanisms.
|
||
- **The Trigger AHD's sloped segments get the full curve treatment** — exponent 0.1–10,
|
||
inner dials, mid-segment overlay knots, tertiary-purple rendering.
|
||
- **Gate mode unchanged** — the amp envelope stays the AHDSR with the right-anchored
|
||
release.
|
||
- **The filter envelope follows the same rule.** Stated once, governing all three:
|
||
**pitch is always AHD (1:1 overlay); amp and filter are AHDSR in Gate
|
||
(right-anchored release) and AHD in Trigger (1:1 overlay).** The rule selects by the
|
||
envelope's **current shape under the active playback mode**, not by which processor it
|
||
modulates. No new rule is needed — item 8's rule already decides every case.
|
||
- **Pre-existing instances reopen sounding identical.** A Trigger instance's prior
|
||
fade-in/fade-out contour must be reproduced by the loaded AHD. The evident mapping
|
||
(Attack ← fade-in, Decay ← fade-out, Hold ← the full remainder, exponents at whatever
|
||
reproduces the prior fade shape) is a verify-at-implementation detail. **A prior zero
|
||
fade-out is Decay = 0** — the abrupt end stays representable, so nothing the old
|
||
controls could express is lost.
|
||
|
||
**Behavior — the Trigger×Preserve click (item 4).**
|
||
- In Trigger mode — polyphonic or monophonic alike — with the pitch engine in
|
||
**Preserve mode only**, the end of the sample produces an audible click. The scoping is
|
||
the useful part of the report and is recorded as given: Trigger × Preserve ×
|
||
end-of-sample; Varispeed is not implicated.
|
||
- Expected: a Trigger one-shot in Preserve ends silently — no click or discontinuity at
|
||
the sample end, in either voice mode.
|
||
- **Why it is in this track and not earlier:** the defect sits in the region the
|
||
fade-out currently governs, and this track retires that mechanism. Fixing it in a
|
||
prior wave means fixing it, then re-verifying it under the surviving mechanism
|
||
anyway — and the only other instrument-side track before this one owns the whole
|
||
engine. The gate below is the *post-consolidation* gate.
|
||
|
||
**Acceptance criteria.**
|
||
- Each envelope deck shows a corner radio switch; activating one puts that envelope in
|
||
the overlay, editable there, and the overlay tracks the switch immediately. At most one
|
||
envelope is overlay-active; with none active the overlay draws no envelope.
|
||
- The editor opens with no envelope overlay-active.
|
||
- Every curvable-segment knob shows the inner dial (inner arc, tertiary-purple hover
|
||
accent, needle, numeric label); sweeping it 0.1 → 10 visibly reshapes the overlay
|
||
segment and **audibly** reshapes the envelope on played notes. Hold and Sustain knobs
|
||
carry no inner dial.
|
||
- Dragging on an overlay segment adds a round mid-segment knot; dragging the knot adjusts
|
||
that segment's curve, and the segment's inner dial reflects the same value.
|
||
- Overlay envelope segments render in tertiary purple and are clearly legible against the
|
||
primary green waveform.
|
||
- With release at zero or minimum, the sustain segment extends to (near) the right edge —
|
||
the overlay reads full-width, not bunched left. Dragging the sustain→release top node
|
||
adjusts release; the bottom-right corner is fixed and not draggable.
|
||
- Envelopes with a sustain stage follow that anchoring policy; the pitch envelope's Decay
|
||
is **not** right-anchored.
|
||
- The pitch envelope plays and displays three stages — Attack, Hold, Decay — with Hold
|
||
spanning 0–100% of the time remaining after Attack and Decay. **No combination of A, H,
|
||
D exceeds the sample length, and no clamp fires, because none exists.**
|
||
- The overlay of any sustain-less envelope is 1:1 with the waveform's time axis: a stage
|
||
boundary at N seconds sits over the waveform at N seconds.
|
||
- In Trigger mode the amp deck shows Attack / Hold / Decay — **no fade-in or fade-out
|
||
control anywhere in Trigger mode** — with curve inner dials on Attack and Decay and
|
||
none on Hold.
|
||
- Switching between Gate and Trigger switches the amp *and* filter envelope surfaces
|
||
(deck and overlay) between AHDSR and AHD accordingly.
|
||
- The Trigger amp AHD behaves stage-for-stage like the pitch AHD — Hold semantics, curve
|
||
treatment, and overlay mapping match. (The observable proxy for the consolidation
|
||
motive: one staged-envelope design, several consumers.)
|
||
- **Migration:** a project saved before this change reopens sounding identical — every
|
||
curve at exponent 1.0, and a Trigger instance's prior fade contour (including a zero
|
||
fade-out's abrupt end) audibly reproduced by the loaded AHD.
|
||
- **Item 4's gate, under the surviving mechanism:** a Trigger-mode note played in
|
||
Preserve, in both Poly and Mono, ends with no audible click at the sample end —
|
||
verified by ear *and* by inspecting the rendered output for a terminal discontinuity.
|
||
Varispeed playback is unchanged.
|
||
|
||
**Open questions.**
|
||
- **Stage-value state across the Gate/Trigger switch — shared or per-mode? [propose]**
|
||
The Gate AHDSR and Trigger AHD share stage names (A, H, D); whether they share *values*
|
||
(one envelope whose S and R fall away in Trigger) or keep per-mode state (item 3's
|
||
"save but inactive" dual-state precedent) is unspecified — and the question covers the
|
||
**filter envelope too**. Migration leans per-mode for the amp: an old instance carries
|
||
both its AHDSR values and its Trigger fade values, and a shared-value model cannot
|
||
preserve both modes' prior sound at once. The filter is new and carries no migration
|
||
weight either way; matching the amp's answer is the natural default. Resolve with a
|
||
proposal at review — the sound-identical gate must hold for whichever mode a saved
|
||
instance plays in.
|
||
|
||
---
|
||
|
||
### Θ-W4 — Loop sustain and the velocity deck
|
||
|
||
**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain
|
||
is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves
|
||
modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for
|
||
the filter's existence and on W2-T3 for the preview button's toolbar position.
|
||
|
||
**Two tracks, in priority order.** Disjoint: T1 owns the engine loop path and the
|
||
waveform band's marker layer; T2 owns the deck band, the curve popup, and the preview
|
||
button in the chrome band. Neither touches the other's band.
|
||
|
||
#### Θ-W4-T1 — `gate-loop-sustain`
|
||
|
||
**Goal.** Establish (or restore) loop points, and make a Gate-mode loop function as the
|
||
sustain — indefinite playback until note-off, with a crossfaded seam.
|
||
|
||
**Consolidates item 9 (both halves: the suspected regression and the feature spec).**
|
||
|
||
**Surface boundary — owns:** the voice-render loop read path (post-W1 seam),
|
||
`core/instrument/map/component_state_io` (loop parameters), `core/instrument/ui/
|
||
waveform_view` markers, and the **waveform band's marker layer**. Reads W2-T2's lane
|
||
geometry and W3's overlay contract; re-owns neither.
|
||
|
||
**Behavior.**
|
||
- **Regression half.** Daniel's observation: loop points appear to have been lost.
|
||
Whether they were genuinely removed from playback or are merely unexposed in the
|
||
current UI **must be verified as the first act of implementation** — it was not
|
||
guessable in the source doc, which was written under a no-code-reads constraint.
|
||
- **Feature half.** In **Gate mode**: a loopable section with **parameterized start and
|
||
end points** and a **parameterized crossfade on loop reset**. The loop functions as the
|
||
sustain — indefinite playback cycling the loop until note-off, then release.
|
||
- **Loop-sustain is a Staged/Gate-mode feature.** Gate mode is unavailable while a
|
||
Spline EG is active (item 3, Θ-W5), so Spline mode remains full-sample-length playback
|
||
with no loop. This track must not assume Gate is always available.
|
||
- **Storage:** loop parameters belong to the instrument's one parameter set. There is no
|
||
per-zone side to confirm.
|
||
- **Preserve-mode contract (existing, do not break):** *loop the source, shift the
|
||
output* — loop points stay source-frame facts.
|
||
- **Reset-scope note for Ξ:** item 15 classifies loop points as **baked-in** — they
|
||
shaped the render and old positions are meaningless against new audio — so they reset
|
||
on a resample. Nothing to build here; recorded so Ξ-W2-T1 does not re-derive it.
|
||
|
||
**Acceptance criteria.**
|
||
- In Gate mode with a loop defined, a held note sustains indefinitely, audibly cycling
|
||
the loop section; note-off exits into the release stage.
|
||
- With a nonzero crossfade the loop seam is smooth — **no click at the loop reset** — and
|
||
crossfade length audibly follows its parameter.
|
||
- Loop start, end, and crossfade are user-parameterized, editable, and persisted across
|
||
save/reload.
|
||
- Whatever the regression finding, the end state is loop points exposed and functional
|
||
per this spec.
|
||
- Looping adds no allocation to `process()`; the crossfade is computed on the per-sample
|
||
path without a branch-heavy or virtual detour.
|
||
|
||
**Open questions.**
|
||
- **Regression verification — removed vs. unexposed [verify].** The first act of
|
||
implementation. Report the finding; the end state is the same either way.
|
||
- **Crossfade units and range [propose].** Unspecified. Surface a proposed default at
|
||
implementation review (ms is the likely denomination given item 10 puts time constants
|
||
in ms).
|
||
- **Editing surface for loop start/end [propose].** Waveform markers, knobs, or both, is
|
||
unspecified. The waveform display is the natural home for range markers — and
|
||
`waveform_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-T1 | `pth-w3-t1-staged-envelope-curves` |
|
||
| 2 | MM preamp Filter — resonant HP/LP stage | Θ-W1-T3 (DSP) **+** Θ-W2-T1 (integration) | `pth-w1-t3-filter-dsp-port`, `pth-w2-t1-filter-voice-path` |
|
||
| 3 | Alternative Spline EGs | Θ-W5-T1 | `pth-w5-t1-spline-egs` |
|
||
| 4 | Bug: end-of-sample click, Trigger × Preserve | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` |
|
||
| 5 | Bug: drag-out sometimes lands without audio | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
|
||
| 6 | Bug: FX-container drop loses the capture | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
|
||
| 7 | Stereo waveform shows both channels | Θ-W2-T2 | `pth-w2-t2-stereo-waveform-lanes` |
|
||
| 8 | Release anchoring; the Pitch AD becomes AHD | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` |
|
||
| 9 | Loop points — regression + Gate loop-sustain | Θ-W4-T1 | `pth-w4-t1-gate-loop-sustain` |
|
||
| 10 | Knob/label sizing, ms units, double-click reset | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
|
||
| 11 | Preview glyph; VELOCITY deck; bipolar curves | Θ-W4-T2 | `pth-w4-t2-velocity-deck-and-bipolar-curves` |
|
||
| 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` |
|
||
| 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
|
||
| 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T1 | `pth-w3-t1-staged-envelope-curves` |
|
||
| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` |
|
||
| 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` |
|
||
| 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` |
|
||
|
||
### Deliberate compressions
|
||
|
||
Recorded so a reader of `TODO-1.0.md` can see what this plan did to the source, rather
|
||
than discovering it later:
|
||
|
||
- **Item 2 is split across two waves.** The DSP port (Θ-W1-T3) is deliberately separated
|
||
from the integration (Θ-W2-T1) so the external-input dependency on Daniel's Cortex-M4
|
||
code sits on a standalone, disjoint track instead of blocking a wave. Item 2's
|
||
acceptance criteria are split accordingly — the range/resonance assertions land in
|
||
W1-T3's tests, the audible/pipeline/deck criteria in W2-T1.
|
||
- **Item 2's filter envelope ships twice.** W2-T1 ships it in the existing staged AHDSR
|
||
shape; W3-T1 gives it the curve treatment and the mode-driven Gate→AHDSR /
|
||
Trigger→AHD shape. This is deliberate: waiting would put the filter behind the whole
|
||
envelope system.
|
||
- **Item 4 lands in Θ-W3, not Θ-W1.** Its fix region is the region item 14 retires, and
|
||
the only earlier instrument-side track owns the entire engine. Its acceptance gate is
|
||
stated as the post-consolidation gate. It is not gated behind the *whole* editor chain
|
||
— three waves of six — and the source doc itself requires re-verification under the
|
||
surviving mechanism either way.
|
||
- **Item 11's filter velocity curve is split from item 2's filter velocity
|
||
modulation.** W2-T1 wires the modulation path (following the amp/pitch precedents);
|
||
W4-T2 sets the curve's bipolar domain and default and homes its button. Neither track
|
||
can do the other's half.
|
||
- **Item 15's undo/recovery is carried as a note, not a criterion** — Daniel set it at
|
||
exactly "a plus." The guaranteed recovery floor (the superseded file surviving until
|
||
prune) is a criterion.
|
||
- **Nothing else was compressed.** Every other item's behavior bullets and acceptance
|
||
criteria are carried at full strength into the track that owns it.
|
||
|
||
---
|
||
|
||
## Outline at a glance
|
||
|
||
```
|
||
Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor
|
||
W1 Collapse and re-seam
|
||
T1 zone-retirement ......................... 16
|
||
T2 capture-handoff-bugs .................... 5, 6
|
||
T3 filter-dsp-port ......................... 2 (DSP) [needs Daniel's M4 code]
|
||
W2 Filter in the voice path; waveform and chrome bands
|
||
T1 filter-voice-path ....................... 2 (integration)
|
||
T2 stereo-waveform-lanes ................... 7
|
||
T3 toolbar-and-piano-strip ................. 12
|
||
W3 The staged envelope system
|
||
T1 staged-envelope-curves .................. 1, 8, 14, 4
|
||
W4 Loop sustain and the velocity deck
|
||
T1 gate-loop-sustain ....................... 9
|
||
T2 velocity-deck-and-bipolar-curves ........ 11
|
||
W5 Spline EGs
|
||
T1 spline-egs .............................. 3
|
||
W6 Editor legibility pass
|
||
T1 legibility-and-antialiasing ............. 10, 13
|
||
|
||
Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward)
|
||
W1 Consolidated tracking, and the programmed-note model
|
||
T1 tracking-consolidation .................. 17
|
||
T2 note-program-model ...................... 15 (model)
|
||
W2 The bake chain [requires all of Phase Θ]
|
||
T1 resample-bake-chain ..................... 15 (chain)
|
||
W3 The capture-signal popup
|
||
T1 capture-signal-popup .................... 15 (popup)
|
||
```
|