From 27712b757305d36ecd4695a041f88761050162dc Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 22:31:54 -0400 Subject: [PATCH] docs: retire Phase Gamma from PLAN.md; repair stale line/symbol citations across PLAN, the Lambda build-toolchain audit note, and root CLAUDE.md --- CLAUDE.md | 6 +- docs/PLAN.md | 605 +----------------- .../lambda-w1-t1-build-toolchain.md | 6 +- 3 files changed, 26 insertions(+), 591 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1491952..7925d08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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::` for `core/`, house style for `shell/`. `app/` holds -`main.cpp` only: API-pointer ownership, `ReaperPluginEntry`, and dispatch. +`param/` / `ui/`. Namespaces mirror directories — `reasampler::` 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. diff --git a/docs/PLAN.md b/docs/PLAN.md index 096ab81..53d0d8a 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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` §§6–10**. 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**: §§1–5 are the original analysis, **§§6–10 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 50–200 % 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 `:135–137`, the dialog proc's platform contract, the drop-accept opt-in at `:145–150`), +at `:169–171`, the dialog proc's platform contract, the drop-accept opt-in at `:179–184`), `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:8–51` (**the +**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:8–55` (**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:91–92`). The whole modstub file is inside + (`src/app/CMakeLists.txt:103–104`). 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 diff --git a/docs/product/audit-notes/lambda-w1-t1-build-toolchain.md b/docs/product/audit-notes/lambda-w1-t1-build-toolchain.md index 481689c..d31ad6d 100644 --- a/docs/product/audit-notes/lambda-w1-t1-build-toolchain.md +++ b/docs/product/audit-notes/lambda-w1-t1-build-toolchain.md @@ -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:53–55` 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:57–59` 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,