docs: retire Phase Gamma from PLAN.md; repair stale line/symbol citations across PLAN, the Lambda build-toolchain audit note, and root CLAUDE.md

This commit is contained in:
2026-08-02 22:31:54 -04:00
parent 2ee984ed16
commit 27712b7573
3 changed files with 26 additions and 591 deletions
+3 -3
View File
@@ -122,9 +122,9 @@ The top-level split is by the pure/shell discipline: `core/` never includes REAP
types; `shell/` is where those host types are actually touched — the discriminator is "may this
file touch a host type, REAPER *or* VST3 SDK." Subsystem directories sit beneath `core/` (see the
table above); `core/instrument/` further subdivides into `bake/` / `engine/` / `map/` / `note/` /
`param/` / `ui/`. Namespaces
mirror directories — `reasampler::<subsystem>` for `core/`, house style for `shell/`. `app/` holds
`main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and dispatch.
`param/` / `ui/`. Namespaces mirror directories — `reasampler::<subsystem>` for `core/`, house
style for `shell/`. `app/` holds `main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and
dispatch.
For the module list within any one directory — what lives there, its invariants — open that
directory's own `CLAUDE.md` rather than looking here.
+20 -585
View File
@@ -190,9 +190,8 @@ in Γ-W4-T1 and changes no wave boundary.
## Phase-wide acceptance criteria
These bind every track in both phases and are stated once here rather than repeated
per track. **Phase Γ adds a set of its own**, stated in its phase header. **Phase Λ adds
a set of its own**, stated in its phase header.
These bind every track in this plan and are stated once here rather than repeated
per track. **Phase Λ adds a set of its own**, stated in its phase header.
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
@@ -241,570 +240,6 @@ touches them. The instrument adds a sixth surface that binds every Θ track:
---
## 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 **over a stage-time range raised 2 s → 10 s**, a fix for staged
contour traces drawing straight, a re-approached loop/crossfade marker UX under an explicit
chrome-row loop enable, **the Phase Ξ bake's reset list corrected**, and — as the phase's last
track — **the instrument's first VST3 automatable parameters, reported to the host under a
frozen id contract.**
**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`**, and the parameter
system's is in **`docs/product/parameter-automation.md` §§610**. 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.
**All four waves have landed — Phase Γ is complete.** W1 through W4 each carry their own
landed note below; see `docs/COMPLETED.md` for every track's full narrative.
**Fork state — SEVEN ruled, ONE OF THEM LATER REVERSED, NONE OPEN.** 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 — RULED, THEN REVERSED THE SAME DAY.** First ruled *"the ceiling stays 2.0 s in this
phase"*; then Daniel: ***"extend the stage lengths to 10s."*** `kEnvTimeMaxSeconds` /
`kGateStageMaxSeconds` move **2.0 → 10.0 in Γ-W1-T1**, and the `docs/TODO.md` entry that
carried the ambition is discharged rather than deferred. **The reversal's cause is Ruling 1**
— parameters now ship in-phase, so the ceiling is a one-way door that must be walked through
before them, not after. Spec §4.3.1.
- **Γ-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. (**Moved afterwards:**
Γ-W3-T1 widened the row block, so §1.6's ledger is now 82 px — the ruling and its
purchasing power are unchanged, only the number.)
- **Γ-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.
- **Γ-F7 — RULED: SIGNAL FLOW.** Daniel, 2026-08-01: *"signal flow order."* The VST3 parameter
order — both the frozen id numbering and the `getParameterInfo` presentation index — is
**PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE → MASTER**, the
deck's own `sampleDeckGroups` rule, with each group's cells in the semantic order the id
table freezes. The editor's visual rows after the reflow were the rejected alternative.
**The reason, because a future reader will ask why the id order does not match the screen:**
the editor's layout has already moved twice (Θ-W6-T1 grew the floor 840 → 980; Γ-W3-T1 takes
it to 1198 and re-rows every group) and within-row order is settled by width fitting, not by
meaning — so **binding a permanently-frozen id order to a demonstrably mobile layout
guarantees the two drift apart**, after which the order is neither logical nor matching.
Signal flow is the axis that does not move. Full argument and the accepted residual cost:
`docs/product/parameter-automation.md` §6.4; **the resulting 44-id table is stated at §6.2.**
**Ruling 1 (Daniel, 2026-08-01) — VST3 parameter reporting ships in this phase.** Verbatim
intent: *"correct the phase gamma plan to account for complying with the VST3 standard for
parameter reporting… by the end of gamma we have the automatable params reported. Make the
parameter order logical."* `docs/product/parameter-automation.md` was written as scoping and
has been **promoted in place**: §§15 are the original analysis, **§§610 are the
specification** Γ-W4-T1 is built from. Four things it decides that the scoping pass left
open: the blob stays authoritative and parameters are a third surface onto the one model
(§6.1); the id space is an independent, hand-assigned, FOREVER-FROZEN table, **now stated in
full as 44 numbered rows in signal-flow order** (§6.2, §6.3); the exposed list is **derived
from the three-state commit predicate**, never hand-maintained (§7); and, under Ruling 3,
every parameter's **plain unit, range and display precision** (§6.7). Today the plugin has
**zero** parameters — `ReaSamplerProcessor::initialize`
(`reasampler_processor.cpp:56-73`) never populates `SingleComponentEffect::parameters`, so
`getParameterCount()` returns the SDK default 0.
**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 made the cycle expensive **here** was entirely
our own doing: `ReaSamplerProcessor::setActive(true)` called `reloadInstrument()` — a bridge
read plus a full WAV re-decode — where a typical plugin's `setActive` only allocates and frees
buffers. **The cost was therefore ours to reduce, and the reduction was decoupling reload from
activation — not abandoning dynamic latency.** **Landed in Γ-W3**: the activate branch
(`reasampler_processor.cpp:86-132`) now resumes the voice state around a parked `SampleData`
and reloads only when there is nothing to resume from or a bank refresh moved what the park was
decoded from; the deactivate branch parks the PCM and frees everything else. Narrative and
consequences are in `docs/COMPLETED.md`.
**Sequencing against Phase Ξ — the ordering claim is RETIRED and replaced by an owned
correction.** This plan previously asserted that Γ must run before Ξ-W2 and called it *"a
correctness point, not a preference."* **Ξ-W2-T1 ran first.** That was not a decision anyone
took — the track was live before this phase existed (Daniel: *"xi was started before I spun
you up, we'll have to correct phase xi inside gamma. wasn't a choice."*). So:
1. **The bake's reset list is incomplete as shipped, and Γ-W3-T2 amends it.** Rate, pitch
offset and the limiter flag are all **reset** under Ξ-W2's own ratified rule (spec §3.4),
so no Daniel decision is owed — only the edit, and it must be made **against what Ξ-W2-T1
actually shipped rather than against what this plan predicted it would ship.**
2. **Ruling 1 adds a second correction of the same shape, and it lands one wave later.**
Exposing the reset-class values as VST3 parameters means the bake's reset must notify the
host, and a host automation lane on a reset-class parameter re-imposes its curve onto
already-baked audio. Both are Γ-W4-T1's acceptance criteria — that track creates the
condition, so it carries it (`docs/product/parameter-automation.md` §9).
3. **The payload-ladder half of the old claim needs re-checking, not restating** — see the
ladder block below, which now states rungs **relatively** rather than by number.
**The organizing constraint.** Six surfaces are single-writer and dictate the wave shape:
`ui/deck_values.cpp` **and the taper module extracted from it** (the taper law and the new
ceiling, then the two new controls, then the host normalization — three tracks, three waves),
`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), `shell/instrument/reasampler_processor` (the limiter chain and latency, then the
parameter surface), 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), three changes.** The prior four-wave shape put the reflow
at W3 and the Preserve stretcher at W4; both moved. **Ruling 1 then added a fourth wave**
see "The wave shape after Ruling 1" below.
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 1198 × 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 wave shape after Ruling 1 — three waves become four.** The parameter system cannot be
a track inside any existing wave, and the reason is a chain of hard prerequisites, not
caution:
- **after W1-T1**, because the taper and the 10 s ceiling *are* the host-facing
normalization, and Γ-W1-T1 is also what extracts them into the one module the host will
read through;
- **after W1-T2 and W2-T1**, because every control that could be a parameter must exist
before the list is declared — the list is derived from the control inventory, and an
inventory that is still growing produces a list that has to be re-frozen;
- **after W2-T1 specifically**, because `deckParamCommit` becoming three-valued is the
*prerequisite* of the classification, not an incidental of it: the exposed set is exactly
`Live NoteOnLatched`;
- **after W3-T1**, because MASTER's inventory (the limiter toggle, the GR bubble, the
reserved cell) is the last change to what controls exist at all;
- **after W3-T2**, so the bake's reset list is already complete when Γ-W4-T1 adds the
host-notification obligation over it — one amendment instead of an amendment to an
amendment.
The result is a single-track **Γ-W4**, which is the right shape for it anyway: the storage
decision governs every part of the work, exactly as Ξ-W2-T1's crossing decision governs its
chain. **And it satisfies Daniel's own framing literally** — *"by the end of gamma we have
the automatable params reported."*
**The params-payload ladder — re-checked, and now stated RELATIVELY.** The old block named
v14 and v15 as absolutes. **That is no longer safe to assume**, because Ξ ran ahead of its
sequencing and this plan is not the record of what Ξ-W2-T1 actually took. On `dev` today
`kParamsPayloadVersion` is **14** (`map/component_state_io.h:163`) and Ξ-W2-T1 was specced to
take no rung — but the plan's prediction is not evidence. So:
> **Γ owns the next three rungs above whatever `dev` carries when Γ-W1-T2 dispatches, and
> that number is READ, not assumed.** In order: **the first rung to W1-T2** (the limiter
> enable flag), **the second to W2-T1** (rate + pitch offset), **the third RESERVED for
> W4-T1** — spent only if the storage-architecture verification forces a persisted field,
> which the specification says it will not (`docs/product/parameter-automation.md` §6.1,
> §10). If unspent, that rung falls through to the next phase unclaimed.
>
> **On `dev` as of 2026-08-01 that resolves to v15 / v16 / v17-reserved.** Ξ-W3-T1 landed and
> consumed a rung (v14, the bake Hold division) — not Ξ-W2-T1, which took none as specced — so
> every number shifted by one and **nothing else about the ownership changes** — which is the
> whole point of stating it relatively.
**Every other track in the phase owns no rung**: W1-T1 changes no persisted field (the payload
stores raw engine doubles, so both the taper and the new ceiling are persistence-neutral),
W1-T3, W1-T4 and W1-T5 add no field, W2-T2's loop enable maps onto the already-persisted
`SampleLoop::hasLoop`, W3-T1 is layout only, and W3-T2 changes a reset list, not a format.
**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 **and raising the stage-time ceiling** (both Γ-W1-T1) change
needle angles only — the payload stores raw engine doubles, so saved values reload
bit-identical, and a 3 s stage saved at the old ceiling is simply unreachable-by-hand
rather than altered.
- **`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. **From Γ-W4-T1 it is also a frozen host normalization** — one more
reason, not a new rule.
- **The taper has exactly ONE home and three consumers, and from Γ-W4-T1 the taper IS the
host's `toPlain`/`toNormalized`.** Γ-W1-T1 extracts it into a pure module; the knob's needle
(`deck_values`), the AHDSR overlay's schematic axis (`envelope_overlay` + `envelope_edit`),
and the host's `normalizedParamToPlain` / `plainParamToNormalized` (Γ-W4-T1) all call the
same function. **Three functions that agree today is a defect, not an implementation
choice** — the failure it prevents is a host automation lane that means one value and a
needle that draws another. Under Ruling 3 this stops being an analogy: `toNormalized` is
not *like* the taper, it *is* the taper (`docs/product/parameter-automation.md` §6.7.3).
- **ONE formatter per unit category, and the editor and the host are both its callers.** The
formatter is pure and returns the **digits** of a plain value in that category's single
`units` string — no embedded unit, no magnitude-switched unit, no width-conditional
abbreviation, no caller-side branch. The editor's knob label and
`getParamStringByValue` read the same function. **The editor and the host printing
different text for the same stored value is a defect class, forbidden structurally rather
than caught at review** — same discipline, same reason, as the taper criterion above.
Consequences (existing formatters stop embedding their unit; cutoff's `k` abbreviation is
retired; the curve dial's `^` is static cell chrome, not value): spec §6.7.2.
- **Every default value has an EXACT normalized preimage under its own taper.** Binds
Γ-W1-T1 (which designs the taper) and Γ-W4-T1 (which declares
`ParameterInfo::defaultNormalizedValue`). `resetDeckParam` bypasses the taper; **a host's
reset-to-default cannot**, so exactness in the map itself is the only thing that makes the
editor's reset and the host's reset land on the same value. **Ruling 3 tightens this twice
and adds one non-requirement** (§6.7.7): `defaultNormalizedValue` is **computed** as
`toNormalized(default)`, never written as a normalized literal; the assertion is made on
`toPlain(defaultNormalizedValue)`, the pair the host actually calls; and
`toNormalized(toPlain(n)) == n` at **arbitrary** n is explicitly NOT required — no log map
satisfies it in double, and demanding it would over-constrain the taper for nothing.
- **Nothing in this phase may re-map the filter's four normalized controls.** Cutoff, Q,
morph and drive persist as `*Norm` doubles in payload v9 — their laws are already
wire-frozen, and re-tapering them would re-tune every saved project independently of
automation. The snap-unit table names them; that is display, not law. **This does NOT
conflict with Ruling 3's real-unit requirement**, and the two must not be read as a
collision: `toPlain` is a pure read-side mapping that never touches the stored value, so
reporting Hz / Q / drive depth means **calling** `filterCutoffHzFromNorm` and its peers, not
replacing them — which the editor's own labels already do today. The prohibition forbids
*editing* those laws; the requirement is satisfied by *calling* them. One additive gap:
drive has no published inverse and `filterNormFromDriveDepth` must be added beside the two
that exist — the analytic inverse of a frozen law is not a change to it. Spec §6.7.5.
- **Shift-snap is a drag rule; `stepCount` is a parameter property; they are independent.**
The editor's snap grid must never be exposed as `ParameterInfo::stepCount` — that would
quantize the parameter itself, permanently and for the host's automation too, freezing the
grid into the forever contract and putting continuous cents out of reach from a lane.
**All 44 exposed parameters ship `stepCount = 0`**, swept and confirmed, and the coincidence
is structural: every discrete control is reload or rebuild tier and therefore omitted by the
predicate. Spec §6.7.6.
- **From Γ-W4-T1, the parameter-id table is FOREVER-FROZEN**, on the same footing as the
extension's `"STABLE_FOREVER_STRING"` command ids, the two VST3 class UIDs, and the
params-payload field order. No id is reassigned, reused or re-pointed; no exposed
parameter's normalization ever changes; a retired control's id is retired with it.
Full wording: `docs/product/parameter-automation.md` §6.3.
- **The exposed parameter set is DERIVED, never hand-maintained.** A control is a parameter
if and only if its commit class is `Live` or `NoteOnLatched`. There is no second table
beside `deckParamCommit` / `liveCommitFor`, and no list that can drift from it.
- **The window floor is 1198 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 set it at
1190, in wave 1; no other track in the phase may move it** — with ONE ruled exception,
Daniel 2026-08-02: Γ-W3-T1 widened the row block 1020 → 1028 and the floor 1190 → 1198, so
the justification law puts both rows' filter edges on one pixel (spec §1.3). That is the
only reopening, and only these two constants moved. 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 **82 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.
**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`
**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`
**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`
**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`
**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. **Superseded in part:** Γ-W3-T1 asserted the fit and found 1020 could not deliver the
tie-line it was chosen for, so the block is now 1028 and the floor 1198 — see that track's
block below.
#### Γ-W1-T5 — `preserve-time-stretch`
**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`
**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.
**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.
---
### Γ-W2 — New controls, and the overlay's marks
**Depended on Γ-W1** for the taper law and the Preserve stretcher (T1), and the contour trace
and the floor/row law (T2) — see `docs/COMPLETED.md` for the full narrative.
**Both tracks have landed** — Γ-W2-T1 (`pitch-rate-deck`) and Γ-W2-T2 (`loop-crossfade-ux`) —
see `docs/COMPLETED.md` for the full narrative of each.
#### Γ-W2-T1 — `pitch-rate-deck`
**Landed** — see `docs/COMPLETED.md` for the full narrative. PITCH became PITCH/RATE: three
knobs (`Key Trk | Rate | Pitch`) under the existing Varisp|Presrv toggle, both new controls
wired through the engine. Rate is 50200 % on a taper linear in semitones over ±12 (the stated
exception to the centre-expansion law), note-on latched; Pitch is a ±24 st baseline offset,
live. Keytrack × rate × pitch-offset compound into a single read-increment multiply — the
per-sample path gained nothing. Merged as `9dbb8b8`; spent the phase's second payload rung,
v16, as a strict suffix, a v15 blob lifting to rate 100 % / pitch 0 st.
**`isLiveDeckParam` was renamed `deckParamCommit`** and now returns a three-state `LiveCommit`
(`Live` / `NoteOnLatched` / `Reload`) rather than a bool — one predicate widened, not a second
mechanism. **Rate classifies `NoteOnLatched`, Pitch classifies `Live`; Γ-W4-T1 derives the
VST3 exposed parameter set from this predicate**, so the classification is load-bearing two
waves out.
Code review found one Critical (the resample bake read a stale frame window at non-unity Rate;
fixed by deriving the window from the rate the voice actually reads at, proved non-vacuous by
reverting the fix) and folded in a ruling that Pitch, unlike key-track and velocity, is now
compensated against Varispeed's stage-time coupling. A Varispeed golden hash was added,
honestly witnessed from the remediation commit rather than pre-track, so it stands as a
witness for the next track rather than proof of this one.
#### Γ-W2-T2 — `loop-crossfade-ux`
**Landed** — see `docs/COMPLETED.md` for the full narrative. An explicit loop enable, a
legible mark grammar, and the crossfade painted where it is heard. **No format change** — no
`ComponentState` version moved, no new persisted field, `resolveLoop` untouched, audio
unchanged. The enable **is** `SampleLoop::hasLoop`, whose provenance changes from
marker-gesture-derived to user-owned, with the gestures as shortcuts onto it. Merged as
`a8e30a9`. A new pure module, `core/instrument/ui/loop_marks`, holds the state machine
(`resolveLoopMarks`/`applyLoopMarks`); the four marks (START/LOOP/END/XFADE) get one grammar —
line + shaped cap + label, the cap being the grip — with cap/label/suppression geometry pure
and unit-tested. The crossfade moved to `[loopEnd crossfade, loopEnd)`, drawing as a
top-and-bottom edge wedge, never a second fill; the loop fill's peak alpha stays exactly 0.20.
**START draws in `overlay/trace`, a deliberate deviation from
`docs/product/instrument-control-surface.md` §6.3's table**, because the spec's
`accent/primary` choice would measure 1:1 against the waveform fill it marks.
Code review found three Majors, all fixed before merge. `docs/TODO.md`'s "Pre-existing
staged-envelope-node shadow at zero-attack" entry was resolved incidentally — giving START a
cap is what closed it — and rewritten in place with the recorded outcome by the track itself.
**One thing is deliberately left open, not accepted:** the audible crossfade wedge draws at
1.60:1 against the envelope trace, under the 3:1 floor, with no alpha fix available; Daniel is
judging it visually in the DAW and has not yet ruled. **Neither track has been verified in a
running DAW; both are asserted in CTest only** — 99/99, Debug config, on one machine.
---
### Γ-W3 — The reflow, and the bake correction
**Depended on Γ-W2** for the PITCH/RATE descriptor (W2-T1) — the reflow measured the real
three-cell group rather than a forecast of it, which is the whole reason the arrangement was
scheduled late. **Depended on Γ-W1** for W1-T2's published meter/GR/clip state, which MASTER's
deck draws, and W1-T4's floor, budget constants and row predicate, which T1 consumed rather
than re-derived. **T2 depended on Phase Ξ**`Ξ-W2-T1 (resample-bake-chain)` landing on `dev`
first, the phase's only external gate — see `docs/COMPLETED.md` for the full narrative of each
landed track.
**All three tracks have landed** — Γ-W3-T1 (`deck-reflow`), Γ-W3-T2
(`bake-reset-amendment`), and Γ-W3-T3 (`bake-prints-limiter`) — see `docs/COMPLETED.md` for
the full narrative of each.
**None of the three tracks takes a payload rung.** T1 was layout only; T2 changed a reset list,
not a format; T3 changes what the render's audio contains, not what is stored.
#### Γ-W3-T1 — `deck-reflow`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The knob deck's row law stops
being a wrap outcome and becomes a property of the group descriptor: two categorical rows
(Sound — PITCH/RATE, FILTER, VELOCITY, VOICE; Contour — PITCH ENV, FILTER ENV, AMP ENVELOPE)
plus a double-height, right-anchored MASTER bus deck outside both, carrying the limiter enable
toggle, one reserved cell, the output meter column and a passive gain-reduction lamp.
`DeckRow`/`deckRowFor` (`ui/deck_groups`) is an exhaustive switch over every `DeckGroupId`, so a
group added later without a row assignment is a compile error; the greedy whole-group wrap this
replaces is gone entirely. FILTER's `Band|Notch` moves into its caption's previously-unused
second toggle slot, taking the group from 524 to 432 px; VOICE deliberately keeps its
`Retrig|Legato` row toggle, since moving it to the caption would make VOICE wider, not narrower.
**The row block widened 1020 → 1028 px and the editor floor moved 1190 → 1198 px (Daniel's
ruling, 2026-08-02).** The originally specified 1020 could not simultaneously deliver the
filter tie-line (both rows' FILTER/FILTER ENV right edges at one x) and equal,
no-narrower-than-12px gutters on both rows; at 1028 all three hold, with 82 px of headroom left
against the 1280 px ceiling.
The MASTER meter's per-block state moved from a plain overwriting store to an accumulated one —
at 48 kHz/512-frame blocks roughly 47 blocks elapse between two 500 ms UI ticks, and the
overwriting store had been displaying one block in ~47 and dropping the rest. The processor now
folds a per-channel peak max and a limiter min-gain across the whole interval, drained by
`masterBusMeter()`. **The instrument reload was decoupled from VST3 activation as part of this
track** — `setActive(false)` now parks the decoded `SampleData` and destroys only the voice
state, `setActive(true)` rebuilds the voices around the parked sample — discharging the
`docs/TODO.md` follow-up already recorded in full there. The limiter toggle's commit is split so
the audible state commits inline on the click and only the host's
`restartComponent(kLatencyChanged)` notification is deferred to the editor's existing 500 ms
sync tick.
**Not verified in a running DAW — CTest-asserted only:** the meter at its 500 ms UI cadence, the
GR lamp under real limiter action, the limiter toggle's latency renegotiation, the clip cap's
click-to-clear, and the recapture-while-editor-closed path.
#### Γ-W3-T2 — `bake-reset-amendment`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The correction Phase Γ owed Phase
Ξ: Ξ-W2-T1's bake shipped ahead of the sequencing this plan asserted, so its reset list predated
rate, pitch offset, the limiter enable, and the loop enable. **`resetAfterBake` needed no code
change** — all four already reset by construction, since the function defaults everything and
copies back only survivors, and none of the four was ever named a survivor. The track shipped
field-by-field assertions over two independently-dialled fixtures, never struct equality, plus a
mutation-verified spot-check sweep confirming both fixtures actually moved every asserted field
off its default.
**One invariant correction, at the time this track landed:** `bake/CLAUDE.md` had claimed the
whole signal chain prints, master gain included. It didn't yet — the render's gain multiply was
the only master-stage value it printed; the limiter ran in the processor's block, off the bake
path entirely, so a capture baked with the limiter engaged came back unlimited. **Γ-W3-T3
(below) has since closed that gap** — the limiter is printed too now, and `bake/CLAUDE.md`'s
invariant text is corrected again to match.
**Spec:** `docs/product/instrument-control-surface.md` §3.4, and Ξ-W2-T1's own "Reset scope"
block, now in `docs/COMPLETED.md` (Phase Ξ has landed and its `PLAN.md` section is retired) —
**which is the ratified rule this track applies, not a rule it may reinterpret.**
**Neither track has been verified in a running DAW; both are asserted in CTest only.**
#### Γ-W3-T3 — `bake-prints-limiter`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The bake's master stage now
prints the limiter as well as the gain multiply: `renderBake` instantiates its own `Limiter`
(the bake-only-engine precedent, never linked into `reaper_reasampler`), the lookahead is
compensated inside the render (an extra `flushFrames` of silence past the window, the capture
read out at `leadInFrames + flushFrames`), and `bake/CLAUDE.md`'s invariant text is corrected
alongside the code. All three of the track's `[propose at review]` open questions are
answered in that entry.
---
### Γ-W4 — VST3 parameters
**Depended on every earlier wave in this phase** — W1-T1's taper module (the host-facing
normalization itself), W1-T2 and W2-T1's control inventory and three-state commit
predicate (the exposed set is exactly `Live NoteOnLatched`), W3-T1's final MASTER
inventory, and W3-T2's completed bake reset list — each a hard prerequisite rather than a
courtesy. See `docs/COMPLETED.md` for the full narrative of each.
**The track has landed** — Γ-W4-T1 (`vst3-parameter-set`) — see `docs/COMPLETED.md` for
the full narrative. One track: the storage decision governed every part of the work, so a
pure/host-wiring split would have been serial and bought no concurrency.
#### Γ-W4-T1 — `vst3-parameter-set`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The instrument now reports
its automatable parameters to the host under a FOREVER-FROZEN `ParamID` table (blocks of
100 per deck group in signal-flow order, steps of 10 within a block, a curve dial at its
outer knob's id + 1) — 44 of 44 issue, derived from `deckParamCommit` / `liveCommitFor`
rather than hand-maintained, each with real units, a plain range and display precision at
the host boundary. A new pure module, `core/instrument/param`, holds the id table, the
plain-value layer, the one formatter per unit category, and the audio thread's
block-boundary merge decision; `shell/instrument/instrument_params` adapts it onto
`Steinberg::Vst::Parameter` and decides nothing.
**Both VST3 delivery channels are serviced** — the controller's `setParamNormalized` and
the audio thread's `IParameterChanges` drain, mirroring the SDK's own
`SingleComponentEffect` sample rather than the controller-only delivery an earlier pass
shipped. **A host automation point's authority is bounded**, not permanent: it outranks
the model only until the UI thread folds it in and republishes — at most one UI tick —
never a later restore, bake reset or knob move. `shell/instrument/CLAUDE.md`'s "THE
AUTHORITY MODEL" section states it, `param_merge` enforces it.
**Two rulings, both Daniel, 2026-08-02.** Pitch key-track and Trigger length promote from
`Reload` to `NoteOnLatched` — the promotion that takes the count to 44 of 44 and issues
ids 1000 and 1450, and which needed `LiveValues`/`foldLive` widened and `Voice::start`
handed the two latched values, not just a predicate flip; the new `param::valueHomeFor`
guard closes the class of bug the promotion exposed. And the curve-shape dials' ±0.01
snap-to-centre band now applies on the mouse-drag path only, never on a host-facing map.
**The bake's reset now notifies the host; the double-processing limitation (a host
automation lane outranking the reset) is a named, documented boundary, not discovered
later.** `IMidiMapping`, `IParameterFunctionName` and `IAutomationState` are all assessed
and NOT implemented.
**Not verified in a running DAW — CTest-asserted only.** `docs/TODO.md` carries the
residual DAW-verification items.
---
## Phase Λ — ReaSampler on Linux: both artifacts, shipped
**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented
@@ -1109,18 +544,18 @@ only; the `WIN32` gate is Λ-W6-T1's**), `README.md`, and root `CLAUDE.md` §"Bu
**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T3 and §Λ-F2; findings Λ-01, L2-06, L2-07.
**The defect.** `panel_window.cpp:135` is `CreateDialogParam(g_hInst,
**The defect.** `panel_window.cpp:169` is `CreateDialogParam(g_hInst,
MAKEINTRESOURCE(IDD_BANK_PANEL), …)`, which SWELL resolves out of a per-module registry
populated by a **resgen-generated source file that is not in the Linux target**:
`src/app/CMakeLists.txt:97` has the `target_sources` line commented out (`:86` for macOS).
The registry head stays null, `SWELL_CreateDialog` returns null, `:137` returns, and the
`src/app/CMakeLists.txt:109` has the `target_sources` line commented out (`:98` for macOS).
The registry head stays null, `SWELL_CreateDialog` returns null, `:171` returns, and the
toggle action is a **silent no-op** — no console line, no Actions-list checkmark. Three
defects stack inside the commented-out instructions themselves: the script named at `:96`
defects stack inside the commented-out instructions themselves: the script named at `:108`
(`mac_resgen.php`) does not exist, the output filename is wrong, and the output is an
`#include`-only artifact that cannot be a `target_sources` entry at all.
**Surface boundary — owns:** `src/shell/panel/panel_window.cpp` (the `CreateDialogParam` call
at `:135137`, the dialog proc's platform contract, the drop-accept opt-in at `:145150`),
at `:169171`, the dialog proc's platform contract, the drop-accept opt-in at `:179184`),
`src/resource.rc`, `src/resource.h`, and — **under the resgen route only** — one
`target_sources` line in `src/app/CMakeLists.txt`'s `else()` branch plus a new include-shim
TU. **Does not own:** any other panel TU, `draw_kit`, or any CMake target property.
@@ -1340,7 +775,7 @@ invariants Linux weakens are stated where a reviewer will read them.
**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T3 and §Λ-F3; findings Λ-06, L2-10, L2-11.
**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:851` (**the
**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:855` (**the
list; the property blocks are Λ-W2-T2's**), any new platform-sibling TU the sweep showed was
needed, `src/shell/actions/drag_out_win.h`'s invariant comment, and the corresponding
`src/shell/**/CLAUDE.md` invariant passages. **Does not own:** any behaviour change in a
@@ -1476,7 +911,7 @@ or `.h` under `shell/instrument/`.
`$APPFOLDER/vst3/`.
- **`SWELL_PROVIDED_BY_APP` + the modstub TU** are added to this target in the **default
(non-`SWELL_LOAD_SWELL_DYLIB`) branch** — the same branch the extension already uses
(`src/app/CMakeLists.txt:9192`). The whole modstub file is inside
(`src/app/CMakeLists.txt:103104`). The whole modstub file is inside
`#ifdef SWELL_PROVIDED_BY_APP`, so the VST3 target must define that symbol; today it does
not. **The `dlopen` itself is Λ-W7's**; what this track lands is the compiled-in, inert
table plus the exported `SWELL_dllMain`.
@@ -1858,6 +1293,7 @@ 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)
[LANDED — all four waves (W1-W4); see docs/COMPLETED.md; full detail section removed from this file]
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]
@@ -1872,23 +1308,22 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends
W2 New controls, and the overlay's marks — landed [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
W3 The reflow, and the bake correction [3 tracks; T1/T2 landed, T3 open]
W3 The reflow, and the bake correction — landed [3 tracks]
T1 deck-reflow ................ two rows + double-height MASTER, by construction
T2 bake-reset-amendment ....... the Xi correction Gamma owns
T3 bake-prints-limiter ........ NOT STARTED — bake to print the limiter
through the master stage [Daniel, 2026-08-02]
W4 VST3 parameters [1 track]
T3 bake-prints-limiter ........ landed — bake prints the limiter through
the master stage
W4 VST3 parameters — landed [1 track]
T1 vst3-parameter-set ......... 44 derived params, frozen id table [Ruling 1]
[rung 3 RESERVED, spent only if verify says so]
[OPEN: Gamma-F7, the parameter order — Daniel]
[rung 3 RESERVED, unspent]
[Gamma-F7 RULED: signal flow order]
Resequenced 2026-08-01, three times: the reflow split canvas (W1-T4) from arrangement
(W3-T1); preserve-time-stretch moved W4 -> W1-T5, retiring Rate's interim stand-in; then
Ruling 1 added W4 and Ruling 2 grew W1-T1.
Payload rungs are RELATIVE, not absolute. On this branch at 2026-08-01, the first is already
spent — kParamsPayloadVersion == 15 (Γ-W1-T2, landed, above) — the second is the next free
rung, v16 (Γ-W2-T1, stated there), and the third remains RESERVED as v17, spent only if
Γ-W4-T1's storage verification forces it.
Payload rungs are RELATIVE, not absolute. Both are spent — kParamsPayloadVersion == 15
(Γ-W1-T2, landed, above) and v16 (Γ-W2-T1, stated there) — and the third remains RESERVED
as v17, unspent: Γ-W4-T1's storage verification did not force it.
Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5), ui/CMakeLists.txt
(W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not
semantic contention. W3-T2's disjointness from W3-T1, once a CONDITIONAL risk to confirm
@@ -1907,7 +1342,7 @@ Phase Psi — The extension trust pass (none of the seventeen; a direct
Three invariant amendments are track deliverables: never-touch-solo (W1-T2, three
files), the action-registration contract (W1-T3, root CLAUDE.md), channel-count-
preserved (W2-T2, root CLAUDE.md:208).
preserved (W2-T2, root CLAUDE.md:215).
Shared files, named: panel_input.cpp (W1-T2 footer block | W1-T4 drag-arm block);
capture.cpp + capture_realtime_finalize.cpp (W2-T1 naming lines | W2-T2 channel
lines) — all textual adjacency, not semantic contention. main.cpp is W1-T3's
@@ -283,10 +283,10 @@ This matters more here than the usual "forgot to set the build type" — but onl
`CLAUDE.md`'s §"Build and test" (not §"Performance guardrails") states, at `CLAUDE.md:54`,
that the header-inline hot paths "presume an **optimizing** build", with no LTO configured to
recover it if they don't. `peaks`' `computeEnvelope`/`lastFrameAboveThreshold` is one of
those paths and IS linked into the extension (`src/app/CMakeLists.txt:52`), so the finding
those paths and IS linked into the extension (`src/app/CMakeLists.txt:56`), so the finding
holds for Fork A as stated. It does **not** extend to `Voice::advanceFrame` or the three
`envelopes.h` evaluators (named at `src/core/instrument/CLAUDE.md:342`, not in root
`CLAUDE.md`) — those belong to `sampler_core`, which `src/app/CMakeLists.txt:5355` states is
`envelopes.h` evaluators (named at `src/core/instrument/CLAUDE.md:378`, not in root
`CLAUDE.md`) — those belong to `sampler_core`, which `src/app/CMakeLists.txt:5759` states is
deliberately NOT linked into the extension and which only enters a build at all inside the
Windows-gated `reasampler_vst` target (§1b). On Fork A that code never compiles into
`reaper_reasampler` in the first place; the unoptimized-build risk to it is a Fork B concern,