256216d670
Dynamic latency ships as ruled; the restart cost is re-attributed to our own setActive and filed in TODO. The reflow splits canvas (W1-T4) from arrangement (W3-T1); preserve-time-stretch moves to W1-T5.
1707 lines
114 KiB
Markdown
1707 lines
114 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 — **plus Phase Γ**, which did not come from those seventeen
|
||
(it came from a direct interview, 2026-08-01) and is scoped in
|
||
`docs/product/instrument-control-surface.md`.
|
||
|
||
## 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`**, **Γ → `g`**. So Θ-W1-T1 dispatches into
|
||
`pth-w1-t1-zone-retirement` and Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`.
|
||
|
||
## Decision state
|
||
|
||
Everything carried forward from `TODO-1.0.md` is classified **[verify]** (answerable by
|
||
reading code or running the DAW) or **[propose]** (a design call made at implementation
|
||
review with a proposal, not a Daniel call); that classification is preserved per
|
||
question, attached to the track that will answer it.
|
||
|
||
Θ-W3-T1's two genuine **[Daniel]** questions — which no amount of code-reading could
|
||
answer — are both ruled on and the track has landed; see `docs/COMPLETED.md` for the
|
||
full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cutoff/Q/
|
||
morph/drive/mod amount/key-track, every envelope stage time and level; root note, loop
|
||
span, and start frame still trigger a full reload). **Mid-stage rule = candidate (iv),
|
||
hold normalized stage position** (φ = elapsed/duration held fixed across a duration
|
||
change, then advancing at 1/newDuration). **Phase Γ opened six [Daniel]-class forks
|
||
(Γ-F1…Γ-F6) and all six are ruled** (Daniel, 2026-08-01) — the rulings are folded into the
|
||
tracks below and indexed in `docs/product/instrument-control-surface.md` §8. **Γ-F6 closed
|
||
with a correction to the analysis, not merely a ruling**: dynamic reported latency is routine
|
||
for VST3 instruments and REAPER handles it as a matter of course; what makes the mandated
|
||
restart expensive *here* is self-inflicted (`setActive(true)` calls `reloadInstrument`), so
|
||
the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed
|
||
around. **No track in this plan carries an unanswered [Daniel]-class question.**
|
||
|
||
### Flagged for awareness — not blocking, but decision-grade
|
||
|
||
1. **Item 15 has an unresolved cross-artifact seam, and it is the phase's largest
|
||
unknown.** The instrument is a read-only bank consumer by invariant
|
||
(`src/shell/instrument/CLAUDE.md`), and the one previous attempt at an
|
||
instrument→extension relay (S13) closed with a **DEGRADED** spike verdict and was
|
||
deferred (`docs/TODO.md`). Resample requires that crossing. Ξ-W2-T1 opens with the
|
||
architecture decision and this plan names the candidates rather than assuming the
|
||
deferred relay shape; see that track. Consequence to hold: if every candidate fails
|
||
verification, item 15's "one click from inside the VST" framing is what gives, not
|
||
the read-only invariant — the fallback is a bindable extension-side action.
|
||
|
||
2. **Phase Γ must land before Ξ-W2, and this is a correctness point, not a preference.**
|
||
Ξ-W2's settled reset scope enumerates parameters by name; Γ adds rate, pitch offset and
|
||
the limiter flag, so shipping the bake first means its reset list is incomplete on the
|
||
day it lands. Γ's product doc pre-classifies all three against the ratified rule (all
|
||
**reset**), so this costs no Daniel decision — only ordering. Second, weaker reason: Γ
|
||
owns params-payload v14 and v15, and Ξ-W3's programmed-signal persistence will want the
|
||
next rung; two phases contending for the ladder is the fight Θ's organizing constraint
|
||
exists to avoid.
|
||
|
||
3. **The taper work inside Γ-W1-T1 is a one-way door with respect to automation.** Once
|
||
VST3 parameters exist, the taper *is* the host-facing normalization, and re-tapering
|
||
re-interprets every recorded automation point in project files we do not own and cannot
|
||
migrate. Re-tapering is free today and permanently expensive afterwards. See
|
||
`docs/product/parameter-automation.md` §4 — that doc is scoping only, nothing in it is
|
||
scheduled here. **The same door applies to the stage-time ceiling**, which Γ-F3 left at
|
||
2.0 s with a 10 s ambition recorded in `docs/TODO.md`: if that ceiling is ever raised, it
|
||
wants to happen before the parameter system, not after.
|
||
|
||
## Phase-wide acceptance criteria
|
||
|
||
These bind every track in all three phases and are stated once here rather than repeated
|
||
per track. **Phase Γ adds five of its own**, stated in its phase header.
|
||
|
||
### 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.
|
||
|
||
**All seven waves have landed — Phase Θ is complete.** W1 through W7 each carry their own
|
||
landed note above; see `docs/COMPLETED.md` for every track's full narrative. Θ-W7 was
|
||
opened after Θ-W6-T1 shipped, to fix two rendering defects Daniel found by eye; it did not
|
||
exist in the plan when this phase was originally scoped.
|
||
|
||
**The organizing constraint.** Three surfaces in the instrument are single-writer by
|
||
nature and dictate the wave shape:
|
||
|
||
1. **The parameter model** (`zone_params.h` + `component_state_io`) — every parameter
|
||
addition touches both. Two tracks adding parameters concurrently is a merge fight
|
||
and two competing `ComponentState` version bumps.
|
||
2. **The voice render path** (`sampler_core.cpp`) — filter insertion, envelope
|
||
evaluation, loop read, and the Trigger tail all live there.
|
||
3. **The Sample face** (`editor_paint_sample.cpp` 516 lines / `editor_input_sample.cpp`
|
||
583 lines) — every UI item repaints it, and both are already at the ceiling.
|
||
|
||
Parallelism in this phase therefore comes from **splitting the Sample face into bands**
|
||
(done once, in Θ-W1-T1) and from **extension-side work being genuinely disjoint** — not
|
||
from running two parameter-model tracks at once. Where a wave has one track, the
|
||
collision is real and the serialization is the correct answer.
|
||
|
||
---
|
||
|
||
### Θ-W1 — Collapse and re-seam
|
||
|
||
**All three tracks have landed** — Θ-W1-T1 (`zone-retirement`), Θ-W1-T2
|
||
(`capture-handoff-bugs`), and Θ-W1-T3 (`filter-dsp-port`) — see `docs/COMPLETED.md` for
|
||
the full narrative of each. Between them: the zone subsystem is retired, the wave's two
|
||
responsibility seams (the `sampler_core` split, the Sample-face band split) are in
|
||
place, the two extension-side capture-handoff defects are fixed, and the filter DSP has
|
||
landed as a standalone pure module — Θ-W2-T1 has since wired it into the voice path (see
|
||
below).
|
||
|
||
---
|
||
|
||
### Θ-W2 — Filter in the voice path; waveform and chrome bands
|
||
|
||
**Depends on W1 for:** the one-parameter-set model and the `sampler_core` seam that T1
|
||
writes the filter into; the `filter` module T1 wires up; the Sample-face band split and
|
||
band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which decides
|
||
what T3's piano strip displays. Authoring any of this against the per-zone model means
|
||
writing storage plumbing W1 deletes.
|
||
|
||
**All three tracks have landed** — Θ-W2-T1 (`filter-voice-path`), Θ-W2-T2
|
||
(`stereo-waveform-lanes`), and Θ-W2-T3 (`toolbar-and-piano-strip`) — see
|
||
`docs/COMPLETED.md` for the full narrative of each. Between them: the filter sits in the
|
||
per-voice signal path with its own deck, the waveform band shows both channels in
|
||
stereo mode behind a type-enforced full-height overlay contract, and the chrome band's
|
||
toolbar and piano strip are cleaned up per spec. The three tracks were disjoint by
|
||
band — T1 owned the parameter model and the deck band, T2 owned the waveform band, T3
|
||
the chrome band — and none re-allocated the band stack.
|
||
|
||
---
|
||
|
||
### Θ-W3 — Live parameters, then the staged envelope system
|
||
|
||
**Depends on W2 for:** the filter envelope's existence — items 1, 8, and 14 govern
|
||
*three* envelopes, and the filter is the third; and for the Filter deck, which must
|
||
exist before it can receive a corner radio switch and inner curve dials. T1 additionally
|
||
depends on W2-T1 for the filter itself: the filter is the first control set where the
|
||
latched-at-note-on delivery model fails audibly, and it is what made the defect visible.
|
||
|
||
**Both tracks have landed** — Θ-W3-T1 (`live-parameter-delivery`) and Θ-W3-T2
|
||
(`staged-envelope-curves`) — see `docs/COMPLETED.md` for the full narrative of each.
|
||
Between them: every continuous playback control (filter cutoff/Q/morph/drive/mod
|
||
amount/key-track, every stage time and level on all three envelopes) now reaches a
|
||
sounding voice live instead of latching at note-on, and the envelope-overlay editor
|
||
grew from an amp-only fixture into the shared graphical surface for all three
|
||
envelopes — a corner radio switch per deck (none active by default), curve-shapeable
|
||
segments on every sloped stage (0.1–10 exponent, an inner dial paired with an overlay
|
||
knot), the release-right-anchored AHDSR layout against the pitch envelope's 1:1 AHD,
|
||
and the Trigger amp/filter fade pair folded into a Trigger AHD, consolidating what
|
||
were two staged-shape mechanisms into one. The Trigger × Preserve end-of-sample click
|
||
is fixed; the landed fix is wider than scoped, also ringing out Gate × Preserve ×
|
||
source-exhaustion, previously a hard cut. The ordering was deliberately serial — T1's
|
||
live-delivery mechanism landed first so T2's new curve exponents and Trigger AHD
|
||
fields were authored directly into it rather than backfilled afterward.
|
||
|
||
---
|
||
|
||
### Θ-W4 — Loop sustain and the velocity deck
|
||
|
||
**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain
|
||
is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves
|
||
modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for
|
||
the filter's existence and on W2-T3 for the preview button's toolbar position.
|
||
|
||
**Both tracks have landed** — Θ-W4-T1 (`gate-loop-sustain`) and Θ-W4-T2
|
||
(`velocity-deck-and-bipolar-curves`) — see `docs/COMPLETED.md` for the full narrative of
|
||
each. Between them: loop points are now a usable feature, with a Gate-mode loop acting
|
||
as the sustain and a parameterized crossfade at the seam; and the three velocity-curve
|
||
popups (amp, pitch, filter) now live together in a new VELOCITY deck group, with the
|
||
pitch and filter curves bipolar and flat-by-default so their modulation is off until
|
||
drawn, while the amp curve stays unipolar and unchanged, and the preview button's text
|
||
is replaced by a drawn play-triangle glyph. Params payload reached v11 with T1's loop
|
||
block and v12 with T2's velocity→pitch curve appended after it.
|
||
|
||
---
|
||
|
||
### Θ-W5 — Spline EGs
|
||
|
||
**Depends on W4 for:** the bipolar velocity-curve domain (W4-T2) — the spline algorithm
|
||
is **singly implemented and multi-referenced**, so its enhancement must land against the
|
||
final consumer set, and the last consumer to change domain is the pitch/filter velocity
|
||
curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline mode" is
|
||
only a real, testable rule once Gate has something to be unavailable *for*. It also
|
||
depends on W3's radio switch, which is how a spline contour reaches the overlay at all.
|
||
|
||
**Θ-W5-T1 has landed** — `spline-egs` — see `docs/COMPLETED.md` for the full narrative.
|
||
It shipped a free-drawn alternative to every staged envelope: pitch, filter, and amp
|
||
EGs can switch Staged → Spline and have their contour drawn directly on the waveform
|
||
overlay. The one shared monotone-spline implementation gained hard points — sharp
|
||
corners, no smoothing on either adjacent segment — flowing to every consumer including
|
||
the existing velocity→amp transfer curve, no fork. Both Staged and Spline state persist
|
||
simultaneously (saved-but-inactive, lossless round-trip); params payload reached v13,
|
||
and v12 projects still load. Gate is unavailable while a Spline EG is active; the
|
||
contour is a pure time function over the full sample length, normalized and drawn 1:1
|
||
with the sample's time axis. Point grammar converged on left-click add / right-click
|
||
delete / control-click hard-smooth toggle, one grammar across both spline consumers.
|
||
Staged segment knobs and their inner curve dials render disabled and reject edits while
|
||
Spline is active. Point-count ceiling: 128, a musical bound rather than a performance
|
||
one. A follow-on change in the same track reworked deck cell width: `-1` in `cellIds`
|
||
now means one cell's width, reserved and redistributed, rather than a blank cell holding
|
||
geometry — a Trigger face that drops Sustain and Release gets wider cells instead of
|
||
dead slots; group widths, row packing, deck height, and Gate-mode cell widths are
|
||
unchanged.
|
||
|
||
---
|
||
|
||
### Θ-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 was still moving would have meant
|
||
auditing and then re-auditing. Θ-W5-T1 has landed (see `docs/COMPLETED.md`), so that
|
||
surface has stopped moving and this dependency is satisfied. 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 has landed** — `legibility-and-antialiasing` — see `docs/COMPLETED.md` for the
|
||
full narrative. It shipped both halves together: knobs grew 28→40 px (inner curve dial
|
||
14→20), the deck cell 48×58→60×74, and the label band 12→16 px, now drawn in
|
||
`Font::Label` rather than `Font::Micro` — group captions and toggle segments deliberately
|
||
stayed `Font::Micro`, since bumping them would outgrow row 1's headroom at the floor
|
||
width. The editor's default/minimum size grew 840×620→980×680 to fit the wider deck at
|
||
floor width; an existing saved instance's window grows on open, and the floor is
|
||
validated by a derived test rather than literals. All fourteen time-constant labels now
|
||
read in ms (display-only; internal representation untouched) — `holdFraction` knobs,
|
||
`Len %`, and the bank panel's duration readout stayed out of scope. Double-click resets
|
||
each ring independently — outer ring resets the value, inner dial resets the exponent to
|
||
1.0 — reaching the chrome's preview-velocity knob too via a shared `inKnobFace` rule.
|
||
|
||
The antialiasing audit fixed knob arcs and needle, the inner dial arc and needle, staged
|
||
and spline envelope slopes, the spline contour, the velocity-popup trace, the waveform
|
||
outline, and the preview triangle; node handles, curve knots, knob discs, buttons, piano
|
||
keys, loop markers, borders, gradients, and text were already clean. The disposition
|
||
table is a standing artifact in `docs/product/visual-design-language.md` §8. The
|
||
piano-key open question is answered: not aliasing — every key is an axis-aligned
|
||
`LICE_FillRect`, so the earlier width defect was integer-division residue in the tiling,
|
||
not a sloped edge. High-DPI host scaling itself is unverified (deferred, see
|
||
`docs/TODO.md`).
|
||
|
||
All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.
|
||
|
||
---
|
||
|
||
### Θ-W7 — Arc-and-spline antialiasing fix
|
||
|
||
**Depends on W6 for:** a drawn surface to find a defect on — this wave did not exist in
|
||
the plan; it was opened after Daniel found two rendering defects by eye once Θ-W6-T1
|
||
shipped, so the audit's own output is what surfaced them.
|
||
|
||
**One track.**
|
||
|
||
**Θ-W7-T1 has landed** — `arc-and-spline-aa` — see `docs/COMPLETED.md` for the full
|
||
narrative. The stacked-`LICE_Arc` knob and dial rings never reached an opaque core (peak
|
||
alpha measured 138/255), and the staged/spline envelope and velocity-curve traces were
|
||
fully aliased rather than gapped, from integer `cy` quantizing the slope. Both defects,
|
||
plus the two needles, now route through one pure analytic thick-stroke rasterizer —
|
||
coverage in a new `core/ui/stroke_aa`, the single LICE blend in a new
|
||
`shell/instrument/editor_stroke` — replacing `LICE_Arc` and `LICE_ThickFLine` outright.
|
||
Measured: arc peak alpha 138/255 → 255/255, arc perpendicular-weight ripple 67% → 5%,
|
||
spline weight ripple 29% → 3%, at a cost of +0.09 ms per full editor repaint. Daniel then
|
||
ruled every sub-2 px stroker width up to 2 px, since the stroker only guarantees an
|
||
opaque core at width ≥ 2 px; the knob track arc, the inner-dial needle, and the deck's
|
||
mini velocity trace each moved 1.0 → 2.0 px. `docs/product/visual-design-language.md` §8
|
||
is corrected — a false "keeps every ring antialiased" claim is deleted, the rows are
|
||
re-dispositioned with measurements, and the audit's methodological lesson (verifying
|
||
which primitive was called is not verifying what it rasterized) is recorded as a standing
|
||
blockquote. All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.
|
||
|
||
---
|
||
|
||
## 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. Phase Θ landed as of Θ-W7-T1 (see
|
||
`docs/COMPLETED.md`); this gate is satisfied.
|
||
|
||
---
|
||
|
||
### Ξ-W1 — Consolidated tracking, and the programmed-note model
|
||
|
||
#### Ξ-W1-T1 — `tracking-consolidation`
|
||
|
||
**Landed** — see `docs/COMPLETED.md` for the full narrative. The provenance/usage
|
||
territory is now one system: a new `src/core/tracking/` directory holds `origin_ledger`
|
||
(the record family — `OriginRecord`/`OriginKind`, the insertion-ordered `OriginLedger`,
|
||
its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion` load
|
||
classification) and `tracking_authority` (the one decision surface: `pruneProtection`
|
||
and `tiedUsageExists`), retiring `core/model/owned_manifest`. Both prune's protected set
|
||
and the resample's replace-vs-add decision are computed from one borrowed
|
||
`TrackingState`, so the two safety-critical consumers cannot drift apart.
|
||
`sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
|
||
decisions, not the codecs. The deferred persisted-instance-identity fix was **not**
|
||
folded in — the deferral is restated in `docs/TODO.md`, its one home. This track
|
||
landed `OriginRecord`'s birth-time `parentSampleId` chain as the lineage mechanism, but
|
||
**Ξ-W2-T1's "naming and lineage" open question (jointly held with this track) is
|
||
unaffected and still theirs to close** — display naming and user-readable iteration
|
||
lineage were not decided here.
|
||
|
||
#### Ξ-W1-T2 — `note-program-model`
|
||
|
||
**Landed** — see `docs/COMPLETED.md` for the full narrative. The programmed-capture-
|
||
signal model is a new pure module directory, `src/core/instrument/note/` — a fourth
|
||
peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_division`,
|
||
`tempo`, and `note_program` (`Velocity`, the denominated `OffsetAmount`, the anchored
|
||
`StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below
|
||
are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign
|
||
uniform, positive is later in time; only an inverted window is refused, reported via
|
||
`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed — note length
|
||
stays musical-division-only, and an offset stores the denomination it was entered in.
|
||
|
||
---
|
||
|
||
### Ξ-W2 — The bake chain
|
||
|
||
**Depends on Ξ-W1 for:** the consolidated lineage records that make replace-vs-add
|
||
computable (T1) and the programmed-note record the offline pass renders (T2). **Also
|
||
depends on all of Phase Θ** — see the phase note above.
|
||
|
||
**One track.** The bake is one gesture and one chain; the architecture decision at its
|
||
head governs every step after it.
|
||
|
||
#### Ξ-W2-T1 — `resample-bake-chain`
|
||
|
||
**Goal.** One click bakes the dialed sound into a bank capture, re-points the instance at
|
||
it, and hands the instrument back at neutral — the dial → bake → dial-again loop, run
|
||
without leaving the sampler.
|
||
|
||
**Consolidates item 15 (the chain; the popup UI is Ξ-W3-T1).**
|
||
|
||
**Surface boundary — owns:** whichever seam the architecture decision selects (see below),
|
||
plus `shell/capture/capture_orchestrator` (a resample entry point alongside
|
||
`captureAndIndexOne`), `core/model/bank_model` + `owned_manifest` at the add boundary,
|
||
`shell/actions` (a new action, if the decision needs one), and the instrument-side bake
|
||
trigger. **Does not own** the capture-signal popup's UI — that is Ξ-W3-T1.
|
||
|
||
##### The architecture decision — this track's first deliverable, before any chain work
|
||
|
||
Item 15 requires a click **inside the VST3 editor** to cause a **bank write**, and the
|
||
instrument is a read-only bank consumer by invariant. The one prior attempt at this
|
||
crossing (S13) closed **DEGRADED** and was deferred. Resolve this before building the
|
||
chain. Two orthogonal choices:
|
||
|
||
**Decision 1 — how the click crosses to the extension.**
|
||
- **(1a) Ext-state request key + extension-side timer poller**, with a claim/clear nonce —
|
||
the shape S13 spiked and deferred. It needs a new instrument WRITE seam into ext-state,
|
||
though the `rsusage_`-prefixed guarded write already establishes the precedent that such
|
||
a seam can exist without weakening the read-only-*bank* invariant. Known cost: the
|
||
cross-process handshake race the S17 spec rejected.
|
||
- **(1b) Direct action invocation over the VST-host bridge — the lead candidate.** A
|
||
REAPER-hosted VST3 already resolves REAPER API functions **by name** over the host
|
||
callback (that is how the instrument reads `GetProjExtState`), and can fetch its own
|
||
host context — the track/take/project it was instantiated in. If `NamedCommandLookup`
|
||
and `Main_OnCommand` resolve the same way, the editor can invoke the extension's bake
|
||
action **synchronously**, with no request key, no poller, and no nonce — which dissolves
|
||
the S13 problem for this case rather than re-litigating it. **[verify]** against
|
||
`vendor/reaper-sdk/sdk/` (`reaper_plugin.h`, `video_processor.h`,
|
||
`reaper_plugin_functions.h`) that both resolve over the callback, and that calling them
|
||
from the editor's UI thread is safe. This is a verification task, not an assertion — do
|
||
not build on it until confirmed.
|
||
- **(1c) No crossing — a bindable extension-side action only.** Fallback. It abandons
|
||
Daniel's "from directly inside the ReaSampler 9000" framing, so it is the answer only if
|
||
(1a) and (1b) both fail. If it is taken, say so explicitly rather than quietly shipping
|
||
a panel button.
|
||
|
||
**Decision 2 — what actually renders the audio.**
|
||
- **(2a) Headless voice-engine render, extension-side.** The extension reads the instance's
|
||
component state, reconstructs the engine from the shared pure core, renders the
|
||
programmed note, and banks the result through the normal capture add-path. Exact and
|
||
fully deterministic. **Trade-off:** the extension currently links `component_state_io`
|
||
but deliberately **not** `sampler_core`/`pitch_shift` — `component_state_io` was split out
|
||
precisely to avoid pulling the voice engine into the extension. This reverses that.
|
||
- **(2b) REAPER offline render of the hosting track**, with the programmed note. Reuses
|
||
`OfflineRenderBackend` and with it the whole precision-invariant apparatus — exact
|
||
bounds, bit-identical repeats, relative paths, tail control — for free. **Trade-offs:**
|
||
it needs a *temporary* arrange mutation to program the note (the realtime backend's
|
||
temp-track-created-and-removed-cleanly is the precedent), and the existing scope model
|
||
has no "this one FX only" scope, so post-instrument track FX would be baked in unless a
|
||
new bypass scope is added. The arrange must be byte-identical afterward.
|
||
- **(2c) The instrument renders, the extension banks it.** The instrument produces the
|
||
audio (it already owns the engine) into a temp file or shared buffer; the extension
|
||
moves it into the bank and indexes it. Splits the work along the existing ownership
|
||
line — but adds a handoff artifact and its cleanup.
|
||
|
||
**Recommended sequence for the decision:** verify (1b) first — if it holds, the crossing
|
||
is nearly free. Then pick between (2a) and (2b) on the strength of the
|
||
"only-the-instrument, not the track chain" requirement: Daniel's ask says "send a trigger
|
||
or gate **through the sampler** offline," which favours (2a) or (2c) unless (2b) gains a
|
||
single-FX bypass scope. Record the verdict in this track's review, and if the verdict is
|
||
DEGRADED, escalate rather than improvise — the fallback is (1c) + (2a).
|
||
|
||
**Behavior — the chain.**
|
||
- **One gesture, whole chain.** A single click performs: offline pass → capture → bank
|
||
update (replace or add-distinct) → instance re-point → parameter reset. **One action
|
||
from the user's side, not a wizard.**
|
||
- **Offline pass through the instrument's own processing.** The audio is produced by
|
||
sending a trigger or gate through the sampler **offline** — the instrument's own voice
|
||
path, with filtering, pitching, and amp exactly as dialed. The recapture is of that
|
||
processed output, not of the raw source.
|
||
- **The recapture is a bank capture like any other** — project-relative, indexed,
|
||
browsable from any surface that browses the bank, recorded in the owned-file manifest,
|
||
and governed by the same safety rules as every file the system creates.
|
||
- **The note is the capture's root.** The rendered note is the capture's root note — which
|
||
is exactly why **the root-note parameter is not reset by resampling**: capturing at root
|
||
is what makes root survivable, and resetting it would detune every subsequent iteration.
|
||
- **Gate's hold and tail come from the programmed window.** The programmed note length is
|
||
the Gate hold bound — the gate holds for the note length, then releases; a Gate loop
|
||
cycles within the held span and the render still terminates. The **end offset** is the
|
||
home of the tail policy: captured time past the note's end is where the release rings.
|
||
- **Replace, or add distinct.** When nothing else references the source capture, the
|
||
recapture **replaces** it as the bank entry; when other references exist, the original
|
||
entry stays and the recapture is **added as a new distinct capture**. "Other reference"
|
||
means **any usage of the source capture tied to it by the provenance/recapture system**
|
||
— Ξ-W1-T1's records. Read plainly: the reference universe is the resample system's own
|
||
lineage records, **not** the prune-protection universe. Bank multi-membership, items
|
||
placed in the arrange, and a plain hold by another instance outside any recapture
|
||
lineage do **not** force add-distinct, and need not for safety — the superseded file
|
||
survives until prune, and prune's protection universe is unchanged and broader.
|
||
- **Replacement never destroys audio bytes.** "Replace" means the bank entry now denotes
|
||
the recapture; the superseded file is not deleted. Prune remains the system's only
|
||
file-deletion authority — resample writes a new file and retires the old one to
|
||
reclaimable-by-prune status; it never overwrites or deletes it. Until a prune reclaims
|
||
it, the pre-bake audio survives on disk — **the iterate loop's built-in recovery floor.**
|
||
- **The instance re-points** at the new/corrected bank capture and plays the baked sound.
|
||
- **Parameters reinitialize to default** — destructive to the dialed settings **by
|
||
design**: the processing now lives in the recaptured audio, and neutral controls are the
|
||
starting point for the next iteration.
|
||
- **Reset scope: only what the bake baked in.** Ratified by Daniel, not derived:
|
||
- **Reset** (their effect is in the audio): the envelope contours — staged and spline
|
||
alike — the filter parameters, master gain, the pitch envelope/engine settings, the
|
||
velocity transfer curves (their effect at the programmed velocity is in the audio), and
|
||
the loop points (they shaped the render, and old positions are meaningless against new
|
||
audio).
|
||
- **Survive** (mapping facts, not present in the audio): the **root note**, whatever
|
||
remains of key mapping (key-tracking; any key-range concept Θ-W1-T1 settled), and the
|
||
VOICE group (polyphony behavior leaves no trace in a single rendered note).
|
||
- **No timeline item, ever.** Resampling is a capture act: it writes a file to the bank and
|
||
updates the index; nothing is placed in the arrange view. Any framing that auto-inserts
|
||
the recapture is invalid — and if architecture (2b) is chosen, its temporary programming
|
||
item must leave the arrange byte-identical.
|
||
- **Undo/recovery: a plus, not a requirement.** Welcome if it falls out cheaply; the
|
||
feature ships without it. The guaranteed recovery path is the superseded file surviving
|
||
on disk until a prune reclaims it.
|
||
|
||
**Acceptance criteria.**
|
||
- On a dialed-in instrument, one click yields all of: a recapture in the bank, the instance
|
||
holding that recapture, and the baked-in audio parameters at their defaults — with the
|
||
root note and the other surviving mapping parameters untouched.
|
||
- **The bake is audible and faithful:** after the click, playing the programmed capture
|
||
note (root, at the programmed length, offsets, and velocity, in the active mode) through
|
||
the now-neutral controls sounds as the dialed instrument sounded just before the click —
|
||
the processing has moved from the controls into the audio.
|
||
- **A Gate-mode bake terminates on its own:** the gate holds for the programmed note
|
||
length, then releases — even with loop-sustain active, the render ends. No indefinite
|
||
capture.
|
||
- **After the bake the root note is unchanged** — iteration never detunes; the next bake
|
||
plays the same root.
|
||
- **Sole-reference case:** the bank afterwards shows the recapture where the source
|
||
capture's entry was; no other bank entry is disturbed. **Other-references case**
|
||
(provenance-tied usage of the original exists, computed by Ξ-W1-T1): the original entry
|
||
is untouched, a distinct new entry appears, and **every other holder of the original
|
||
sounds exactly as before.**
|
||
- **The click deletes no file:** the superseded audio file still exists on disk afterwards,
|
||
and only a later prune — under the settled orphan rules, only when nothing references it
|
||
— can reclaim it.
|
||
- **The arrange timeline is untouched:** no item appears anywhere, on any track, and if a
|
||
temporary programming item was used it is gone and the arrange is byte-identical.
|
||
- **Iteration composes:** dial → click → dial → click bakes the second pass onto the
|
||
first's result, repeatable indefinitely.
|
||
- **Save/reload:** an instance holding a recapture reloads and plays it exactly like any
|
||
other loaded capture.
|
||
- The bake adds nothing to `process()` — the offline pass runs off the audio thread.
|
||
|
||
**Open questions.**
|
||
- **The architecture decision [propose, with a verification step].** See above. The (1b)
|
||
bridge verification is **[verify]**; the choice among the candidates is **[propose]**.
|
||
- **Extension presence [propose].** The instrument plays self-contained with the extension
|
||
absent, but the bank is the extension's surface and resampling mutates the bank. The
|
||
natural answer is that resample **requires the extension present and is cleanly
|
||
unavailable — not silently lossy — without it**; propose the exact behavior at review.
|
||
- **Reset-scope edge cases [verify].** The rule is settled and the per-parameter
|
||
classification is ratified. Only a genuinely new parameter — one arriving with a Θ track
|
||
and absent from the ratified lists — is classified **against the rule** and surfaced at
|
||
review. Not a new Daniel call. (Θ adds: curve exponents → reset; spline contours →
|
||
reset, already named; the filter's velocity/key-tracking mod → reset with the filter;
|
||
loop crossfade → reset with the loop points; the Staged/Spline mode flag → classify.)
|
||
- **Naming and lineage [propose, jointly with Ξ-W1-T1's lineage-record question].** When
|
||
add-distinct fires, the new capture needs a display name (derived from the original?),
|
||
and the bank some way to read iteration lineage across repeated bakes. One proposal,
|
||
both tracks.
|
||
- **Provenance of the recapture — homed in item 17**, i.e. Ξ-W1-T1. A resample's recipe is
|
||
the instrument's own settings, not a track's chain. Nothing to decide here beyond
|
||
consuming that answer.
|
||
|
||
---
|
||
|
||
### Ξ-W3 — The capture-signal popup
|
||
|
||
**Depends on Ξ-W2 for:** the bake chain that the popup programs and its preview must not
|
||
diverge from. The acceptance criterion "preview and bake cannot diverge" requires one
|
||
render path, which W2 owns — building the preview against a second path is exactly the
|
||
defect the criterion exists to catch.
|
||
|
||
**One track.**
|
||
|
||
#### Ξ-W3-T1 — `capture-signal-popup`
|
||
|
||
**Goal.** Let the user program the capture signal — note length, offsets, velocity — and
|
||
hear it before committing the bake.
|
||
|
||
**Consolidates item 15 (the popup sub-feature).**
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/curve_popup`'s sibling — a new pure popup
|
||
geometry module for the capture-signal sheet (mirror the `curve_popup` precedent: centered
|
||
sheet, width/height clamps, title row, Close button rect, outside-sheet dismissal test) —
|
||
plus the editor's popup paint/input wiring in the deck or chrome band, and the preview
|
||
trigger's call into W2's render path. Reads Ξ-W1-T2's note-program record; does not
|
||
re-own it.
|
||
|
||
**Behavior.**
|
||
- **A popup menu programs the capture signal:** **note length**, **start and end offsets —
|
||
in ms AND in beats** — and **velocity**.
|
||
- **A preview trigger button** auditions the capture note exactly as currently programmed
|
||
— the user hears the bake before committing it — and the offline pass renders that same
|
||
programmed performance.
|
||
- **Note length is a musical-division picker** spanning 1/64th to 64/1 with dotted and
|
||
triplet multipliers.
|
||
- **Offsets are anchored** — start to note-on, end to note-off — and each is readable and
|
||
editable in **both** ms and beats, the two views of one stored value.
|
||
- **Beat-denominated values resolve against the project tempo under the cursor.** The
|
||
shell reads the tempo; the arithmetic is Ξ-W1-T2's.
|
||
- **The programmed velocity is the render velocity** — material because the velocity
|
||
transfer curves modulate amp, pitch, and filter at that velocity.
|
||
- House-consistent: drawn through the shared kit by palette role, geometry pure and
|
||
CTest-covered, no decorative animation.
|
||
|
||
**Acceptance criteria.**
|
||
- The popup exposes: note length as a musical-division picker spanning **1/64th to 64/1
|
||
with dotted and triplet multipliers**; start and end offsets, **each readable and
|
||
editable in both ms and beats**, anchored to note-on and note-off respectively; and
|
||
velocity.
|
||
- **Its preview trigger auditions the capture note exactly as programmed, and the bake
|
||
renders that same programmed performance — preview and bake cannot diverge.** Verify
|
||
structurally (one render path), not just by ear.
|
||
- **Beat-denominated values resolve against the project tempo under the cursor:** the same
|
||
programmed division yields a correspondingly different rendered duration when the tempo
|
||
at the cursor differs.
|
||
- The programmed signal persists with the instance and round-trips save/reload.
|
||
- The popup's geometry and dismissal test are pure and unit-tested; no hit-test math lands
|
||
in shell code.
|
||
|
||
**Open questions.** Both of Ξ-W1-T2's residuals (negative offsets; the note-length
|
||
denomination seam) surface here as UI consequences — if T2 answered them, this track
|
||
implements the answer; if T2 deferred either, this track is where it becomes visible and
|
||
must be closed.
|
||
|
||
---
|
||
|
||
## Phase Γ — The instrument's control surface
|
||
|
||
**Ships:** the deck reflowed into two categorical rows with a double-height MASTER bus deck,
|
||
a PITCH/RATE deck with playback-rate and baseline-pitch controls, a master limiter with
|
||
dynamic reported latency and a real output meter, one consistent knob interaction/taper law
|
||
across every variable control, a fix for staged contour traces drawing straight, and a
|
||
re-approached loop/crossfade marker UX under an explicit chrome-row loop enable.
|
||
|
||
**Consolidates:** none of the seventeen. Phase Γ came from a direct interview with Daniel
|
||
(2026-08-01); the product reasoning, the measured layout table, the invariant collisions and
|
||
the fork rulings are in **`docs/product/instrument-control-surface.md`**. Read §1.2 (the
|
||
layout table) and §7 (collisions) before dispatching any track here — every number in this
|
||
phase is derived there, and `docs/TODO.md`'s old deck-rework geometry is superseded.
|
||
|
||
**Fork state — all six forks are ruled; nothing in this phase awaits Daniel.** Indexed at
|
||
spec §8, folded into the tracks below:
|
||
- **Γ-F1** — `kEditorMinHeight` stays **680**.
|
||
- **Γ-F2** — the limiter has **lookahead with DYNAMIC reported latency** (zero when off,
|
||
the lookahead when on, reported to host PDC). *This inverted the product recommendation;*
|
||
W1-T2's scope grows accordingly — spec §3.1.1.
|
||
- **Γ-F3** — the stage-time ceiling stays **2.0 s** in this phase. The 10 s ambition is
|
||
carried in `docs/TODO.md` with its rationale and its prerequisites.
|
||
- **Γ-F4** — there **is** an explicit loop enable, and it lives on the **chrome row**, not
|
||
in a deck. W2-T2's scope grows accordingly — spec §6.4.
|
||
- **Γ-F5** — MASTER's reserved slot is **one** cell. The 90 px headroom argument behind
|
||
that is spec §1.6 and governs every future control addition.
|
||
- **Γ-F6** — **ship dynamic latency as ruled.** The `restartComponent(kLatencyChanged)`
|
||
deactivate/reactivate the SDK mandates is accepted: *"the limiter will either be on or off
|
||
on its instance, toggling during playback is not a use case."* No constant-latency
|
||
fallback, no measurement gate. *This ruling also corrected the analysis* — spec §3.1.1.
|
||
|
||
**What the Γ-F6 ruling changed in the analysis, not just in the plan.** Dynamic latency
|
||
reporting is **routine** for VST3 instruments and REAPER handles it as a matter of course;
|
||
the SDK's deactivate/reactivate requirement (`pluginterfaces/vst/ivsteditcontroller.h:105-108`)
|
||
is the normal contract, not an exotic one. What makes the cycle expensive **here** is entirely
|
||
our own doing: `ReaSamplerProcessor::setActive(true)` calls `reloadInstrument()` — a bridge
|
||
read plus a full WAV re-decode (`reasampler_processor.cpp:89-97`) — where a typical plugin's
|
||
`setActive` only allocates and frees buffers, and the deactivate side's freeing of
|
||
`live_`/`draining_`/graveyard (`:98-107`) is likewise our own design. **The cost is therefore
|
||
ours to reduce if it ever matters, and the reduction is decoupling reload from activation —
|
||
not abandoning dynamic latency.** That improvement is filed as a `docs/TODO.md` entry with its
|
||
trigger condition; it is not scheduled in this phase.
|
||
|
||
**Sequencing against Phase Ξ — Γ runs BEFORE Ξ-W2.** Two reasons, both the same shape as
|
||
Ξ's own stated gate:
|
||
1. **The bake bakes the control surface.** Ξ-W2's settled reset scope enumerates parameters
|
||
by name; Γ adds rate, pitch offset and the limiter flag. Shipping Ξ-W2 first means its
|
||
reset list is incomplete on the day it lands. (Γ's doc §3.4 pre-classifies all three
|
||
against the ratified rule — all **reset** — so this is a sequencing point, not a new
|
||
Daniel question.)
|
||
2. **One params-payload ladder.** Γ takes v14 and v15. Ξ-W2 does not currently bump the
|
||
payload, but Ξ-W3's programmed-signal persistence will, and two phases contending for the
|
||
ladder is exactly the fight Θ's organizing constraint calls out.
|
||
|
||
**The organizing constraint.** Five surfaces are single-writer and dictate the wave shape:
|
||
`ui/deck_values.cpp` (the taper law, then the two new controls), `editor_paint_waveform.cpp`
|
||
(the contour trace, then the loop marks), `ui/deck_groups.cpp` (the row predicate, then the
|
||
PITCH/RATE descriptor, then the reflow's row consumption — three tracks, three waves),
|
||
`engine/voice.cpp` (the Preserve read path, then the rate compounding into it), and the
|
||
params-payload ladder. **Every wave boundary below is one of those collisions**, not a
|
||
preference. Where a wave has more than one track, the tracks are disjoint by surface.
|
||
|
||
**Resequenced 2026-08-01 (Daniel), two changes.** The prior four-wave shape put the reflow at
|
||
W3 and the Preserve stretcher at W4; both moved.
|
||
|
||
1. **The reflow is split, canvas from arrangement.** The window floor and the width budget it
|
||
is derived from land **early** (Γ-W1-T4), so every other UI track in the phase is drawn,
|
||
tested and judged at the final 1190 × 680 window instead of at a size a later wave changes
|
||
under it. The two-row *arrangement* stays late (Γ-W3-T1), because it can only be measured
|
||
once the final PITCH/RATE and MASTER descriptors exist. The seam is stated at Γ-W1-T4.
|
||
2. **`preserve-time-stretch` moved W4 → W1-T5.** It is the longest pole in the phase and has
|
||
**zero dependency on any UI work** — a pure `core/instrument/engine/` module. Scheduling it
|
||
last was a scheduling error. Consequence: it is no longer Rate's *successor* but its
|
||
**prerequisite**, which retires the interim resample-and-cancel stand-in entirely — see
|
||
Γ-W2-T1.
|
||
|
||
Net: four waves become three, and both of the phase's DSP unknowns (the limiter, the
|
||
stretcher) are exposed in wave 1 rather than one of them landing last.
|
||
|
||
**The params-payload ladder is unchanged by the resequencing, and re-checked against the new
|
||
membership.** Exactly one bump per wave, owned by exactly one track: **W1-T2 owns v14** (the
|
||
limiter flag), **W2-T1 owns v15** (rate + pitch offset), and **every other track in the phase
|
||
owns no rung** — W1-T4 changes no persisted field, W1-T5 adds no parameter, W2-T2's loop
|
||
enable maps onto the already-persisted `SampleLoop::hasLoop`, and W3-T1 is layout only. The
|
||
ordering still works because v14 lands a whole wave before v15, and neither of the two tracks
|
||
that moved touches the codec.
|
||
|
||
**The editor's deck is knowingly mis-composed between Γ-W1-T4 and Γ-W3-T1, and that is not a
|
||
defect report.** Raising the floor without the reflow leaves the greedy whole-group wrap
|
||
packing two ragged left-aligned rows with categorically wrong membership. Γ-W1-T4 states the
|
||
exact interim layout; do not "fix" it in a track that does not own it.
|
||
|
||
**Phase-wide acceptance criteria** (in addition to the ones stated at the top of this file):
|
||
- **Bypassed means byte-identical.** With the limiter off, the per-sample output path is
|
||
byte-identical to today's bare ramped multiply — the same discipline that makes
|
||
`live == nullptr` byte-identical to the pre-live core and the filter's exact skip at
|
||
`modAmount == 0` hold the at-rest path unchanged.
|
||
- **No `ComponentState` sound change.** A project saved before this phase reopens sounding
|
||
identical: absent rate lifts to 100 %, absent pitch offset to 0 st, absent limiter flag to
|
||
bypassed. Re-tapering a knob (Γ-W1-T1) changes needle angles only — the payload stores raw
|
||
engine doubles, so saved values reload bit-identical.
|
||
- **`kVelocityPitchRangeSemitones` / `kPitchDepthMaxSemis` (24.0) does not move.** It is
|
||
load-bearing in the v12 wire format. The new Pitch knob **reads** it; it does not mint a
|
||
second ±24 constant.
|
||
- **The window floor is 1190 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 sets it, in
|
||
wave 1; no other track in the phase may move it**, and from that point every track is
|
||
authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun.
|
||
**`kEditorMinHeight` stays 680** (Γ-F1). The remaining **90 px of width headroom is the
|
||
budget for the life of this layout** — one deck cell is 60 px, so there is room for exactly
|
||
one more, once. Spec §1.6 states the ledger; read it before adding any control. Chrome-row
|
||
additions are a **separate purse** (they are paid for out of the title slot, not the floor)
|
||
and must not be charged against this one.
|
||
- **Reported latency is zero unless the limiter is on.** `getLatencySamples()` returns 0 with
|
||
the limiter bypassed, in every track and at every point in the phase. Only W1-T2 may
|
||
introduce a non-zero value, and only under the limiter-on condition.
|
||
- **Geometry stays pure.** Every new layout, cap, label and hit-test rule lands in a pure
|
||
CTest-covered module (`knob_deck`, `sample_bands`, `waveform_view`), never in a painter.
|
||
|
||
---
|
||
|
||
### Γ-W1 — Foundations
|
||
|
||
**Depends on:** nothing in this phase. **Five tracks, disjoint by surface** — re-verified
|
||
against this membership rather than carried over from the four-wave shape:
|
||
|
||
| Track | Owns |
|
||
|---|---|
|
||
| **T1** `knob-interaction-law` | `ui/deck_values`, `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, the shared modifier helper in `editor_internal.h` |
|
||
| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**v14**) |
|
||
| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **pure** tessellation helper |
|
||
| **T4** `editor-floor-and-row-law` | `ui/sample_bands.h` (the floor), `ui/knob_deck.h` (budget constants + two invalidated header notes), `ui/deck_groups` (the row predicate **only**), five test fixtures |
|
||
| **T5** `preserve-time-stretch` | `engine/pitch_shift` + a new pure stretcher module, `engine/voice.{h,cpp}`'s Preserve read path |
|
||
|
||
**One shared file in the wave, named rather than discovered at merge:
|
||
`src/core/instrument/engine/CMakeLists.txt`.** T2 declares two new pure libraries and their
|
||
test targets there; T5 declares one. Both are append-only additions in separate blocks —
|
||
**textual merge adjacency, not semantic contention.** Whichever lands second rebases.
|
||
|
||
**Two near-misses that are avoided by construction, and must stay avoided.** (a) T3's
|
||
tessellation helper **lands pure** (`ui/envelope_overlay` or a new pure module), *not* in
|
||
`editor_internal.h`, which T1 is editing — this also satisfies the phase's geometry-stays-pure
|
||
criterion, so it costs nothing. (b) T4 touches `deck_groups` but adds **only** the new row
|
||
predicate; it does not touch `sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and
|
||
it does not touch `deck_values`, which is T1's.
|
||
|
||
**Both of the phase's DSP unknowns are in this wave** — T2's limiter and T5's stretcher. That
|
||
is deliberate: they are the two tracks whose gate can fail, and failing in wave 1 is
|
||
recoverable in a way that failing in the last wave is not.
|
||
|
||
#### Γ-W1-T1 — `knob-interaction-law`
|
||
|
||
**Goal.** One consistent, unit-category-driven interaction and taper rule across every
|
||
variable control, landed **before** any new control is added so the new ones are authored
|
||
into it rather than retro-fitted.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §4.
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/deck_values` (the taper maps, the
|
||
snap-unit table, `resetDeckParam`), `core/instrument/ui/param_slider` (the drag law),
|
||
`shell/instrument/editor_input_*` (modifier read + re-anchor), and the modifier-reading
|
||
helper the three input paths share. **Does not own** any deck descriptor, any parameter, or
|
||
the waveform painter.
|
||
|
||
**Behavior.**
|
||
- **Shift snaps to whole numbers in the control's displayed unit**; **Ctrl scales the drag by
|
||
0.05**; **Shift+Ctrl = Shift wins** (Ctrl is ignored — with an integer-quantized output a
|
||
finer drag yields the same sequence, so this is identity, not a compromise).
|
||
- **Snap unit by category:** ms knobs → whole ms; semitone knobs (incl. Rate, when it
|
||
arrives) → whole semitones; percent/fraction knobs → whole percent; the 12 curve-exponent
|
||
inner dials → whole numbers (which puts 1.0, the linear neutral, one snap away); master
|
||
gain → whole dB; already-integer controls unchanged. Full table in the spec §4.2.
|
||
- **Mid-drag modifier transitions re-anchor** — on every press *and* release during an active
|
||
drag, the current value becomes the anchor value and the current cursor position the anchor
|
||
position. The value is continuous across the transition; only the rate changes. Without
|
||
this the grab-anchored absolute drag (`kKnobDragRangePixels = 128`) jumps by
|
||
`(1 − 0.05) ×` the accumulated delta.
|
||
- **Millisecond knobs become log-scaled.** Exactly 0 s at norm 0 and exactly
|
||
`kEnvTimeMaxSeconds` at norm 1, monotone throughout; **10 ms lands within 0.12–0.20 of
|
||
travel and 100 ms within 0.42–0.52**. The ceiling stays **2.0 s** — it reads
|
||
`kGateStageMaxSeconds`, which the AHDSR overlay's schematic scale is derived from, and the
|
||
two must agree.
|
||
- **Semitone knobs become log2/centre-expanded.** Symmetric, exactly 0 at centre, exactly
|
||
±`kPitchDepthMaxSemis` at the ends, monotone; **±7 st reached at 50–58 % of each
|
||
half-travel**.
|
||
- **`resetDeckParam` bypasses the taper** — it writes the default value directly instead of
|
||
round-tripping through `norm → value`. This *removes* the power-of-two dependency the
|
||
header currently documents rather than working around it; that comment
|
||
(`deck_values.h:42-46`) becomes wrong and must be rewritten.
|
||
- **Scope is the parameter, not the widget.** Deck knobs (outer ring and inner dial),
|
||
envelope stage nodes and curve knots all honour it — they are surfaces onto one model, and
|
||
a snap on one but not the others is a divergence. **Waveform markers are explicitly
|
||
excluded**: they carry a shipped zero-crossing snap on the same modifier space and their
|
||
domain is frames.
|
||
|
||
**Acceptance criteria.**
|
||
- Every taper change is verified **persistence-neutral**: a project saved before the change
|
||
reopens with bit-identical stored values and identical audio; only needle angles move.
|
||
- Holding Shift mid-drag on each unit category lands the documented whole unit; releasing it
|
||
does not jump the value.
|
||
- Holding and releasing Ctrl mid-drag is continuous — no step at either transition.
|
||
- Double-clicking any knob (outer ring and inner dial independently) lands **exactly** on its
|
||
default at every taper, verified against a default-constructed `PlaySeconds` rather than a
|
||
round trip.
|
||
- The log/log2 landmark positions above are asserted in `deck_values`' own tests.
|
||
- One shared modifier-read helper serves all drag surfaces; no second modifier grammar exists.
|
||
|
||
**Open questions.** None **[Daniel]** — fork Γ-F3 is ruled: **the ceiling stays 2.0 s.** The
|
||
10 s ambition Daniel described (*"a horrifically long decay with tight exp"*) is carried as a
|
||
`docs/TODO.md` entry, and **this track lands both of its prerequisites**: the log taper (which
|
||
is what makes a higher ceiling usable at the low end rather than unusable) and the reset
|
||
bypass (which retires the power-of-two dependency — 2.0 is a power of two, 10.0 is not).
|
||
**Neither is optional on that basis alone** — they are already required by this track — but
|
||
the engineer should know the reset bypass is doing double duty, and should not "simplify" it
|
||
back into a norm round-trip.
|
||
|
||
#### Γ-W1-T2 — `master-bus-audio`
|
||
|
||
**Goal.** The master limiter and the meter's **audio and publication halves**, plus **the
|
||
plugin's first latency reporting** — no editor drawing. Landing the audio ahead of the deck is
|
||
what lets Γ-W3 draw against real published state instead of a stub.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §3.1, **§3.1.1 (latency — read this
|
||
first; it was rewritten when Γ-F6 closed, so an older reading of it is wrong)**, §3.2–3.3,
|
||
§3.5, §7.10, §7.11, §8.2.
|
||
|
||
**Surface boundary — owns:** a new pure limiter module and a new pure meter-ballistics
|
||
module under `core/instrument/engine/` (each with its own `<module>_tests` target),
|
||
`shell/instrument/reasampler_processor` (the chain, the published block state, **and the
|
||
`getLatencySamples` / `restartComponent(kLatencyChanged)` path**), and **params payload v14**
|
||
(the limiter enable flag). **Does not own** MASTER's deck geometry or any drawing — that is
|
||
Γ-W3-T1.
|
||
|
||
**Behavior.**
|
||
- **Chain:** `voice mixer → master gain (existing ramped multiply) → limiter (bypassable) →
|
||
output bus`, with the meter tapped at the **bus output, post-limiter**.
|
||
- **Limiter: a single toggle, no configurable controls.** Baked ceiling **−0.3 dBTP**.
|
||
Default **off**. **No makeup gain, ever, of any kind** — transparent at rest.
|
||
Stereo-linked detection (max |L|,|R| drives one gain) so the image is not moved.
|
||
- **True-peak detection is sidechain-only** — an oversampled detector in the sidechain, never
|
||
oversampling the signal path. Factor is the engineer's call under the measure gate.
|
||
- **Lookahead, with DYNAMIC reported latency (Γ-F2, ruled).** `getLatencySamples()` returns
|
||
**0** when the limiter is off and **the lookahead in samples** when it is on; the toggle
|
||
calls `IComponentHandler::restartComponent(kLatencyChanged)`. **None of this exists today** —
|
||
there is no `getLatencySamples` override, no `kLatencyChanged`, and no `restartComponent`
|
||
call site anywhere in `src/`; the plugin ships the SDK default of 0. This track introduces
|
||
the plugin's first latency reporting.
|
||
- **The restart is routine; the fencing is against a standing scar, not against the flag.**
|
||
Dynamic latency reporting is ordinary VST3-instrument behaviour and REAPER handles it as a
|
||
matter of course. The SDK's deactivate/reactivate requirement
|
||
(`pluginterfaces/vst/ivsteditcontroller.h:105-108`) is the normal contract. **What makes the
|
||
cycle expensive here is this plugin's own `setActive`** — reactivate calls
|
||
`reloadInstrument()`, a bridge read plus a full WAV re-decode
|
||
(`reasampler_processor.cpp:89-97`), where a typical plugin only allocates buffers; deactivate
|
||
frees `live_`/`draining_`/graveyard (`:98-107`) for a documented reason (ghost sustained
|
||
voices). **Γ-F6 is ruled: ship it — the toggle is a patch-design gesture, not a
|
||
during-playback one.** Do **not** build a constant-reported-latency fallback and do **not**
|
||
gate the deliverable on a measurement. The reduction of that self-inflicted cost is filed in
|
||
`docs/TODO.md` ("Decouple the instrument reload from VST3 activation") with its trigger
|
||
condition; it is out of scope here. The four requirements below survive as engineering
|
||
hygiene against the `kIoChanged` scar, and all four are acceptance criteria:
|
||
1. **Verify the whole call sequence against the vendored Steinberg SDK** before writing it,
|
||
including the ordering rule that the new latency is what `getLatencySamples` returns
|
||
*after* `setActive(true)` — so **the reported value must derive from persisted state, not
|
||
from a transient the deactivate clears.**
|
||
2. **Prove the restart does not disturb the output bus arrangement.** The output stays one
|
||
permanently-stereo bus, never renegotiated.
|
||
3. **Ship a regression test in the spirit of `testDualMonoStereoSampleRendersCentered`** —
|
||
a dual-mono capture rendered across a limiter toggle stays centered, L ≡ R.
|
||
4. **`restartComponent` is never called from `process()`.** Main/UI thread only, and
|
||
coalesced so repeated clicks produce one restart per settled state.
|
||
**This is NOT the change `reasampler_processor.cpp:66-68` forbids.** That warning is against
|
||
reintroducing per-mode **bus** renegotiation (`kIoChanged` class), which panned a dual-mono
|
||
capture hard right in the host's pin re-routing; `kLatencyChanged` is a different flag and the
|
||
bus is untouched. But the precedent — mid-session `restartComponent` in this plugin has
|
||
already shipped one real regression — is exactly why (2) and (3) are non-negotiable.
|
||
- **Flipping the toggle during playback: apply immediately, do NOT defer to a transport
|
||
boundary** (product ruling, spec §3.1.1). A deferred restart leaves the plugin misaligned by
|
||
the lookahead with no visible cue, which is worse than a visible interruption; and the host,
|
||
not the plugin, schedules the deactivate/reactivate anyway. **Daniel has accepted the
|
||
interruption outright** (Γ-F6) — it is not a case to design for. Two things remain in scope,
|
||
and neither is a mitigation for it:
|
||
- **A short (≤ 10 ms) equal-gain crossfade over the engage/disengage.** Kept as a *quality*
|
||
measure, not a mitigation: a limiter engaging is a gain-path change, and this codebase
|
||
already ramps every gain-path change (`kGainRampSeconds`, `ValueRamp`). It also earns its
|
||
keep independently of the restart, because **we do not control when the host acts on the
|
||
request** — our own transition must be clean in the window before it does.
|
||
- **The limiter enable is classified NOT automatable**
|
||
(`docs/product/parameter-automation.md` §3.8) so nothing can flip it at rate. It is also
|
||
**not** the plugin's `kIsBypass` parameter.
|
||
- **Per block the processor publishes, as relaxed atomics:** per-channel peak `max|x|`, a
|
||
latched clip flag, and the block's maximum gain reduction. **No dB conversion, no
|
||
ballistics, no hold timers on the audio thread** — the UI converts and runs ballistics from
|
||
block peaks and elapsed time. This widens the existing advisory-peak pattern
|
||
(`reasampler_processor.h:109-113`), which is not reusable as-is.
|
||
- **Meter ballistics (pure, unit-tested):** instantaneous rise; **fall 20 dB/s**; peak-hold
|
||
latched at the running max, **held 1.5 s**, then falling at the same rate; scale **linear in
|
||
dB over −60…+6 dBFS**; clip latches at block peak ≥ 0 dBFS and is cleared on request.
|
||
- **`ComponentState` payload v14** appends the limiter flag as a strict suffix on the existing
|
||
discipline; a v13 blob is a strict prefix and lifts to bypassed.
|
||
|
||
**Acceptance criteria.**
|
||
- **With the limiter bypassed the rendered output is byte-identical to the pre-change build**,
|
||
asserted by a regression baseline, not by ear.
|
||
- With the limiter engaged, no output sample exceeds the ceiling on program material that
|
||
exceeds it by up to +12 dB; with it bypassed and gain driven, the output does exceed
|
||
0 dBFS (proving the toggle is doing the work).
|
||
- **Nothing is louder at rest with the limiter on.** A signal that never reaches the threshold
|
||
is bit-identical engaged and bypassed.
|
||
- No allocation, no lock, no transcendental on the per-sample path; the measure-and-report
|
||
gate reports per-voice-block CPU with the limiter engaged at 32 voices.
|
||
- The meter-ballistics module is pure and CTest-covered: rise, 20 dB/s fall, 1.5 s hold, clip
|
||
latch/clear, and the dB↔pixel map are all asserted without a host.
|
||
- A project saved before this change reopens with the limiter bypassed and sounding identical.
|
||
- **`getLatencySamples()` returns exactly 0 with the limiter off**, and the lookahead in
|
||
samples with it on — asserted against the persisted flag, and correct across a
|
||
deactivate/reactivate cycle.
|
||
- **A dual-mono capture rendered across a limiter toggle stays centered** (L ≡ R), and the
|
||
output bus arrangement after a latency-change restart is identical to before it.
|
||
- **The plugin emits no hard step at the toggle** — the engage/disengage crossfade is asserted
|
||
on a rendered signal, not judged by ear.
|
||
|
||
**Open questions.**
|
||
- **No [Daniel] questions. Fork Γ-F6 is ruled** — dynamic latency ships as specced, the
|
||
deactivate/reactivate is accepted, and there is no fallback design and no measurement gate.
|
||
Do not reintroduce either; the constant-reported-latency option is closed, not shelved.
|
||
- **[verify]** `temp_cortex/` has already been assessed and **rejected** (spec §3.5) — do not
|
||
re-litigate it, and do not transplant from it.
|
||
- **[record, not a gate]** While the limiter is in REAPER under your hand, note what the
|
||
restart actually costs — do notes cut, is the re-decode perceptible, does transport hiccup —
|
||
and record it in this track's review. It is **not** a gate on shipping and no outcome changes
|
||
the design; it is the trigger-condition evidence for the `docs/TODO.md` entry "Decouple the
|
||
instrument reload from VST3 activation," which is where that cost gets reduced if it ever
|
||
matters. Do **not** restructure `setActive` here: its destructive shape is deliberate and its
|
||
reasoning (ghost sustained voices on reactivate) is documented at the call site.
|
||
|
||
#### Γ-W1-T3 — `contour-trace-curves`
|
||
|
||
**Goal.** Staged envelope segments draw as the curve their exponent defines, so the
|
||
mid-segment knot stops floating off its own trace.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §5.
|
||
|
||
**Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s staged-envelope
|
||
trace and any pure tessellation helper it needs. **Does not own** the loop/crossfade marks
|
||
(Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace.
|
||
|
||
**Behavior.** The defect is verified: `editor_paint_waveform.cpp:218` drops knots
|
||
(`if (v.knot) continue;`) and joins the remaining vertices with straight strokes, and
|
||
`curveMap` is never called in the paint path even though the exponent is in scope at `:211`.
|
||
Knot *positioning* already honours the exponent via `curveMidLevel`
|
||
(`envelope_overlay.cpp:94-105`) — that divergence is the visible symptom. The fix draws each
|
||
sloped stage through **the same `curveMap` the audio uses**, so trace and sound cannot
|
||
diverge; tessellation approach is the engineer's call.
|
||
|
||
**Acceptance criteria.**
|
||
- **At every exponent the knot's centre lies on the trace, within 1 px** — the reported defect,
|
||
stated as the gate.
|
||
- **At exponent 1.0 the segment is visually identical to today's straight line.**
|
||
- No visible faceting at the widest segment the canvas can produce; a fixed low tessellation
|
||
count is not acceptable at full width.
|
||
- All three envelopes, both play modes, all sloped stages (attack/decay/release) — one paint
|
||
path, one fix.
|
||
- The established trace grammar is unchanged: one weight, `kEnvTracePx = 2.0`, through the
|
||
analytic stroker. Both overlay layout policies (AHDSR right-anchored schematic, AHD 1:1)
|
||
are honoured unchanged. The spline overlay's own trace is untouched.
|
||
- **Audio is unchanged** — this is a drawing defect only; a regression baseline proves it.
|
||
|
||
#### Γ-W1-T4 — `editor-floor-and-row-law`
|
||
|
||
**Goal.** Commit the **canvas** — the window floor, the width budget it is derived from, and
|
||
the row every deck group belongs to — so every other UI track in the phase is drawn, tested and
|
||
judged at the final window size. The **arrangement** inside that canvas is Γ-W3-T1's.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §1.1 (the two categories), §1.2 (the
|
||
floor arithmetic block), §1.6 (the headroom ledger), §7.1 and §7.4 (the two invalidated
|
||
`knob_deck.h` notes).
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/sample_bands.h` (`kEditorMinWidth`),
|
||
`core/instrument/ui/knob_deck.h` (the declared budget constants and the two invalidated header
|
||
notes), `core/instrument/ui/deck_groups.{h,cpp}` (**the new row predicate only**), and the five
|
||
test fixtures that read the floor — `test_sample_bands.cpp`, `test_deck_groups.cpp`,
|
||
`test_knob_deck.cpp`, `test_sample_chrome.cpp`, `test_keyboard_strip.cpp`. **Does not own**
|
||
`layoutDeck` / `deckRowCount` / `deckHeight` behaviour, the justification law, any descriptor,
|
||
MASTER's inventory or interior, any painter, or any parameter. It changes **no drawing code at
|
||
all.**
|
||
|
||
**Behavior — what it commits.**
|
||
- **`kEditorMinWidth` 980 → 1190. `kEditorMinHeight` stays 680** (Γ-F1).
|
||
- **Three declared budget constants in `knob_deck.h`:** the row block both rows will justify
|
||
inside (**1020**), the right-anchored spanning deck's reserved width (**MASTER 142**), and
|
||
the ceiling (**1280**). These are *declarations of budget*, not measurements — nothing
|
||
computes them from a descriptor, and Γ-W3-T1's job is to prove its content fits inside them.
|
||
- **The floor is derived, not asserted as a literal.** `1020 + kDeckGroupGap(12) + 142 +
|
||
2·kPad(8) = 1190`. `kEditorMinWidth` stays a literal in `sample_bands.h` — **do not add an
|
||
include edge from `sample_bands` to `knob_deck`**, which would invert the allocator's
|
||
deliberate independence from the deck (it takes `deckHeight` as a *parameter* for exactly
|
||
that reason). The identity is asserted in `test_deck_groups.cpp`, which already includes
|
||
both headers. This is the Θ-W6-T1 derived-floor precedent, landed once and never rewritten.
|
||
- **Row membership becomes a property of the group id:** `DeckRow { Sound, Contour, Spanning }`
|
||
+ `deckRowFor(DeckGroupId)` in `deck_groups`, an **exhaustive switch** on the
|
||
`isLiveDeckParam` discipline, so a future group is a compile error rather than a silent
|
||
default. Partition: **Sound** = PITCH/RATE, FILTER, VELOCITY, VOICE; **Contour** = PITCH ENV,
|
||
FILTER ENV, AMP ENVELOPE; **Spanning** = MASTER. **Nothing consumes it until Γ-W3-T1** — that
|
||
is the seam, and it is why the predicate is safe to land now: **membership is a property of
|
||
the group, width is a property of the descriptor**, and only the widths are still moving.
|
||
- **Γ adds no new deck group**, so no later track amends this predicate.
|
||
|
||
**The seam, stated as what this track can and cannot assert.**
|
||
|
||
*Can assert today:*
|
||
- The derived floor identity above, and `kEditorMinWidth ≤ 1280` with **90 px** of headroom.
|
||
- `kEditorMinHeight == 680`, asserted so no later track drifts Γ-F1's ruling.
|
||
- `deckRowFor` is total over `DeckGroupId` and yields exactly the partition above.
|
||
- **Row 2's natural width already fits the block, in both play modes:**
|
||
252 + 312 + 312 = **876 ≤ 1020**, leaving both its gutters ≥ `kDeckGroupGap`. Mode-stable
|
||
because FILTER ENV's and AMP's reserve slots hold them at 312 in Gate and Trigger alike.
|
||
- **MASTER's reserve is not yet spent:** `deckGroupWidth(MASTER) == 72 ≤ 142`.
|
||
- At the floor, deck band **216** and waveform band **358** — the reflow's 112 px arrives here,
|
||
two waves early (see the interim layout below).
|
||
|
||
*Cannot assert yet, and must not force:*
|
||
- **Row 1's natural width does not fit the block.** Today it is PITCH 150 + FILTER 524 +
|
||
VELOCITY 192 + VOICE 164 = **1030**, against the 1020 block. The 50 px deficit is exactly
|
||
what the two descriptor changes buy: PITCH → PITCH/RATE **+42** (Γ-W2-T1) and FILTER's
|
||
`Band|Notch` moving to the caption corner **−92** (Γ-W3-T1), netting **980**. Record the
|
||
target and the two contributions as a test comment; **assert the fit in Γ-W3-T1, and do not
|
||
pre-empt either descriptor change to close it early.**
|
||
- Gutter distribution, the filter tie-line at x = 636, flush outer edges, MASTER's interior and
|
||
its meter — all Γ-W3-T1. Every one of them measures a descriptor that does not exist yet.
|
||
|
||
**The interim editor, stated exactly so it is not filed as a defect.** At the new floor,
|
||
`availWidth = 1190 − 2·kPad = 1174`, and the **unchanged** greedy whole-group wrap packs:
|
||
|
||
```
|
||
row 1 PITCH 150 · PITCH ENV 252 · FILTER 524 = 950 used, 224 px ragged right
|
||
row 2 FILTER ENV 312 · AMP 312 · VELOCITY 192 ·
|
||
VOICE 164 · MASTER 72 = 1100 used, 74 px ragged right
|
||
```
|
||
|
||
**Two rows, not three** — so the deck band is already 216 and the waveform already draws at its
|
||
final 358 px, in both Gate and Trigger. After Γ-W2-T1 lands PITCH/RATE the pack is row 1 = 992,
|
||
row 2 unchanged; still two rows. The composition is wrong in exactly the way the reflow exists
|
||
to fix — PITCH ENV sits up with the sound decks, VOICE and MASTER sit down with the envelopes,
|
||
MASTER is still a single-height 72 px box, and both rows are left-packed with dead space at the
|
||
right. **Worse than today in composition, better in proportion.** That is the accepted
|
||
transitional state for the rest of the phase.
|
||
|
||
**Do not convert the two-row interim into a claim.** It is a coincidence of the greedy wrap at
|
||
exactly this width, not a guarantee — which is precisely why Γ-W3-T1's criterion is "two rows
|
||
**by construction**, asserted against the group inventory, not observed as a wrap outcome."
|
||
`testDeckFitsInsideTheEnforcedMinimumWindow` currently asserts `deckRowCount == 3`; relax it to
|
||
an **upper bound** (`<= 2`), which is a real regression canary throughout the interim and is
|
||
subsumed by Γ-W3-T1's exact claim. An exact `== 2` here is acceptable only with a comment
|
||
naming it as a wrap outcome the reflow replaces.
|
||
|
||
**Acceptance criteria.**
|
||
- **The floor is 1190 × 680, reached by a derived test over the three budget constants**, not
|
||
by a literal — and the derivation is the one Γ-W3-T1 later reads rather than a second copy.
|
||
- **Headroom is exactly 90 px** against the 1280 ceiling, asserted.
|
||
- `deckRowFor` is exhaustive over `DeckGroupId`; adding a group without classifying it fails to
|
||
compile.
|
||
- Row 2's natural width and MASTER's unspent reserve are asserted, in **both** play modes.
|
||
- **All five floor-reading test fixtures pass at the new floor** — including the chrome row,
|
||
whose title slot gets *more* room at 1190, not less.
|
||
- **No drawing code changes, no descriptor changes, no parameter changes, no audio change.**
|
||
A regression baseline proves the last of those trivially.
|
||
- The two invalidated `knob_deck.h` notes (§7.1's fourteen-pixel headroom figure, §7.4's
|
||
cells-and-floor pairing) are **re-derived against the new floor, not deleted** — §7.4's
|
||
restatement is *the deck's cell metrics AND its group/row composition both drive
|
||
`kEditorMinWidth`; none of the three may move alone.*
|
||
|
||
**Open questions.** **No [Daniel] questions.** **[propose at review]** whether the three budget
|
||
constants belong in `knob_deck.h` (the deck owns the row block and the spanning-deck reserve)
|
||
or in `sample_bands.h` (the allocator owns the floor they derive). The plan's lean is
|
||
`knob_deck.h` with the identity in the test, because it adds no include edge; either is
|
||
defensible, but the *derivation must live in exactly one place*.
|
||
|
||
#### Γ-W1-T5 — `preserve-time-stretch`
|
||
|
||
**Goal.** A real pitch-preserving time-stretcher for Preserve mode, written from established
|
||
state-of-the-art literature — landed **before** the control that drives it, so Rate ships onto a
|
||
finished engine rather than onto a disposable stand-in.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §2.5.
|
||
|
||
**Moved from Γ-W4-T1 (Daniel, 2026-08-01).** It is the longest pole in the phase and has zero
|
||
dependency on any UI work. **The consequence is the interesting one: it inverts the
|
||
relationship with Rate.** Under the old order the stretcher was Rate's quality upgrade and
|
||
Γ-W2-T1 shipped an interim resample-and-cancel path to make Rate complete on day one; under
|
||
this order the stretcher is Rate's **prerequisite** and **the interim path is not built at
|
||
all.** Skipping a stand-in that was only ever going to be deleted is the win; see Γ-W2-T1's
|
||
named contingency for what happens if this track's gate slips.
|
||
|
||
**Precedent for landing a DSP module ahead of its consumer:** Θ-W1-T3 (`filter-dsp-port`)
|
||
landed the filter DSP as a standalone pure module a wave before Θ-W2-T1 wired it into the voice
|
||
path, for the same reason — the unknown is the DSP, not the wiring.
|
||
|
||
**Surface boundary — owns:** `core/instrument/engine/pitch_shift` and whatever new pure module
|
||
the stretcher needs (each with its own `<module>_tests` target), plus `voice.{h,cpp}`'s Preserve
|
||
read path. **Does not own** any parameter, any UI, the varispeed path, or the deck. It adds no
|
||
`ComponentState` field and takes **no rung of the payload ladder**.
|
||
|
||
**Behavior and constraints.** The algorithm is **the engineer's call under a
|
||
measure-and-report gate — this plan deliberately names none.** The constraints:
|
||
- **The stretch ratio is an argument, not a parameter.** Nothing publishes a non-unity ratio
|
||
until Γ-W2-T1's Rate knob does. Until then the Preserve read path runs at ratio 1.0 and must
|
||
be **bit-identical to the shipped Preserve read** — a stronger and cheaper regression gate
|
||
than the old plan's A/B-against-an-interim-path, because the baseline is a build that exists.
|
||
- **CPU stance (Daniel, verbatim intent):** *"we should be efficient but accept the cost of
|
||
high-quality algorithm choices. It's 2026, most people's computers can handle audio with
|
||
ease. Just don't be wasteful."*
|
||
- **RT-safe:** no allocation, no I/O, no lock in `process()`; buffers sized at voice
|
||
allocation or at the off-audio-thread reload, on `pitch_shift`'s existing pre-warm
|
||
precedent.
|
||
- **Per-voice state, holding up at the 32-voice ceiling.** The gate is 32 simultaneous
|
||
Preserve voices at 50 % and 200 %, not one voice at 100 %.
|
||
- **No new third-party dependency** (`pitch_shift`'s standing property).
|
||
- **No dispatch on the per-sample path** — concrete, inlineable types; no `IStretcher`.
|
||
- **Onset behaviour is a regression surface.** GA2 eliminated Preserve's ~25 ms onset latency
|
||
by priming the ring with the actual upcoming source. **A stretcher that reintroduces an
|
||
onset delay or a first-frame smear is a regression, not a trade-off.**
|
||
|
||
**Acceptance criteria.**
|
||
- **Ratio 1.0 with no shift is bit-identical to the shipped Preserve read**, asserted by a
|
||
regression baseline — the null case, and the criterion that makes landing this ahead of Rate
|
||
safe.
|
||
- Preserve speaks on frame 0 — no added onset latency, no first-frame smear, in any
|
||
ratio/shift combination.
|
||
- No audible metallic or phasey artefacting on sustained tonal material at ±6 st and
|
||
75–133 % ratio; transient material at 50 % / 200 % is no worse smeared than **varispeed
|
||
playback at the equivalent ratio** — the honest "what does preserving pitch cost" reference,
|
||
and the one that needs **no disposable implementation built to serve the comparison.**
|
||
- The Gate sustain-loop contract is unchanged: **loop the source, shift the output** — loop
|
||
points remain source-frame facts.
|
||
- **Measure and report before the algorithm is final:** per-voice CPU at 32 voices, added
|
||
latency (must be zero at the onset), and A/B recordings on three material classes (one-shot,
|
||
tonal sustain, full-mix bounce). Report to Daniel; the choice is not final until he has heard
|
||
the A/Bs.
|
||
|
||
**Open questions.** **[propose, with a measurement step]** the algorithm family itself.
|
||
**[verify]** that `core/instrument/CLAUDE.md`'s *"`WDL_Resampler` is not a Preserve engine —
|
||
never wire it as the duration-preserving path"* is honoured: under Preserve, Rate legitimately
|
||
changes duration, so a resampled read is an explicit duration control — but the
|
||
*pitch-preserving* mechanism must not be a resampler. **No [Daniel] questions.**
|
||
|
||
---
|
||
|
||
### Γ-W2 — New controls, and the overlay's marks
|
||
|
||
**Depends on Γ-W1 for — four dependencies, two of them new:**
|
||
1. **T1 ← W1-T1 (taper law).** Rate and Pitch must be authored into the finished
|
||
taper/modifier law, not retro-fitted into it, and the semitone taper must exist before a
|
||
second semitone knob does.
|
||
2. **T1 ← W1-T5 (the stretcher) — NEW, and the reason the interim path is gone.** Preserve
|
||
Rate has no engine without it. Under the prior four-wave order this dependency ran the other
|
||
way and was paid for with a disposable resample-and-cancel stand-in; the resequencing
|
||
inverts it. **Rate must not ship before its Preserve engine.**
|
||
3. **T2 ← W1-T3 (the contour trace).** Both write `editor_paint_waveform.cpp`; running them
|
||
together is a merge fight in one file.
|
||
4. **T2 ← W1-T1 and W1-T4, weakly.** W1-T1 also edits `editor_input_waveform.cpp` (the
|
||
modifier read), which T2 rewrites for marker hit-test routing — serial, so not a conflict,
|
||
but T2 rebases onto it. And T2's "the enable costs no window width" criterion is now
|
||
asserted against **W1-T4's** derived floor test rather than one this track has to write.
|
||
|
||
T1 additionally inherits `engine/voice.{h,cpp}` from W1-T5 — a **hand-off, not a conflict**:
|
||
W1-T5 defines the Preserve ratio seam, and T1 feeds it. Serial across waves by construction.
|
||
|
||
**Disjointness — re-verified against this wave's membership, not carried over.** Both tracks
|
||
stayed in W2 and nothing entered or left it, so the prior finding is re-checked and stands. T1
|
||
owns the parameter model, the engine and the deck descriptors; T2 owns the waveform band's
|
||
marks and their pure geometry **and the chrome row's loop enable**. The two are disjoint at the
|
||
module level with **one named exception: `shell/instrument/editor_session.cpp`.** T1 may touch
|
||
it for the third commit tier's routing; **T2 owns `pickedMarkers` and `applyMarkers` there and
|
||
nothing else.** The partition is by function and the two do not overlap — **textual merge
|
||
adjacency, not semantic contention** — but it is a shared file in a phase whose wave boundaries
|
||
are otherwise single-writer surfaces, so it is stated rather than discovered at merge. Whichever
|
||
track lands second rebases onto the first. No *new* in-wave adjacency was created by the
|
||
resequencing: T2 touches neither `deck_values` nor `deck_groups` nor `voice`.
|
||
|
||
**The format ladder stays clean.** The loop enable maps onto the existing
|
||
`SampleLoop::hasLoop`, which is already persisted and whose `start`/`end` are already written
|
||
unconditionally — **no new field, no version bump** — so T1 keeps sole ownership of payload v15
|
||
exactly as specced.
|
||
|
||
#### Γ-W2-T1 — `pitch-rate-deck`
|
||
|
||
**Goal.** PITCH becomes **PITCH/RATE**: three knobs (`Key Trk | Rate | Pitch`) under the
|
||
existing Varisp|Presrv toggle, with both new controls wired through the engine.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §2.
|
||
|
||
**Surface boundary — owns:** `core/instrument/engine/play_params.h` +
|
||
`core/instrument/map/play_seconds.h` (the two new fields),
|
||
`core/instrument/map/component_state_io` + `params_payload` (**payload v15**),
|
||
`core/instrument/engine/voice.{h,cpp}` (the compounding and the note-on latch),
|
||
`core/instrument/ui/deck_groups` (the PITCH/RATE descriptor **and** the three-state live
|
||
predicate), `core/instrument/ui/deck_values` (the two new bindings). **Does not own** the
|
||
deck's row layout — that is Γ-W3-T1 — nor the time-stretcher itself (Γ-W1-T5, already landed
|
||
by the time this track runs).
|
||
|
||
**Behavior.**
|
||
- **Rate: 50 %–200 %, default 100 % at true knob centre, exponential taper** — 50 % = −12 st,
|
||
200 % = +12 st, musically symmetric. This is **linear in semitones over ±12** and is the
|
||
stated exception to W1-T1's centre-expansion law (which applies to semitone knobs whose
|
||
throw exceeds ±12).
|
||
- **Pitch: a baseline pitch offset, ±24 semitones**, centred, on W1-T1's centre-expanded
|
||
semitone taper. **Reads `kPitchDepthMaxSemis`; does not mint a second constant.**
|
||
- **Varispeed:** keytrack ratio × rate ratio × pitch-offset ratio **compound into a single
|
||
read-increment multiply**; the rate offset applies to the varispeed pitch. Composes with
|
||
the pitch envelope's existing per-frame `ratio_` multiply — **no new per-sample stage**.
|
||
- **Preserve:** rate is an **absolute** value driving **duration only**; keytrack and pitch
|
||
offset drive the pitch shifter. **Rate drives the stretch ratio Γ-W1-T5's stretcher already
|
||
consumes — there is no interim path.** The stretcher is this track's prerequisite, not its
|
||
successor; the resample-and-cancel stand-in the prior plan carried is retired unbuilt (see
|
||
Open questions for the contingency). `core/instrument/CLAUDE.md`'s "never wire
|
||
`WDL_Resampler` as the duration-preserving path" is honoured by construction.
|
||
- **Rate is latched at note-on**, delivered by a **third commit class**: published into the
|
||
live block like any live parameter, read only by `snapLive`, never by `applyLive`.
|
||
`isLiveDeckParam`/`liveCommitFor` widens from two states to three
|
||
(`Live` / `NoteOnLatched` / `Reload`) in that one predicate — **not** a second table, and
|
||
**not** the reload tier (a swept knob must never trigger a WAV re-decode). **Record the
|
||
reason in the header:** loop resolution and contour mapping are note-on folds, so live rate
|
||
means re-folding a resolved loop and re-mapping a contour mid-note.
|
||
- **Pitch is live** — under Varispeed one more factor in a multiply the pitch envelope already
|
||
performs; under Preserve an addend to a shift the pitch envelope already modulates.
|
||
- **Loop points scale with rate; contours scale with rate.** Neither rewrites stored values:
|
||
the loop is source-frame facts traversed at the new increment (Varispeed) or the new read
|
||
rate (Preserve), and a contour is a function of normalized position. **Staged envelope stage
|
||
times do NOT scale** — 30 ms is 30 ms at any rate. That asymmetry is deliberate: a contour is
|
||
of the sample, a staged envelope is of the performance.
|
||
- **Deck descriptor:** three cells; `captionWidth` **70**, hard ceiling **80** (above that the
|
||
caption row overtakes the 180 px knob row and the group exceeds 192). If the text will not
|
||
fit at 80, narrow the `Varisp|Presrv` segments 48 → 44 (ceiling becomes 88) — **do not widen
|
||
the group**.
|
||
- **Payload v15** appends both fields as a strict suffix; a v14 blob lifts to rate 100 % /
|
||
pitch 0 st, bit-identical playback.
|
||
|
||
**Acceptance criteria.**
|
||
- Rate at 50 % plays an octave down and half speed under Varispeed; at 200 %, an octave up and
|
||
double speed. Under Preserve the same settings change duration only — pitch is unchanged
|
||
within the stretcher's tolerance. **Preserve Rate is a finished feature the day this lands**,
|
||
because Γ-W1-T5 already shipped its engine; a degraded or inert Preserve Rate is a failed
|
||
track, not an acceptable interim.
|
||
- Rate at exactly 100 % and Pitch at exactly 0 st render **bit-identical** to the
|
||
pre-change build, in both engines.
|
||
- Shift-drag on Rate lands on whole semitones (so an octave and a fifth are reachable by
|
||
hand); Shift-drag on Pitch lands on whole semitones; Ctrl gives cents on both.
|
||
- **A Rate change while a note sounds does not alter that note**; the next note-on takes it.
|
||
**It does not trigger a reload or an engine rebuild** — assert the tier, not just the sound.
|
||
- A Pitch change **does** move a sounding note, in both engines.
|
||
- With a loop set, changing Rate changes the loop's audible period without moving either
|
||
waveform marker.
|
||
- The PITCH/RATE group measures **exactly 192 px**; adding the two `DeckParam`s produces a
|
||
compile error in `isLiveDeckParam`'s exhaustive switch until they are classified.
|
||
- A v14 project reopens at rate 100 % / pitch 0 st and sounds identical.
|
||
|
||
**Open questions.**
|
||
- **None [Daniel].**
|
||
- **[propose at review]** Rate's clamp behaviour at the range extremes as it meets the
|
||
stretcher's own ratio bounds — one clamp, resolved where the two meet, not two that can
|
||
disagree.
|
||
- **Named contingency, not a plan item, and not to be taken silently.** If Γ-W1-T5's
|
||
measure-and-report gate has not passed when this track is ready to dispatch, the pre-agreed
|
||
fallback is the **resample-and-cancel composition** spec §2.5 records — a resampled read with
|
||
the resulting pitch change cancelled in the existing SOLA shifter — shipped as an interim
|
||
Preserve path with the stretcher as its later quality upgrade, i.e. a return to the prior
|
||
four-wave order. **Escalate to Daniel rather than taking it:** it revives a disposable
|
||
implementation and re-opens the `WDL_Resampler` guardrail conversation, and the whole point of
|
||
the resequencing was to avoid building it.
|
||
|
||
#### Γ-W2-T2 — `loop-crossfade-ux`
|
||
|
||
**Goal.** Give the loop an explicit enable, make the loop and crossfade marks legible, and
|
||
paint the crossfade where it is actually heard.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §6 — **read §6.1 (the diagnosis),
|
||
§6.4 (the enable) and §6.5 (trade-offs) in full before starting.** This is the phase's one
|
||
genuinely designed surface; the sections are the brief.
|
||
|
||
**Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s marker/loop draw,
|
||
`shell/instrument/editor_input_waveform.cpp`'s marker hit-test routing,
|
||
`core/instrument/ui/waveform_view` (cap rects, label boxes, the label-suppression rule — all
|
||
pure, all CTest-covered), and — **new, from the Γ-F4 ruling** —
|
||
`core/instrument/ui/sample_chrome` (the enable's rect in the toolbar control run),
|
||
`shell/instrument/editor_paint_chrome` + `editor_input_chrome` (its draw and hit-test), and
|
||
`shell/instrument/editor_session.cpp`'s **`pickedMarkers` / `applyMarkers` only** (the
|
||
retention rule — see the wave header for the shared-file partition). **Does not own**
|
||
`loop_span`, the crossfade model, any parameter, or any `ComponentState` version. **This
|
||
track changes drawing, hit-testing and one editor-state retention rule — no format change.**
|
||
|
||
**Behavior.**
|
||
- **An explicit loop enable on the CHROME ROW (Γ-F4, ruled).** A two-segment `Loop Off|On`
|
||
toggle joins the toolbar row's right-anchored control run, **immediately left of the
|
||
`Mono|Stereo` toggle**, with Browse still rightmost. Loop is a waveform-overlay concept and
|
||
**has no deck** — a deck cell was never the right home. Because the run is right-anchored
|
||
and the title slot absorbs it, **this costs zero window width and none of the 90 px
|
||
headroom**; if the title will not hold its text at the 1190 floor, **the enable's segments
|
||
narrow — the floor does not move.**
|
||
- **The enable IS `SampleLoop::hasLoop`. No new field, no version bump.** The field already
|
||
exists (`play_params.h:210`), is already what `resolveLoop` refuses on
|
||
(`loop_span.cpp:12`), and is already persisted in the payload's `loopOverride` block —
|
||
where **`start`/`end` are written unconditionally whatever `hasLoop` says**
|
||
(`params_payload.cpp:31-36`), so the wire can already carry "off, with a span remembered."
|
||
What changes is the field's *provenance*: today it is derived from the marker gesture, and
|
||
after this track it is **user-owned**, with the gestures as shortcuts onto it.
|
||
- **Collapse-to-off survives as a shortcut, not as a second state machine.** `hasLoop` is the
|
||
single authority; four gestures reach it:
|
||
| Gesture | Enable | Span | Crossfade |
|
||
|---|---|---|---|
|
||
| Enable → On | on | retained | retained |
|
||
| Enable → Off | off | **retained** | **retained** |
|
||
| Collapse the span onto itself | off | **destroyed**, re-parked at `defaultLoopBounds` | **zeroed** |
|
||
| Drag either loop mark while off | **on** | takes the drag | retained, re-clamped |
|
||
- **Two consequent behaviour changes, each with its reason.** (a) `pickedMarkers`'
|
||
re-park (`editor_session.cpp:221-226`) currently triggers on `!hasLoop`; it must become
|
||
conditional on the span being **invalid** (collapsed / inverted / out of range) rather than
|
||
on the enable being off — a toggle whose off→on does not restore what was there is a delete
|
||
button, not a toggle. (b) `applyMarkers`' crossfade zeroing (`:240-243`) moves from "the
|
||
enable is off" to "the span was destroyed." **The original reasoning is preserved, not
|
||
overruled:** it zeroes so a stale length cannot silently re-apply against a span that no
|
||
longer exists; with the span retained, its clamp bound is retained too and there is nothing
|
||
stale.
|
||
- **The "drag me" affordance splits into two off-states.** Off with **no span ever set** —
|
||
pair parked at `defaultLoopBounds`, Disabled, caption `DRAG TO SET LOOP`. Off with a **span
|
||
retained** — pair Disabled *at its own positions*, caption `LOOP OFF` (there is nothing to
|
||
"set"). In both, **dragging a mark turns the enable on** — the shipped drag-to-create
|
||
gesture survives and now teaches the enable by demonstration.
|
||
- **In Trigger the enable draws Disabled and inert, and does NOT clear `hasLoop`** —
|
||
Disabled-not-hidden, the same grammar as the marks, with its state restored on the return to
|
||
Gate. This transitively covers the drawn-EG case via `enforceGateUnavailableWhileDrawn`
|
||
(`play_params.h:198-205`), which forces Trigger whenever an envelope is drawn — one
|
||
predicate, not a second rule. **Disabled-but-grabbable (the off marks) vs.
|
||
Disabled-and-inert (Trigger) is deliberate:** the user's own off is reversible by the very
|
||
gesture on offer; Trigger's refusal comes from the engine and no drag can talk it out of it.
|
||
- **One mark grammar: line + shaped cap + label. The cap IS the grip.** Four marks:
|
||
**START** (`accent/primary`, solid right-pointing triangle cap, solid line — the only
|
||
primary-ink mark, because it is the only one always in effect); **LOOP** (`accent/secondary`,
|
||
L-cap opening right); **END** (`accent/secondary`, L-cap opening left); **XFADE**
|
||
(`accent/secondary` reduced alpha, ramp cap, **dashed** line — a soft boundary). This
|
||
replaces the bare 10 px orphan tab that today marks the crossfade with no line of its own.
|
||
- **Labels** in `Font::Micro`/`TextDim`, drawn **beneath** the trace and handles in z-order.
|
||
A mark's label **re-draws on top on hover or drag** of that mark. **A label is suppressed if
|
||
its box would overlap one already placed**; placement order is grabbed/hovered first, then
|
||
START, LOOP, END, XFADE. Occlusion by an envelope node is **accepted and named** — the cap
|
||
shape carries the identity permanently, the label is for learning.
|
||
- **The crossfade moves to `[loopEnd − crossfade, loopEnd)`** — where it is audible. The
|
||
handle moves to the loop-end side; **drag direction is unchanged** (left lengthens), so the
|
||
muscle memory survives.
|
||
- **The crossfade region draws as a top-and-bottom edge wedge, NEVER as a second fill.** A
|
||
triangular band at the overlay's top and bottom edges growing from zero at
|
||
`loopEnd − crossfade` to ~10 px at `loopEnd`. **This is a hard constraint:** the region is
|
||
now *inside* the loop span, where a translucent fill would stack on the 0.20 loop fill, and
|
||
the envelope trace crossing that fill is a known, accepted under-floor pair at 2.25:1
|
||
(`editor_paint_waveform.cpp:28-34`), whose own note says the FILL is what changes if it is
|
||
ever resolved. **The loop fill's peak alpha must stay exactly 0.20.**
|
||
- **The ingredient draws as a ghost.** `[loopStart − crossfade, loopStart)` draws the mirror
|
||
wedge at half alpha, no handle — **a hairline dashed outline at rest, filling in on hover or
|
||
drag of the crossfade handle**. This makes the `crossfade ≤ min(start, loopLength)` clamp
|
||
self-explanatory: the fade stops growing exactly when the ghost's left edge reaches START or
|
||
LOOP, so the user sees the reason instead of hitting an invisible wall.
|
||
- **Trigger mode:** the loop pair and the crossfade mark draw **Disabled and are not
|
||
grabbable**, with a dim `LOOP — GATE ONLY` caption — Disabled rather than hidden, matching
|
||
the editor's existing Gate-segment grammar, and because hiding a set loop on a mode flip
|
||
destroys information the user put there. START stays fully live.
|
||
|
||
**Acceptance criteria.**
|
||
- The four marks are distinguishable by ink and cap shape with the labels suppressed, and
|
||
named when they are not.
|
||
- **The shaded crossfade region sits over the frames where the fade is audible** — verify
|
||
against a rendered loop, not by reading the code.
|
||
- Every mark is grabbable by its cap; grabbing a mark shows its label.
|
||
- The crossfade at its clamp shows the ghost's left edge coincident with the bounding mark.
|
||
- **The loop fill's peak alpha is unchanged at 0.20** and the accepted 2.25:1 trace pair is
|
||
neither improved nor worsened.
|
||
- In Trigger, no loop mark accepts a grab, the chrome enable is Disabled and inert, and the
|
||
reason is on screen. Returning to Gate restores the enable's prior state.
|
||
- **Turning the enable off and on again restores the loop exactly** — same span, same
|
||
crossfade, no re-park. Collapsing the span instead turns it off, re-parks at
|
||
`defaultLoopBounds` and zeroes the crossfade. Both paths asserted.
|
||
- **Dragging a loop mark while the enable is off turns it on**, in both off-states.
|
||
- **The enable costs no window width:** `kEditorMinWidth` is unchanged by this track, asserted
|
||
by the same derived test that guards the floor.
|
||
- **No `ComponentState` version moves; no new persisted field; `resolveLoop` is untouched;
|
||
audio is unchanged.** The enable round-trips save/reload through the existing
|
||
`loopOverride` block, in both states, with the span retained across an off.
|
||
- All cap/label/suppression geometry is pure and unit-tested; no hit-test math in the painter.
|
||
The enable's rect lands in `sample_chrome` alongside the rest of the control run.
|
||
|
||
**Open questions.**
|
||
- **[propose at review, then verify by hand]** The claim-arbitration inputs change:
|
||
`markerHandleRect` today gives a tab to the crossfade only, and `resolveWaveformClaim`
|
||
breaks ties by smallest nominal target area. Giving every mark a cap-grip changes the
|
||
candidate set **and every nominal area in it**. The arbitration must be re-derived, and
|
||
`docs/TODO.md`'s open entry *"Pre-existing staged-envelope-node shadow at zero-attack"*
|
||
must be **re-evaluated against the new cap geometry and its outcome recorded** — resolved or
|
||
worsened, either is acceptable, silence is not.
|
||
- **No [Daniel] questions.** Fork Γ-F4 is ruled — there **is** an explicit enable and it is on
|
||
the chrome row, in this track. The prior framing ("an enable needs a cell, so it is a Γ-W3
|
||
layout decision") was wrong and is retired: loop has no deck, so it never needed one.
|
||
- **[propose at review]** every site that currently *infers* `hasLoop` — two in
|
||
`editor_input_waveform` (`:255`, `:258`), two in `editor_session` (`:222`, `:236`) — is now
|
||
writing to a user-visible control rather than to an internal flag. Re-read each in that
|
||
light; "it still compiles" is not a disposition.
|
||
- **Named escalation, not a fallback to take silently:** if the top strip reads crowded in the
|
||
DAW, the pre-designed answer is the marker rail (spec §6.2, Direction 2) — a larger build
|
||
that would also dissolve the arbitration problem structurally. Escalate; do not improvise a
|
||
half-rail.
|
||
|
||
---
|
||
|
||
### Γ-W3 — The reflow
|
||
|
||
**Depends on Γ-W2 for:** the PITCH/RATE descriptor (W2-T1) — the reflow measures the real
|
||
three-cell group, and laying it out against a forecast of that group means re-measuring
|
||
afterward. **This is the whole reason the arrangement is late**, and it is why the canvas was
|
||
split out of it into W1-T4.
|
||
|
||
**Depends on Γ-W1 for:** W1-T2's published meter/GR/clip state, which MASTER's deck draws
|
||
(drawing against a stub would mean building the meter twice), and W1-T4's floor, budget
|
||
constants and row predicate, which this track **consumes rather than re-derives**.
|
||
|
||
**One track.** The row law, the group inventory and the double-height deck are one geometry
|
||
decision spread over `knob_deck`, `deck_groups` and the deck painter. Splitting it would put two
|
||
tracks in the same pure modules. **The window floor is no longer part of it** — W1-T4 set it
|
||
two waves ago, and this track must not move it.
|
||
|
||
#### Γ-W3-T1 — `deck-reflow`
|
||
|
||
**Goal.** Two categorical rows plus a double-height MASTER bus deck, inside a 1280 × 720
|
||
ceiling, returning 112 px to the waveform.
|
||
|
||
**Spec:** `docs/product/instrument-control-surface.md` §1 (the whole section, incl. the §1.2
|
||
measured table **and §1.6, the headroom ledger**) and §3.2–3.3 (what MASTER draws). **§7 lists
|
||
the invariants this track invalidates or widens — read it before touching `knob_deck.h`.**
|
||
|
||
**Surface boundary — owns:** `core/instrument/ui/knob_deck` (the row law, the double-height
|
||
group, the justification — **consuming** W1-T4's budget constants, not restating them),
|
||
`core/instrument/ui/deck_groups` (consumption of W1-T4's row predicate, FILTER's `Band|Notch`
|
||
caption move, MASTER's inventory), and `shell/instrument/editor_paint_deck` (the MASTER
|
||
meter/limiter/bubble draw). **Does not own** `kEditorMinWidth` or any budget constant — those
|
||
are W1-T4's and are **read**, never moved — nor any parameter, the limiter DSP, or the
|
||
waveform band.
|
||
|
||
**Behavior.**
|
||
- **Row 1 (sound), one row, non-negotiable:** PITCH/RATE 192 · FILTER 432 · VELOCITY 192 ·
|
||
VOICE 164 = **980** natural.
|
||
- **Row 2 (contour):** PITCH ENV 252 · FILTER ENV 312 · AMP ENVELOPE 312 = **876** natural.
|
||
- **MASTER is double-height (216 px) and right-anchored**, outside both rows, 142 px wide.
|
||
- **FILTER's `Band|Notch` moves from its row-toggle position to the caption corner**, taking
|
||
the group 524 → **432** (−92 px). It occupies FILTER's currently-unused `captionToggle2`
|
||
slot — **no new geometry is required**.
|
||
- **VOICE keeps its `Retrig|Legato` row toggle.** Moving it to the caption makes VOICE
|
||
*wider* (226, not narrower), because its caption row is the binding side. Verified; do not
|
||
"fix" it.
|
||
- **Justification law, applied to BOTH rows:** space-between within the row block; slack
|
||
divided equally among the row's (n−1) gutters, integer residue to the leftmost;
|
||
**no gutter narrower than `kDeckGroupGap` (12)**. **Decks are never stretched.** MASTER is
|
||
not part of either row's justification.
|
||
- **Row block = 1020 px at the floor**, giving row 1 gutters 12/14/14 and row 2 gutters 72/72,
|
||
at which width **FILTER's right edge and FILTER ENV's right edge both land on x = 636**.
|
||
That tie-line, row 2's equal gutters, and row 1's minimum gutter being exactly
|
||
`kDeckGroupGap` all hold at 1020 and only at 1020 — **this is why the floor is 1190 and not
|
||
1186.** Above the floor the tie-line drifts and that is accepted (spec §1.3).
|
||
- **The floor is already 1190 × 680 and the bands are already 216 / 358** — Γ-W1-T4 landed all
|
||
four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track
|
||
changes none of those numbers; it makes them true by construction instead of by coincidence.**
|
||
Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030
|
||
today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion
|
||
and W1-T4 deliberately left it open.
|
||
- **The 90 px of remaining headroom is the budget for the life of this layout**, and one deck
|
||
cell is 60 px. **This is why MASTER's reserved slot is ONE cell** (Γ-F5, ruled): two would
|
||
spend 60 of the 90 up front on a control nobody has named, leaving 30 — which would freeze
|
||
row 1 forever, since any later row-1 addition needs 60. Widening MASTER later costs the same
|
||
60 it would cost now, and by then the trade is against a real control instead of a guess.
|
||
**State this ledger where a future reader will hit it** — spec §1.6 is its home, and a
|
||
reader proposing a new knob needs to see it before they propose.
|
||
- **MASTER's interior** (spec §1.4, exact to the pixel): caption row with the limiter toggle
|
||
and a **round** 12 px `warn` GR bubble in the far corner (non-interactive — the same slot the
|
||
envelope decks' radio uses; round so it reads as a lamp, not a control); **gain knob in the
|
||
upper-left cell at box-relative y = 26** and a **reserved empty slot at y = 138** — i.e. the
|
||
two cells land on row 1's and row 2's knob baselines exactly, which is what stitches the
|
||
spanning deck to both rows; **meter column 62 px wide × 186 px tall** on the right.
|
||
- **Three rules not to generalise wrongly:** MASTER's left column uses **fixed cell slots at
|
||
the two baselines, NOT the horizontal run-division law** (that law would stretch one knob
|
||
over 186 px); the reserved slot **draws nothing** (blank reads as breathing room, a dashed
|
||
placeholder reads as unfinished); the meter is **one rect spanning both baselines**, not two
|
||
per-row meters.
|
||
- **The meter draws W1-T2's published state**, with the ballistics run on the UI timer.
|
||
**Bar count follows the same `LaneSplit` decision `waveformSurface` already folds** (channel
|
||
mode ∧ source channel count) — one wide bar when the waveform draws one lane, two skinnier
|
||
bars when it draws two. Not a second rule: a mono source in stereo mode is dual-mono, and
|
||
two identical bars would be a lie.
|
||
- **Meter appearance:** bar in `accent/primary`; peak-hold tick 2 px in `text/primary`; clip
|
||
cap in `warn`, latched, click-to-clear; scale linear in dB over −60…+6 with ticks every
|
||
6 dB and numerals at 0/−12/−24/−36/−48/−60, the 0 dB tick heavier. **No green/yellow/red
|
||
segmentation** — `warn` stays reserved for clip states.
|
||
|
||
**Acceptance criteria.**
|
||
- At the floor width the deck lays out in **exactly two rows plus the spanning MASTER**,
|
||
**by construction** — asserted against the group inventory, not observed as a wrap outcome.
|
||
- Every group's width matches the §1.2 table exactly, **in both Gate and Trigger** (row 2's
|
||
natural width is mode-stable at 876 because the reserve slots hold FILTER ENV and AMP at
|
||
312 in both modes — assert it).
|
||
- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line
|
||
is exact (both edges at x = 636) and row 2's two gutters are equal.
|
||
- **Row 1's natural width is 980 and fits the 1020 block** — the fit Γ-W1-T4 could not yet
|
||
assert, closed here by the `Band|Notch` move.
|
||
- **`kEditorMinWidth` is still 1190 and the floor is still ≤ 1280 × 720** — unchanged by this
|
||
track, verified against Γ-W1-T4's derived test rather than a second copy of it.
|
||
- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the
|
||
interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the
|
||
row predicate says so, not because a wrap landed there. Assert against the group inventory.
|
||
- MASTER's gain knob shares a knob baseline with FILTER's knobs; its reserved slot shares one
|
||
with AMP ENVELOPE's.
|
||
- The meter reads correctly in mono and stereo, the peak-hold tick holds 1.5 s, the clip cap
|
||
latches and clears, and the GR bubble lights only while the limiter reduces gain.
|
||
- **With the limiter engaged the clip cap never latches** on material the limiter is catching;
|
||
if it does, that is a defect report against W1-T2, not a user error.
|
||
- `knob_deck`'s and `sample_bands`' tests are updated to the new law, and the invalidated
|
||
notes in `knob_deck.h` (the fourteen-pixel headroom figure; the cells-and-floor pairing) are
|
||
**re-derived, not deleted** — spec §7.1, §7.4.
|
||
|
||
**Open questions.**
|
||
- **[propose at review]** Whether the greedy whole-group wrap survives at all as a sub-floor
|
||
degrade, or is replaced outright by explicit row assignment. What is **not** optional: at
|
||
and above the floor width the layout is the specified arrangement, reached by construction.
|
||
`DeckLayout::rowCount`/`::height` change meaning either way (spec §7.3).
|
||
- **No [Daniel] questions.** Forks Γ-F5 (**one cell**) and Γ-F1 (**680 stays**) are both
|
||
ruled; they are stated in Behavior above, not carried here as options.
|
||
- **[verify]** `deck_groups.cpp`'s `kEnvModeSegW = 23` ceiling rises to **47** once PITCH ENV
|
||
is on row 2 (AMP binds at 55). No change is required; the comment stating the old ceiling
|
||
stops being true and must be corrected (spec §7.2).
|
||
|
||
---
|
||
|
||
## 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`. The first such track in
|
||
this plan; see Θ-W7-T1 below for the second.
|
||
- **Θ-W7-T1 — `arc-and-spline-aa`** (`pth-w7-t1-arc-and-spline-aa`). Opened after
|
||
Θ-W6-T1 shipped, when Daniel found two rendering defects by eye in the editor — not
|
||
from `TODO-1.0.md`, and not a track this plan originally scoped. The second such track
|
||
in this plan today; if others appear, they belong on this list rather than in the
|
||
table.
|
||
- **All of Phase Γ** (`pg-*`). Eight tracks across three waves, from a direct interview with
|
||
Daniel (2026-08-01), not from `TODO-1.0.md`. Listed here as a block rather than per track,
|
||
because the whole phase is outside the source doc; the product reasoning lives in
|
||
`docs/product/instrument-control-surface.md` and the automation scoping it defers in
|
||
`docs/product/parameter-automation.md`. Γ-W3-T1 additionally **discharges** the
|
||
`docs/TODO.md` deck-rework entry, whose original "one row of taller decks with
|
||
within-deck stacking" shape Daniel explicitly superseded.
|
||
|
||
### 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
|
||
W7 Arc-and-spline antialiasing fix
|
||
T1 arc-and-spline-aa ....................... (not one of the seventeen)
|
||
|
||
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 Θ, and Phase Γ before it]
|
||
T1 resample-bake-chain ..................... 15 (chain)
|
||
W3 The capture-signal popup
|
||
T1 capture-signal-popup .................... 15 (popup)
|
||
|
||
Phase Γ — The instrument's control surface (none of the seventeen; runs before Ξ-W2)
|
||
W1 Foundations [5 tracks, disjoint by surface]
|
||
T1 knob-interaction-law ....... modifiers + ms/semitone tapers + reset bypass
|
||
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [payload v14]
|
||
T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
|
||
T4 editor-floor-and-row-law ... floor 1190x680 + budget constants + row predicate
|
||
T5 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
||
W2 New controls, and the overlay's marks [2 tracks]
|
||
T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [payload v15]
|
||
T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard
|
||
W3 The reflow [1 track]
|
||
T1 deck-reflow ................ two rows + double-height MASTER, by construction
|
||
|
||
Resequenced 2026-08-01: the reflow split canvas (W1-T4) from arrangement (W3-T1), and
|
||
preserve-time-stretch moved W4 -> W1-T5, which retires Rate's interim stand-in.
|
||
Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5) and editor_session.cpp
|
||
(W2-T1 | W2-T2) — both textual adjacency, not semantic contention.
|
||
```
|