diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index de49d1a..458fadb 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -754,3 +754,62 @@ bank-format change under the current deserializer, which fails the whole bank bl an out-of-range value — it wants its own decision); and `instrument_bake` copies the interleaved render buffer into a `std::vector` for the WAV build, roughly doubling peak memory for a large bake. + +### Ξ-W3-T1 — capture-signal-popup (spec abandoned by ruling; shipped as derived bake window) + +Phase Ξ's final track, and Phase Ξ is now complete. What `docs/PLAN.md` specified was a +popup menu letting the user hand-program the capture signal: note length as a +musical-division picker (1/64 to 64/1, dotted and triplet), start/end offsets editable in +both ms and beats, velocity, and a preview trigger auditioning the programmed note, under +the acceptance criterion "preview and bake cannot diverge." + +**What shipped instead, and why — the spec and the landed code diverge substantially and +deliberately, by Daniel's ruling, not by shortfall.** The popup was built (~1500 lines) +and then abandoned unmerged. Daniel, verbatim: *"I didn't realize you had already derived +a usable window. The manual stuff for baking a specific midi length was just an idea, if +we have a smarter, fewer-clicks way of doing it, that is ideal. I just don't want to lose +anything when we bake. We can abandon the whole parameterized bake window if we can safely +derive the window in gate and trigger modes."* An audit then established, with executable +tests (`tests/test_bake_window.cpp`), that the window derives losslessly everywhere except +one irreducible case. What actually shipped, in `src/core/instrument/bake/`: + +- **The bake window derives itself.** Trigger derives from the play span; Gate *without* + an active sustain loop derives from source exhaustion + release; Gate *with* an active + loop takes one user value, because a loop sounds for as long as it is held and no + derivation can supply a duration. +- **One control: "Hold,"** a musical-division picker in the chrome row, visible and + settable only when Gate + an active sustain loop. `bakeWindowNeedsHold` is the predicate + and it reads the ENGINE's loop fold (`resolveLoop`) rather than the loop fields. +- **Velocity comes from the instance's persisted preview velocity**, not a hard-coded 100 + — three velocity curves are live, so the velocity is a property of the sound being + printed. +- **No preview trigger, and no popup at all.** The chrome-row play button stays a pure + MIDI trigger; bake parameters are their own thing. Daniel's ruling: *"play button is + pure MIDI trigger, Bake parameters are their own thing."* So the plan's acceptance + criterion 2 ("preview and bake cannot diverge") and its preview-trigger behavior bullet + are **retired by ruling.** +- **Three truncation bugs that pre-existed on `dev` were found and fixed:** a drawn EG + plus a stale stored `%`-length lost up to the whole take; the Preserve pitch engine's + window closed on the exact frame the terminal declick ramp began, ending files on a + full-scale hard cut; and the Gate hold length quantized onto a musical ladder that + **saturated at 384 beats**, cutting any source past it mid-sound (at 120 BPM, anything + from 192 s up — a full-mix bounce). +- **An invariant was deliberately amended:** `note/CLAUDE.md`'s "note length stays + musical-division-only" is superseded — a note length now carries EITHER an exact + duration (every derived path) or a musical division (the Hold picker only). + Quantizing a derived length is what caused the saturation truncation. +- Two undefined-behaviour paths closed as fallout: a NaN `keyTrack` from a corrupt payload + reached a narrowing cast on the per-sample audio path, and a misaligned payload tail + could fabricate a value rather than degrade to absent. +- Payload rung **v14** consumed (the Hold division). + +**Not verified in a live REAPER session** — worth carrying forward as owed: the "Bake +Hold" label fitting its 56 px cell, the Hold knob's duration-ordered travel, and the +control's appearance/disappearance on the 500 ms sync tick. + +**Deferred, not done — logged to `docs/TODO.md`:** the loop intrinsic is folded twice +(the editor's `pickedMarkers` resolves it from the live bank blob first, the processor's +`reloadInstrument` resolves it from the instance ref via `resolveCapture`), so the two can +disagree whenever a bank blob's loop for a capture differs from the copy in the instance's +own refs table. Pre-existing — `bakeWindowNeedsHold` is only a new *consumer* of +`pickedMarkers`, not the origin of the divergence. diff --git a/docs/PLAN.md b/docs/PLAN.md index d0cf0e6..0988f89 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -71,15 +71,15 @@ in Γ-W4-T1 and changes no wave boundary. ### Flagged for awareness — not blocking, but decision-grade -1. **Item 15 has an unresolved cross-artifact seam, and it is the phase's largest - unknown.** The instrument is a read-only bank consumer by invariant - (`src/shell/instrument/CLAUDE.md`), and the one previous attempt at an - instrument→extension relay (S13) closed with a **DEGRADED** spike verdict and was - deferred (`docs/TODO.md`). Resample requires that crossing. Ξ-W2-T1 opens with the - architecture decision and this plan names the candidates rather than assuming the - deferred relay shape; see that track. Consequence to hold: if every candidate fails - verification, item 15's "one click from inside the VST" framing is what gives, not - the read-only invariant — the fallback is a bindable extension-side action. +1. **Item 15's cross-artifact seam is RESOLVED — this is no longer an unknown.** The + instrument is a read-only bank consumer by invariant (`src/shell/instrument/CLAUDE.md`), + and the one previous attempt at an instrument→extension relay (S13) closed with a + **DEGRADED** spike verdict and was deferred (`docs/TODO.md`). Resample required that + crossing. Ξ-W2-T1 ratified Decision 1 = (1b): the editor invokes the extension's bake + action directly over the VST-host bridge (`NamedCommandLookup`/`Main_OnCommandEx`), no + poller, no nonce — dissolving the S13 DEGRADED verdict rather than re-litigating it. The + read-only bank invariant held: the crossing is a bridge call, not a shell-side bank + write. See `docs/COMPLETED.md` for the full narrative. 2. **The "Γ before Ξ-W2" ordering is VIOLATED, it was never Daniel's choice, and Γ now owns the correction.** Daniel, 2026-08-01: *"xi was started before I spun you up, we'll have to @@ -396,6 +396,9 @@ and the loop points by name. Baking a processing chain that does not exist yet i schedule preference; the feature is not expressible. Phase Θ landed as of Θ-W7-T1 (see `docs/COMPLETED.md`); this gate is satisfied. +**All three waves have landed — Phase Ξ is complete.** W1 through W3 each carry their own +landed note below; see `docs/COMPLETED.md` for every track's full narrative. + --- ### Ξ-W1 — Consolidated tracking, and the programmed-note model @@ -427,8 +430,12 @@ peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_div `StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign uniform, positive is later in time; only an inverted window is refused, reported via -`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed — note length -stays musical-division-only, and an offset stores the denomination it was entered in. +`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed as of this +landing — note length stays musical-division-only, and an offset stores the denomination +it was entered in. **Superseded by Ξ-W3-T1's landed work** (see `docs/COMPLETED.md`): +`note/CLAUDE.md`'s musical-division-only rule is amended — a note length now carries +EITHER an exact derived duration or a musical division, not division-only. The offset +denomination rule is unaffected. --- @@ -470,63 +477,35 @@ Daniel's manual verification is still owed. ### Ξ-W3 — The capture-signal popup -**Depends on Ξ-W2 for:** the bake chain that the popup programs and its preview must not -diverge from. The acceptance criterion "preview and bake cannot diverge" requires one -render path, which W2 owns — building the preview against a second path is exactly the -defect the criterion exists to catch. +**Depends on Ξ-W2 for:** the bake chain this track was originally scoped to build a +preview against, under the acceptance criterion "preview and bake cannot diverge." **That +motivation is retired, not satisfied** — see below. **One track.** -#### Ξ-W3-T1 — `capture-signal-popup` - -**Goal.** Let the user program the capture signal — note length, offsets, velocity — and -hear it before committing the bake. - -**Consolidates item 15 (the popup sub-feature).** - -**Surface boundary — owns:** `core/instrument/ui/curve_popup`'s sibling — a new pure popup -geometry module for the capture-signal sheet (mirror the `curve_popup` precedent: centered -sheet, width/height clamps, title row, Close button rect, outside-sheet dismissal test) — -plus the editor's popup paint/input wiring in the deck or chrome band, and the preview -trigger's call into W2's render path. Reads Ξ-W1-T2's note-program record; does not -re-own it. - -**Behavior.** -- **A popup menu programs the capture signal:** **note length**, **start and end offsets — - in ms AND in beats** — and **velocity**. -- **A preview trigger button** auditions the capture note exactly as currently programmed - — the user hears the bake before committing it — and the offline pass renders that same - programmed performance. -- **Note length is a musical-division picker** spanning 1/64th to 64/1 with dotted and - triplet multipliers. -- **Offsets are anchored** — start to note-on, end to note-off — and each is readable and - editable in **both** ms and beats, the two views of one stored value. -- **Beat-denominated values resolve against the project tempo under the cursor.** The - shell reads the tempo; the arithmetic is Ξ-W1-T2's. -- **The programmed velocity is the render velocity** — material because the velocity - transfer curves modulate amp, pitch, and filter at that velocity. -- House-consistent: drawn through the shared kit by palette role, geometry pure and - CTest-covered, no decorative animation. - -**Acceptance criteria.** -- The popup exposes: note length as a musical-division picker spanning **1/64th to 64/1 - with dotted and triplet multipliers**; start and end offsets, **each readable and - editable in both ms and beats**, anchored to note-on and note-off respectively; and - velocity. -- **Its preview trigger auditions the capture note exactly as programmed, and the bake - renders that same programmed performance — preview and bake cannot diverge.** Verify - structurally (one render path), not just by ear. -- **Beat-denominated values resolve against the project tempo under the cursor:** the same - programmed division yields a correspondingly different rendered duration when the tempo - at the cursor differs. -- The programmed signal persists with the instance and round-trips save/reload. -- The popup's geometry and dismissal test are pure and unit-tested; no hit-test math lands - in shell code. - -**Open questions.** Both of Ξ-W1-T2's residuals (negative offsets; the note-length -denomination seam) surface here as UI consequences — if T2 answered them, this track -implements the answer; if T2 deferred either, this track is where it becomes visible and -must be closed. +**Ξ-W3-T1 has landed** — `capture-signal-popup` — see `docs/COMPLETED.md` for the full +narrative, and it diverges substantially and deliberately from this section's spec. The +popup this track was scoped to build (a musical-division note-length picker, ms/beat- +editable offsets, velocity, and a preview trigger) was built (~1500 lines) and then +**abandoned unmerged** on Daniel's ruling: *"I didn't realize you had already derived a +usable window. The manual stuff for baking a specific midi length was just an idea, if we +have a smarter, fewer-clicks way of doing it, that is ideal. I just don't want to lose +anything when we bake. We can abandon the whole parameterized bake window if we can +safely derive the window in gate and trigger modes."* An audit, backed by executable +tests, established the window derives losslessly everywhere except Gate over an active +sustain loop, which has no intrinsic duration to derive. What shipped instead: the bake +window derives itself in Trigger and loop-less Gate; one control, a musical-division +**Hold** picker, covers the one irreducible case and is shown only for Gate-with-active- +loop (`bakeWindowNeedsHold`, reading the engine's own loop fold); bake velocity now reads +the instance's persisted preview velocity rather than a hard-coded value; and the +chrome-row play button stays a pure MIDI trigger — Daniel's ruling: *"play button is pure +MIDI trigger, Bake parameters are their own thing."* **This retires this section's +acceptance criterion 2 ("preview and bake cannot diverge") and its preview-trigger +behavior bullet by ruling, not by shortfall** — there is no popup and no preview trigger. +Three pre-existing truncation bugs were found and fixed along the way, and +`note/CLAUDE.md`'s musical-division-only invariant is amended (see Ξ-W1-T2 above): a note +length now carries either an exact derived duration or a musical division, not +division-only. --- @@ -682,7 +661,7 @@ 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 **13** (`map/component_state_io.h:154`) and Ξ-W2-T1 was specced to +`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 @@ -692,9 +671,10 @@ take no rung — but the plan's prediction is not evidence. So: > 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 v14 / v15 / v16-reserved.** If Ξ-W2-T1 landed -> a bump, every number shifts by one and **nothing else about the ownership changes** — which -> is the whole point of stating it relatively. +> **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), @@ -2769,7 +2749,7 @@ The check that nothing was dropped. Every row points at a track that exists abov | 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` | | 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` | | 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` | -| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` | +| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup abandoned by ruling; window derives itself) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` | | 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` | | 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` | @@ -2870,8 +2850,8 @@ Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W T2 note-program-model ...................... 15 (model) W2 The bake chain [ran AHEAD of Γ; its reset list is corrected by Γ-W3-T2] T1 resample-bake-chain ..................... 15 (chain) - W3 The capture-signal popup - T1 capture-signal-popup .................... 15 (popup) + W3 The capture-signal popup [popup abandoned by ruling; bake window derives itself] + T1 capture-signal-popup .................... 15 (popup abandoned; window derives) Phase Γ — The instrument's control surface (none of the seventeen; ends with VST3 params) W1 Foundations [5 tracks, disjoint by surface]