docs: collapse Phase Gamma Wave 1 to its landed record, and correct T7's superseded status

This commit is contained in:
2026-08-02 06:31:27 -04:00
parent 35ab59d7f9
commit 56bf26d8b6
+75 -577
View File
@@ -843,604 +843,98 @@ exact interim layout; do not "fix" it in a track that does not own it.
### Γ-W1 — Foundations
**Depends on:** nothing in this phase. **Six tracks, disjoint by surface** — re-verified
against this membership rather than carried over from the four-wave shape. The phase's track
numbering runs to T7; **T6 landed within this wave but has no entry in this document**:
**Depends on:** nothing in this phase.
| Track | Owns |
|---|---|
| **T1** `knob-interaction-law` | a **new pure taper module** under `core/instrument/ui/`, `ui/deck_values`, `ui/envelope_overlay` + `ui/envelope_edit` (the AHDSR schematic axis and its drag inverse), `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, `editor_controls.cpp`'s `envClampBounds` only, 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` (**the wave's payload rung**) |
| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **new pure** tessellation module |
| **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 |
| **T7** `psola-preserve` | a **new pure module** `engine/period_detect` (two-pass YIN, with its own `period_detect_tests` target), `pitch_shift`'s jump geometry, the load-time hook in `map/sample_map`'s `buildSampleData`, `voice`'s note-on |
**Two shared files 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. **And, newly, `src/core/instrument/ui/CMakeLists.txt`**
T1 declares the taper module and its test target, T3 declares the tessellation module and
its. All four are append-only additions in separate blocks — **textual merge adjacency, not
semantic contention.** Whichever lands second rebases.
**Three near-misses that are avoided by construction, and must stay avoided.**
(a) **T3's tessellation helper lands in a NEW pure module — explicitly NOT
`ui/envelope_overlay`, which T1 now owns**, and not in `editor_internal.h`, which T1 is also
editing. The prior wording offered `envelope_overlay` as an option; Ruling 2 removed it,
because T1's schematic-axis work rewrites that module's whole time→x map. This still
satisfies the phase's geometry-stays-pure criterion, so it costs nothing.
(b) **T1 and T3 are disjoint by file but coupled by data, and the coupling has a stated
resolution.** T1 owns where an AHDSR's vertices *land*; T3 owns the stroke *between*
vertices. T3's tessellation is over φ across a segment's pixel span, so the tapered axis
changes nothing about the curve it draws — **but T3's tests must assert against the returned
vertices, not against absolute pixel literals**, or they break when T1 lands. Whichever
track lands second rebases; expressing T3's assertions relatively makes that rebase free.
(c) **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.
**Two consumption boundaries worth stating, because they look like collisions and are not.**
T1 **consumes** `engine/master_gain`'s dB taper for its whole-dB snap and does not edit it;
T2 does not touch it either. And T2's payload rung is the wave's only format change —
T1's taper and ceiling changes are persistence-neutral by construction (the payload stores
raw engine doubles).
**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.
**All seven tracks have landed** — Γ-W1-T1 (`knob-interaction-law`), Γ-W1-T2
(`master-bus-audio`), Γ-W1-T3 (`contour-trace-curves`), Γ-W1-T4 (`editor-floor-and-row-law`),
Γ-W1-T5 (`preserve-time-stretch`), Γ-W1-T6 (`exhaustive-switch gate on pure libraries`), and
Γ-W1-T7 (`psola-preserve`) — see `docs/COMPLETED.md` for the full narrative of each. **T6 has
no subsection below**, matching this plan's original choice not to give it one; its record in
`docs/COMPLETED.md` is reconstructed from the CMake change and its enforcement call site
rather than from a spec section here.
#### Γ-W1-T1 — `knob-interaction-law`
**Goal.** One consistent, unit-category-driven interaction and taper rule across every
variable control, **over a stage-time range raised 2 s → 10 s**, landed **before** any new
control is added so the new ones are authored into it rather than retro-fitted — and before
any parameter is declared, so the law is what the host is handed rather than something the
host has to be reconciled with later.
**Spec:** `docs/product/instrument-control-surface.md` §4, **§4.3.1 (the 10 s ceiling and the
overlay-legibility design — new, read it before scoping this track)**, and
`docs/product/parameter-automation.md` §8 (the one-way-door sweep this track discharges).
**Surface boundary — owns:** a **new pure taper module** under `core/instrument/ui/` (the
ms/semitone/exponent maps, extracted so they have one home),
`core/instrument/ui/deck_values` (the bindings, the snap-unit table, `resetDeckParam`),
`core/instrument/ui/envelope_overlay` (the ceiling constant **and** the AHDSR schematic
axis) and `core/instrument/ui/envelope_edit` (its drag inverse),
`core/instrument/ui/param_slider` (the drag law), `shell/instrument/editor_input_*`
(modifier read + re-anchor), `shell/instrument/editor_controls.cpp`'s `envClampBounds`
**only** (it reads `kEnvTimeMaxSeconds`), and the modifier-reading helper the three input
paths share. **Does not own** any deck descriptor, any parameter, the waveform painter, or
`engine/master_gain` (consumed, not edited).
**Why this track does NOT split, asked and answered.** Ruling 2 makes it materially bigger —
tapers, modifiers, re-anchor, reset bypass, the ceiling, and the overlay's schematic scale.
Two splits were considered and both are **serial, not parallel**, so neither buys any
concurrency: an *interaction* half (modifiers, snap, re-anchor) needs the *domain* half's
taper and snap-unit table to exist first; and a standalone *overlay-axis* track needs the
taper module and the final ceiling before it can define a stage's slot width. Splitting
would therefore cost a wave and gain nothing, while putting the single most
identity-critical function in the phase across a wave boundary — the same function the host
will normalize against three waves later. **The seam that matters is internal and is a
deliverable: the taper is extracted into its own pure module**, which is what makes "the
taper IS the host-facing normalization" structurally true rather than a comment. The
~600-line ceiling is a per-file bar, and the extraction is what keeps every file under it.
**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.120.20 of
travel and 100 ms within 0.420.52**.
- **The stage-time ceiling moves 2.0 s → 10.0 s** (Daniel, reversing Γ-F3):
`kGateStageMaxSeconds` (`envelope_overlay.h:85`) and, through it, `kEnvTimeMaxSeconds`
(`deck_values.h:22`). **The two move together or not at all**`deck_values.h` reads the
overlay's constant rather than restating it precisely so they cannot drift
(`deck_values.h:19-22`). The taper's landmarks above are fit against the **new** ceiling,
which is why the ceiling cannot be a follow-up: fitting the taper twice is the only other
way to get there.
- **`resetDeckParam`'s bypass becomes MANDATORY rather than merely required-anyway.**
`deck_values.h:42-46` records that exact default recovery depends on the ceiling being a
power of two; **2.0 is, 10.0 is not**, and the log taper compounds it. Nothing here may be
"simplified" back into a norm round-trip under any circumstance.
- **NEW, and the sharpest requirement in the track: every default must have an EXACT
normalized preimage under its taper.** `ParameterInfo::defaultNormalizedValue` (Γ-W4-T1)
is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)` — and **the
host has no `resetDeckParam` bypass to use**. The bypass fixes the editor's reset and
cannot fix the host's; only exactness in the map itself makes the two land on the same
value. This binds the taper's *shape*, so it belongs here and cannot be handed forward.
Master gain's unity (≈ 0.714 norm) is the case where a hair off is audible.
- **The AHDSR overlay's schematic axis becomes the taper — the ceiling's real cost, and it
is design work, not a constant change.** Each of the four timed stages gets an equal slot
and today maps seconds across it linearly (`gatePxPerSecond`,
`envelope_overlay.cpp:33-34`). At 2 s a 30 ms attack is 1.5 % of its stage's domain; **at
10 s it is 0.3 %, under a pixel at the floor width.** The fix: a stage's slot width becomes
`slotPx × taperNorm(seconds)` instead of `slotPx × seconds / ceiling`, so a node's position
within its slot **is** its knob's needle position. Legibility becomes ceiling-independent by
construction; the one-model invariant gets stronger rather than strained; and **the drawn
curve is unaffected**, because the taper decides only where a stage's end node lands while φ
still runs linearly across the stage's pixel span — so Γ-W1-T3's φ^p trace composes with it
rather than fighting it. **The AHD policy is untouched**: an AHD maps 1:1 onto the
waveform's own PCM-aligned time axis and stays linear in seconds. Two alternatives
(content-fit auto-scale; a minimum drawn stage width) were considered and rejected — spec
§4.3.1 names why, and neither is to be reintroduced as a "simplification."
- **`envelope_edit`'s drag inverse must remain the EXACT inverse of the draw.** Both read the
same taper module; a node dragged to a pixel and the knob's value at that pixel are one
number, not two that agree.
- **The taper is EXTRACTED into its own pure module**, with its own `<module>_tests` target,
because it now has three consumers in two different dependency layers: `deck_values` (which
sits above `envelope_overlay`), `envelope_overlay`/`envelope_edit` (which sit below it),
and — from Γ-W4-T1 — the host. Leaving it inside `deck_values` would force an inverted
include edge. **Do not solve that by copying the map.**
- **Semitone knobs become log2/centre-expanded.** Symmetric, exactly 0 at centre, exactly
±`kPitchDepthMaxSemis` at the ends, monotone; **±7 st reached at 5058 % 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 the **taper module's** own tests, and
hold at the **10 s** ceiling — the fit is against the new ceiling, not the old one.
- **Every default round-trips exactly through `norm → value`**, asserted per unit category
against a default-constructed `PlaySeconds` and against `master_gain`'s unity. This is the
criterion Γ-W4-T1 will declare `defaultNormalizedValue` from; it fails here, not there.
- **A stage time of several seconds is reachable by hand with no loss of resolution below
100 ms**, and `kEnvTimeMaxSeconds == kGateStageMaxSeconds` is asserted, not assumed.
- **A project saved at the 2 s ceiling reloads with identical stored seconds and identical
audio** — the ceiling change is persistence-neutral for the same reason the taper is.
- **The AHDSR overlay reads legibly at both ends of the new range**: a default 3 ms attack is
a visible, grabbable node at the floor width, and a 10 s decay still lands its end node at
its slot's edge. Assert the node separation, then judge the result by eye in the DAW.
- **The overlay's drag inverse is the exact inverse of its draw** at the tapered axis —
`nodeAtPoint` / `resolveNodeDrag` and `buildEnvelopePolyline` round-trip.
- **The AHD 1:1 policy is unchanged**, asserted: a sustain-less envelope's x-axis stays
wall-clock over the waveform.
- **The taper module is pure, CTest-covered, and is the ONLY definition of each map** — a
grep finds no second copy in `deck_values`, `envelope_overlay`, or the shell.
- **The filter's four `*Norm` controls are untouched by the taper pass** — cutoff, Q, morph
and drive are already wire-frozen in payload v9; a regression baseline proves their audio
is unchanged.
- One shared modifier-read helper serves all drag surfaces; no second modifier grammar exists.
**Open questions.**
- **No [Daniel] questions.** Fork **Γ-F3 is REVERSED**: the ceiling moves to **10.0 s, in this
track.** Daniel's *"a horrifically long decay with tight exp"* is the case it serves, and
the `docs/TODO.md` entry that carried it is discharged rather than deferred again. **The
reversal's cause is Ruling 1** — a range endpoint is host-facing normalization, free to
move now and permanently expensive after Γ-W4-T1. Both of the prerequisites the deferred
entry named are in this track anyway: 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). The engineer should know the
reset bypass is now doing triple duty and must not be "simplified" back into a norm
round-trip under any circumstance.
- **[propose at review]** the exact shape of the taper, subject to the landmark bounds **and**
the exact-default-preimage requirement. Those two together are tighter than either alone,
and the second is easy to satisfy by accident and easy to lose in a refactor — **assert it,
do not observe it.**
- **[propose at review]** whether the tapered schematic axis wants a visible tick or
gradation cue, now that it is no longer linear in time. The plan's lean is **no** — the ms
labels carry the number and the editor's no-decoration policy stands — but a reader who
finds the axis illegible in the DAW should say so rather than silently adding one.
**Landed** — see `docs/COMPLETED.md` for the full narrative. One consistent interaction and
taper law across every variable control, landed before Rate/Pitch or any VST3 parameter
existed so both are authored into it rather than retrofitted. The taper is extracted into
its own pure module, `core/instrument/ui/param_taper` — the one home three consumers read
(the knob's needle, the AHDSR overlay's schematic axis and its drag inverse, and — from a
later wave — the VST3 host's `toPlain`/`toNormalized`). Shift snaps to whole units in the
control's displayed category; Ctrl scales the drag by 0.05; Shift+Ctrl resolves to Shift; a
mid-drag modifier transition re-anchors value and cursor position. **The stage-time ceiling
moves 2.0 s → 10.0 s** (`kGateStageMaxSeconds`/`kEnvTimeMaxSeconds`, moved together so they
cannot drift), reversing Γ-F3 on Daniel's later ruling, and every default now has an exact
normalized preimage under its own taper — the requirement Γ-W4-T1's `defaultNormalizedValue`
depends on, since a host's reset-to-default has no `resetDeckParam` bypass to fall back on.
The filter's four `*Norm` controls stay untouched (wire-frozen in payload v9); the change is
persistence-neutral throughout.
#### Γ-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.23.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 **the phase's FIRST
params-payload rung** (the limiter enable flag). **Does not own** MASTER's deck geometry or
any drawing — that is Γ-W3-T1. **This track spends that rung: `kParamsPayloadVersion == 15`**
(`map/component_state_io.h:170`), confirmed on this branch as of 2026-08-01 — the full ladder
(spent / next-free / reserved) is stated in the phase summary's resequencing note near the end
of this phase's ASCII block.
**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.
- **The `ComponentState` payload rung** appends the limiter flag as a strict suffix on the
existing discipline; the preceding version's 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.
**Landed** — see `docs/COMPLETED.md` for the full narrative. The master bus: a bypassable
true-peak limiter (baked 0.3 dBTP ceiling, default off, no makeup gain, stereo-linked,
sidechain-only oversampled detection), the meter's audio and publication half, and the
plugin's first latency report — `getLatencySamples()` returns 0 with the limiter off and the
lookahead in samples with it on, driving `restartComponent(kLatencyChanged)` on toggle. New
pure modules `core/instrument/engine/limiter` and `engine/meter_ballistics`. **Spent the
phase's first payload rung: `kParamsPayloadVersion` reaches 15**, appending the limiter
enable flag as a strict suffix.
#### Γ-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 a **new pure tessellation module** for it. **Does not own** the loop/crossfade
marks (Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace.
**The helper's home is now constrained, not a choice.** `ui/envelope_overlay` was previously
offered as a candidate home for the tessellation helper; **Γ-W1-T1 now owns that module**
(the AHDSR schematic axis, per Ruling 2), so the helper lands in a **new** pure module under
`core/instrument/ui/`. T1 also owns where an AHDSR's vertices land — this track owns only the
stroke *between* vertices, and tessellates over φ across a segment's pixel span, so the
tapered axis changes nothing about the curve drawn. **Express this track's assertions against
the returned vertices, not against absolute pixel literals**, and the rebase onto T1 is free.
**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.
**Landed** — see `docs/COMPLETED.md` for the full narrative. Staged envelope segments now
draw as the curve their exponent defines, closing the defect where the mid-segment knot
floated off its own trace. A new pure module, `curve_tessellate`, draws every sloped stage
through the same curve the audio's evaluators use, so the drawn stage and the sound it makes
cannot diverge. All three envelopes, both play modes, every sloped stage, share the one fix;
audio is unchanged.
#### Γ-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*.
**Landed** — see `docs/COMPLETED.md` for the full narrative. Commits the editor's canvas
ahead of the rest of the phase's UI work: `kEditorMinWidth` moves 980 → 1190
(`kEditorMinHeight` stays 680, Γ-F1), derived from three budget constants — the row block
(1020), MASTER's reserved width (142), and the 1280 ceiling (`kEditorCeilingWidth`, relocated
into `sample_bands.h`) — leaving 90 px of headroom. Row membership becomes a property of the
group id via an exhaustive `deckRowFor(DeckGroupId)` switch (Sound / Contour / Spanning),
consumed by no one yet — **that consumption, and the fit inside the 1020 block, is
Γ-W3-T1's** to assert. No drawing code, descriptor, parameter, or audio changed in this
track.
#### Γ-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
75133 % 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.**
**Landed** — see `docs/COMPLETED.md` for the full narrative. A real pitch-preserving
time-stretcher for Preserve mode, moved up from a later wave (Daniel, 2026-08-01) so Rate
ships onto a finished engine instead of a disposable stand-in — the interim
resample-and-cancel path that had been planned for Γ-W2-T1 was not built at all. New
header-only pure module `time_stretch` alongside `pitch_shift`'s existing shift-ratio
control; rate 1.0 is exactly one source frame per output frame with no residue, keeping the
unity-ratio Preserve read bit-identical to the pre-stretch engine. No new third-party
dependency, no allocation/lock in `process()`, no per-sample dispatch.
---
#### Γ-W1-T7 — `psola-preserve`
**Goal.** Preserve's splices become pitch-synchronous: the source's fundamental period is
detected offline at sample load, cached, and the splice jump becomes a whole number of that
period, so an aligned landing point exists by construction rather than being searched for.
**Landed** — see `docs/COMPLETED.md` for the full narrative, including the corrected closure
status below. Preserve's splices become pitch-synchronous: a new pure module,
`core/instrument/engine/period_detect` (two-pass YIN), estimates the source's fundamental
period once at load; `pitch_shift`'s splice jump becomes the multiple of that period nearest
the fixed window, so an aligned landing point exists by construction. Detection runs off the
audio thread by link graph — `sampler_core` does not link `period_detect` — and an unknown
period restores the fixed-window geometry byte for byte. A period is derived from the audio
at load, so it is cache, not state: no `ComponentState` field, no payload rung. It gates the
Rate control (Γ-W2-T1) on the plan's own stated principle that Rate must not ship before its
Preserve engine.
**Origin — Daniel's ruling, 2026-08-01, after Γ-W1-T5 landed.** T5's measurement pass showed
the shipped fixed 50 ms OLA window with a ±window/4 correlation search could only reach
landings spanning `[¾w, 1¼w]` — a 1.5:1 span that cannot contain a whole number of periods for
low material. A 30 Hz tone (1470 frames at 44.1 kHz) had no phase-aligned landing at all. A
second, distinct failure mode: splices recur every `window/|rate shift|` frames and fail
when that interval is shorter than the output period. Three cheaper options were offered and
declined: widening `maxLag` to `window/2` (fixes the geometry, not the cadence); sizing the
window from the note's known fundamental at note-on (fixes both, but trusts root-note
tagging); enlarging the window to ~200 ms (fixes both, smears transients — the OLA crossfade
is `window/4`).
**Why this is a new track and not an amendment to Γ-W1-T5.** It touches the sample-load /
analysis path, which is outside T5's stated surface boundary (`pitch_shift`, the stretcher
module, `voice`'s Preserve read path).
**Why PSOLA is affordable here.** ReaSampler is a sampler, so the source is fixed and fully
known at load. Detection runs once during the reload that already happens, entirely off the
audio thread — the usual real-time objection to PSOLA does not apply.
**Surface boundary — owns:** a new pure module `core/instrument/engine/period_detect`
(two-pass YIN, with its own `period_detect_tests` target), `pitch_shift`'s jump geometry, the
load-time hook in `map/sample_map`'s `buildSampleData`, and `voice`'s note-on. Adds no
`ComponentState` field and takes **no rung of the payload ladder** — a detected period is
derived from the audio, so it is cache, not state.
**Behavior and constraints.**
- **Enforcement is by link graph, not by convention.** The detector runs off the audio thread
because `sampler_core` does not link `period_detect` — no translation unit on the render path
can name `detectPeriod`. An unknown period restores the fixed-window geometry byte for byte.
- **Dependency it discharges:** it gates the Rate control (Γ-W2-T1) on the plan's own stated
principle that Rate must not ship before its Preserve engine.
**Status — landed, with open findings.**
- The **geometry** failure mode is closed and asserted.
- The **cadence** failure mode is **not** closed. It was re-characterized rather than fixed:
the previously-headlined 721 % artifact-energy readings turned out to be ~95 % the
measurement's own spectral leakage, leaving a real excess of 0.230.51 %. The track asserted
no-regression there rather than claiming an improvement.
**Open questions.**
- **Unresolved review findings, not a design fork.** A later review of the follow-up fold
returned unresolved Major findings; remediation has not yet been dispatched. The
findings themselves live in the review, not here.
**Both failure modes this track set out to close are now closed.** The **geometry** failure
mode (no phase-aligned landing existing inside the search window for low material) closed at
the original merge. The **cadence** failure mode — splices recurring faster than the output
period — was left open at that point, with unresolved review findings from a later review of
a follow-up fold; three remediation commits have since landed and a re-review confirmed the
earlier findings closed. The closing measurement (one-machine, Debug-build) is recorded in
`docs/COMPLETED.md`, not restated here.
---
@@ -4354,13 +3848,17 @@ Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W
T1 capture-signal-popup .................... 15 (popup abandoned; window derives)
Phase Γ — The instrument's control surface (none of the seventeen; ends with VST3 params)
W1 Foundations [5 tracks, disjoint by surface]
W1 Foundations — landed [seven tracks, disjoint by surface]
T1 knob-interaction-law ....... modifiers + ONE taper module + reset bypass
+ 10 s ceiling + AHDSR schematic axis [Ruling 2]
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1]
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]
T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on
pure libraries [no PLAN entry — see COMPLETED.md]
T7 psola-preserve ............. PSOLA-aligned splice jump; period_detect
[cadence closure — see COMPLETED.md]
W2 New controls, and the overlay's marks [2 tracks]
T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [rung 2]
T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard