diff --git a/docs/PLAN.md b/docs/PLAN.md index 6071886..72dcffe 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1005,213 +1005,117 @@ running DAW; both are asserted in CTest only** — 99/99, Debug config, on one m ### Γ-W3 — The reflow, and the bake correction -**Depends on Γ-W2 for:** the PITCH/RATE descriptor (W2-T1) — the reflow measures the real -three-cell group, and laying it out against a forecast of that group means re-measuring -afterward. **This is the whole reason the arrangement is late**, and it is why the canvas was -split out of it into W1-T4. **T2 depends on the same wave for a different reason:** rate and -pitch offset must exist before the bake's reset list can name them. +**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. -**Depends on Γ-W1 for:** W1-T2's published meter/GR/clip state, which MASTER's deck draws -(drawing against a stub would mean building the meter twice), and W1-T4's floor, budget -constants and row predicate, which T1 **consumes rather than re-derives**. **T2 depends on -W1-T2 for the limiter enable flag**, the third of the three values it must add. +**Two tracks have landed** — Γ-W3-T1 (`deck-reflow`) and Γ-W3-T2 (`bake-reset-amendment`) — see +`docs/COMPLETED.md` for the full narrative of each. **A third track is now open and has not +run:** Γ-W3-T3 (`bake-prints-limiter`), added below on Daniel's ruling of 2026-08-02. -**Depends on Phase Ξ for T2 — the phase's only EXTERNAL gate.** `Ξ-W2-T1 -(resample-bake-chain)` must have landed on `dev` before T2 dispatches. T2 amends what that -track shipped; it cannot amend a branch. - -**Two tracks, disjoint by surface — but T2's disjointness is CONDITIONAL and must be -confirmed, not assumed.** T1 owns `ui/knob_deck`, `ui/deck_groups` (row-predicate -consumption, FILTER's caption move, MASTER's inventory) and `shell/instrument/editor_paint_deck`. -T2 owns the bake's reset step wherever Ξ-W2-T1 put it. **T2's first act is to read what -actually shipped and confirm its reset surface touches none of T1's three modules.** If the -shipped reset enumerates controls through `deck_groups` or `deck_values`, the two are not -disjoint and **T2 serializes behind T1 inside the wave** — a named contingency, taken openly, -not discovered at merge. That risk is real precisely because this plan cannot predict the -shipped shape; predicting it is what put the phase in this position. - -**Why T1 is one track.** The row law, the group inventory and the double-height deck are one -geometry decision spread over `knob_deck`, `deck_groups` and the deck painter. Splitting it -would put two tracks in the same pure modules. **The window floor is no longer part of it** — -W1-T4 set it two waves ago, and this track must not move it. - -**Neither track takes a payload rung.** T1 is layout only; T2 changes a reset list, not a -format. +**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` -**Goal.** Two categorical rows plus a double-height MASTER bus deck, inside a 1280 × 720 -ceiling, returning 112 px to the waveform. +**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. -**Spec:** `docs/product/instrument-control-surface.md` §1 (the whole section, incl. the §1.2 -measured table **and §1.6, the headroom ledger**) and §3.2–3.3 (what MASTER draws). **§7 lists -the invariants this track invalidates or widens — read it before touching `knob_deck.h`.** +**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. -**Surface boundary — owns:** `core/instrument/ui/knob_deck` (the row law, the double-height -group, the justification — **consuming** W1-T4's budget constants, not restating them), -`core/instrument/ui/deck_groups` (consumption of W1-T4's row predicate, FILTER's `Band|Notch` -caption move, MASTER's inventory), and `shell/instrument/editor_paint_deck` (the MASTER -meter/limiter/bubble draw). **Does not own** `kEditorMinWidth` or any budget constant — those -are W1-T4's and are **read**, never moved — nor any parameter, the limiter DSP, or the -waveform band. +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. -**Behavior.** -- **Row 1 (sound), one row, non-negotiable:** PITCH/RATE 192 · FILTER 432 · VELOCITY 192 · - VOICE 164 = **980** natural. -- **Row 2 (contour):** PITCH ENV 252 · FILTER ENV 312 · AMP ENVELOPE 312 = **876** natural. -- **MASTER is double-height (216 px) and right-anchored**, outside both rows, 142 px wide. -- **FILTER's `Band|Notch` moves from its row-toggle position to the caption corner**, taking - the group 524 → **432** (−92 px). It occupies FILTER's currently-unused `captionToggle2` - slot — **no new geometry is required**. -- **VOICE keeps its `Retrig|Legato` row toggle.** Moving it to the caption makes VOICE - *wider* (226, not narrower), because its caption row is the binding side. Verified; do not - "fix" it. -- **Justification law, applied to BOTH rows:** space-between within the row block; slack - divided equally among the row's (n−1) gutters, integer residue to the leftmost; - **no gutter narrower than `kDeckGroupGap` (12)**. **Decks are never stretched.** MASTER is - not part of either row's justification. -- **Row block = 1028 px at the floor** (widened from the originally specified 1020 — Daniel, - 2026-08-02), giving row 1 gutters 16/16/16 and row 2 gutters 76/76, at which width - **FILTER's right edge and FILTER ENV's right edge both land on x = 640**. The tie-line and - both rows' equal gutters hold at 1028 because each row's slack divides by its gutter count - with no residue. **This is why the floor is 1198 and not 1190.** The originally specified - 1020 delivered NEITHER the tie-line (638 vs 636) nor the claimed exactly-`kDeckGroupGap` - smallest gutter (13); the three properties were never simultaneously satisfiable, and 12 is - a floor rather than a target — spec §1.3 records all three deviations. Above the floor the - tie-line drifts and that is accepted (spec §1.3). -- **The bands are already 216 / 358 and the floor was already 1190 × 680** — Γ-W1-T4 landed all - four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track - changes only the row block and the floor (see above); the rest it makes true by construction rather than by coincidence.** - Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030 - today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion - and W1-T4 deliberately left it open. -- **The 82 px of remaining headroom is the budget for the life of this layout**, and one deck - cell is 60 px. **This is why MASTER's reserved slot is ONE cell** (Γ-F5, ruled): two would - spend 60 of the 82 up front on a control nobody has named, leaving 22 — which would freeze - row 1 forever, since any later row-1 addition needs 60. Widening MASTER later costs the same - 60 it would cost now, and by then the trade is against a real control instead of a guess. - **State this ledger where a future reader will hit it** — spec §1.6 is its home, and a - reader proposing a new knob needs to see it before they propose. -- **MASTER's interior** (spec §1.4, exact to the pixel): caption row with the limiter toggle - and a **round** 12 px `warn` GR bubble in the far corner (non-interactive — the same slot the - envelope decks' radio uses; round so it reads as a lamp, not a control); **gain knob in the - upper-left cell at box-relative y = 26** and a **reserved empty slot at y = 138** — i.e. the - two cells land on row 1's and row 2's knob baselines exactly, which is what stitches the - spanning deck to both rows; **meter column 62 px wide × 186 px tall** on the right. -- **Three rules not to generalise wrongly:** MASTER's left column uses **fixed cell slots at - the two baselines, NOT the horizontal run-division law** (that law would stretch one knob - over 186 px); the reserved slot **draws nothing** (blank reads as breathing room, a dashed - placeholder reads as unfinished); the meter is **one rect spanning both baselines**, not two - per-row meters. -- **The meter draws W1-T2's published state**, with the ballistics run on the UI timer. - **Bar count follows the same `LaneSplit` decision `waveformSurface` already folds** (channel - mode ∧ source channel count) — one wide bar when the waveform draws one lane, two skinnier - bars when it draws two. Not a second rule: a mono source in stereo mode is dual-mono, and - two identical bars would be a lie. -- **Meter appearance:** bar in `accent/primary`; peak-hold tick 2 px in `text/primary`; clip - cap in `warn`, latched, click-to-clear; scale linear in dB over −60…+6 with ticks every - 6 dB and numerals at 0/−12/−24/−36/−48/−60, the 0 dB tick heavier. **No green/yellow/red - segmentation** — `warn` stays reserved for clip states. - -**Acceptance criteria.** -- At the floor width the deck lays out in **exactly two rows plus the spanning MASTER**, - **by construction** — asserted against the group inventory, not observed as a wrap outcome. -- Every group's width matches the §1.2 table exactly, **in both Gate and Trigger** (row 2's - natural width is mode-stable at 876 because the reserve slots hold FILTER ENV and AMP at - 312 in both modes — assert it). -- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line - is exact (both edges at x = 640) and BOTH rows' gutters are equal. -- **Row 1's natural width is 980 and fits the 1028 block** — the fit Γ-W1-T4 could not yet - assert, closed here by the `Band|Notch` move. -- **`kEditorMinWidth` is 1198 and the floor is still ≤ 1280 × 720** — moved 1190 → 1198 by this - track, verified against Γ-W1-T4's derived test rather than a second copy of it. -- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the - interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the - row predicate says so, not because a wrap landed there. Assert against the group inventory. -- MASTER's gain knob shares a knob baseline with FILTER's knobs; its reserved slot shares one - with AMP ENVELOPE's. -- The meter reads correctly in mono and stereo, the peak-hold tick holds 1.5 s, the clip cap - latches and clears, and the GR bubble lights only while the limiter reduces gain. -- **With the limiter engaged the clip cap never latches** on material the limiter is catching; - if it does, that is a defect report against W1-T2, not a user error. -- `knob_deck`'s and `sample_bands`' tests are updated to the new law, and the invalidated - notes in `knob_deck.h` (the fourteen-pixel headroom figure; the cells-and-floor pairing) are - **re-derived, not deleted** — spec §7.1, §7.4. - -**Open questions.** -- **[propose at review]** Whether the greedy whole-group wrap survives at all as a sub-floor - degrade, or is replaced outright by explicit row assignment. What is **not** optional: at - and above the floor width the layout is the specified arrangement, reached by construction. - `DeckLayout::rowCount`/`::height` change meaning either way (spec §7.3). -- **No [Daniel] questions.** Forks Γ-F5 (**one cell**) and Γ-F1 (**680 stays**) are both - ruled; they are stated in Behavior above, not carried here as options. -- **[verify]** `deck_groups.cpp`'s `kEnvModeSegW = 23` ceiling rises to **47** once PITCH ENV - is on row 2 (AMP binds at 55). No change is required; the comment stating the old ceiling - stops being true and must be corrected (spec §7.2). +**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` -**Goal.** Complete the resample bake's reset list against the control surface that now -exists — the correction Phase Γ owes Phase Ξ because Ξ-W2-T1 shipped ahead of the sequencing -this plan asserted. +**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. -**Consolidates:** nothing from the seventeen. It is a **correction obligation**, not a -feature (see "Flagged for awareness" item 2). +**One invariant correction:** `bake/CLAUDE.md` had claimed the whole signal chain prints, master +gain included. It doesn't — the render's gain multiply is the only master-stage value it +prints; the limiter runs in the processor's block, off the bake path entirely. -**Spec:** `docs/product/instrument-control-surface.md` §3.4, and Ξ-W2-T1's own "Reset scope" -block above — **which is the ratified rule this track applies, not a rule it may reinterpret.** +**Outstanding, not closed by this track.** A capture baked with the limiter engaged comes back +unlimited — a real audible gap, and Daniel has ruled that a future track will change the bake to +print the limiter. **That track is now Γ-W3-T3, below.** -**Surface boundary — owns:** the bake's parameter-reset step, wherever Ξ-W2-T1 landed it, and -its tests. **Does not own** the bake chain, the crossing architecture, the replace-vs-add -decision, the capture path, any deck module, any painter, any parameter, or any -`ComponentState` version. **It changes what a shipped list contains — nothing else.** +**Neither track has been verified in a running DAW; both are asserted in CTest only.** -**Behavior.** -- **Three values join the reset list**, all classified against Ξ-W2's own ratified rule - ("reset what the bake baked in"), all **reset**, none of them a new Daniel decision: - **rate**, **pitch offset**, and **limiter enabled**. The limiter's reasoning is worth - carrying rather than re-deriving: master gain is already on the reset list, so the bake - includes the master stage, so the limiter's effect is in the audio. -- **Verified against what shipped, not against what was predicted.** This plan named three - values before either the bake or the controls existed. **Read Ξ-W2-T1's landed reset list - first** and reconcile: if it already anticipated any of the three, say so and drop it; if - it classified something differently from `docs/product/instrument-control-surface.md` §3.4, - **the landed code is the fact and this plan is the prediction** — escalate the difference, - do not silently overwrite either. -- **Re-run Ξ-W2-T1's own "genuinely new parameter" check over everything Phase Γ added**, not - just the three named. Γ also ships the loop enable (W2-T2) and raises the stage-time - ceiling (W1-T1). Classify each **against the rule**: the loop enable is a loop fact whose - effect is in the rendered audio (**reset**, with the loop points it travels with); the - ceiling is not a parameter at all. State each disposition; silence is not one. -- **Root note still survives.** The bake's most load-bearing exception is untouched by this - track — capturing at root is what makes root survivable, and resetting it would detune - every subsequent iteration. +#### Γ-W3-T3 — `bake-prints-limiter` -**Acceptance criteria.** -- After a bake, **rate reads 100 %, pitch offset 0 st, and the limiter reads bypassed** — and - the root note, key-tracking and the VOICE group are still untouched. -- **The bake stays audible and faithful with the new controls dialled in**: dial rate, pitch - offset and the limiter, bake, and the neutral instrument playing the programmed note sounds - as the dialled one did — the criterion Ξ-W2-T1 already carries, now actually exercised over - Γ's controls. -- **A reconciliation note in the track's review** stating, per value, whether the landed code - already covered it, and recording any difference between what shipped and what §3.4 - predicted. -- **No format change, no new field, no version bump, no change to the crossing architecture - or the replace-vs-add decision.** A regression baseline proves the bake's audio is - otherwise unchanged. +**Not started. Opened by Daniel's ruling, 2026-08-02.** -**Open questions.** -- **No [Daniel] questions.** The rule is ratified and §3.4's classification is derived from - it. -- **[verify, FIRST]** the disjointness contingency in the wave header: read the shipped reset - step and confirm it touches none of Γ-W3-T1's modules. If it does, serialize behind T1 and - say so. -- **Explicitly NOT this track's:** the two consequences automation adds to the bake — the - reset having to notify the host, and a host lane re-imposing its curve onto baked audio. - Both are **Γ-W4-T1's**, because that track creates them. Doing this correction once, before - automation, and letting Γ-W4-T1 add its own obligation on top is deliberate: the - alternative is an amendment to an amendment. +**Goal.** Print the limiter through the bake's master stage, so a capture baked with the +limiter engaged returns limited audio rather than unlimited audio. + +**Why this exists.** Γ-W3-T2's own finding disproved the premise +`docs/product/instrument-control-surface.md` §3.4's reset classification rested on: +`renderBake` (`core/instrument/bake/bake_render.cpp`) prints only a flat master-gain multiply, +and the limiter (`core/instrument/engine/limiter`) runs in the processor's `process()` block, +off the bake path entirely. Until this track lands, this is a recorded, known limitation — see +`docs/COMPLETED.md`'s Γ-W3-T2 entry — not an oversight. + +**Consolidates:** nothing from the seventeen. A correction, on the same footing as Γ-W3-T2 (see +"Work in this plan that is not one of the seventeen"). + +**Spec:** none yet written. This ruling postdates §3.4 and has no product-doc section of its +own; §3.4 is superseded on this one point, which a future scoping pass of this track should +correct there as well as here. + +**Surface boundary — likely, not yet confirmed against a full scoping pass:** owns +`core/instrument/bake/bake_render` (the gain-multiply step, extended to also run the signal +through a limiter), consuming `core/instrument/engine/limiter` — not owned, not modified. Does +not own the processor's live block, the limiter DSP itself, the parameter surface, or +`bake_reset` (the limiter-enable reset classification is already Γ-W3-T2's, landed). + +**Open questions — none of this is ruled yet, only the goal is:** +- **[propose at review]** Whether the bake instantiates its own `Limiter` — mirroring + `renderBake`'s existing bake-only `VoiceEngine`, off the audio thread, never linked into + `reaper_reasampler` — or reaches the limiter's settled behavior some other way. The + bake-only-engine precedent (`bake/CLAUDE.md`) argues for the former. +- **[propose at review]** Whether the limiter's lookahead needs any accommodation in an + offline, non-realtime render — the processor's `getLatencySamples()` PDC report exists for + the live block, and a bake is not on that clock, so this may be a non-issue; it has not been + checked. +- **[propose at review]** Whether this track also corrects `bake/CLAUDE.md`'s invariant text + ("the limiter is not [printed]") alongside the code, once scoped in full. +- **No [Daniel] question on the goal itself** — the ruling above is the goal; what is open is + the mechanism, not whether to do it. --- @@ -3520,9 +3424,9 @@ proof it exists to give. from `TODO-1.0.md`, and not a track this plan originally scoped. The second such track in this plan today; if others appear, they belong on this list rather than in the table. -- **All of Phase Γ** (`pg-*`). **Twelve tracks across four waves** (W1 seven, W2 two, W3 two, - W4 one), from a direct interview with Daniel (2026-08-01) and his four later rulings the - same day, not from `TODO-1.0.md`. Listed +- **All of Phase Γ** (`pg-*`). **Thirteen tracks across four waves** (W1 seven, W2 two, W3 + three, W4 one), from a direct interview with Daniel (2026-08-01) and his four later rulings + the same day, not from `TODO-1.0.md`. Listed here as a block rather than per track, because the whole phase is outside the source doc; the product reasoning lives in `docs/product/instrument-control-surface.md` and the parameter system's in `docs/product/parameter-automation.md` §§6–10. **Two `docs/TODO.md` @@ -3542,6 +3446,11 @@ proof it exists to give. plan's sequencing claim. If more corrections of this shape appear, they belong here rather than in the table — the table is a completeness proof over `TODO-1.0.md`, and a correction has no source row to point at. +- **Γ-W3-T3 `bake-prints-limiter` is also a CORRECTION, not a feature**, and for the same + reason as T2: it exists only because T2's own finding disproved the premise §3.4's reset + classification rested on. Unlike the twelve above, it does not come from the 2026-08-01 + interview or that day's four rulings — it is a separate ruling, one day later (2026-08-02), + opened after T2 landed and found the gap. - **All of Phase Ε** (`pe-*`). **Six tracks across three waves**, from a direct request (Daniel, 2026-08-02), not from `TODO-1.0.md`. Listed here as a block, like Γ and Ψ; the product reasoning lives in `docs/product/bank-package.md`. It **supersedes nothing** — @@ -3652,9 +3561,11 @@ 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 [2 tracks] + W3 The reflow, and the bake correction [3 tracks; T1/T2 landed, T3 open] T1 deck-reflow ................ two rows + double-height MASTER, by construction - T2 bake-reset-amendment ....... the Xi correction Gamma owns [needs Xi-W2-T1 on dev] + 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] T1 vst3-parameter-set ......... 44 derived params, frozen id table [Ruling 1] [rung 3 RESERVED, spent only if verify says so] @@ -3669,8 +3580,8 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends Γ-W4-T1's storage verification forces 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 is CONDITIONAL: confirm it against - what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold. + semantic contention. W3-T2's disjointness from W3-T1, once a CONDITIONAL risk to confirm + against what Xi-W2-T1 shipped, is resolved: both tracks landed — see docs/COMPLETED.md. Phase Psi — The extension trust pass (none of the seventeen; a direct list of seven) W1 Exact bounds, disciplined switches, reachable actions, resolved drops [4 tracks]