From 2fa55658c12f763fc7a67ef32fb150f6c6ecc99a Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 1 Aug 2026 17:26:18 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20schedule=20VST3=20parameters=20into=20P?= =?UTF-8?q?hase=20=CE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automation ships as Γ-W4; the stage ceiling goes to 10 s in W1-T1 ahead of the one-way door; a new Γ-W3-T2 corrects Ξ's bake reset list. Four waves, ten tracks. Opens Γ-F7 on parameter order. --- docs/PLAN.md | 781 +++++++++++++++++---- docs/TODO.md | 16 +- docs/product/instrument-control-surface.md | 188 ++++- docs/product/parameter-automation.md | 495 ++++++++++++- 4 files changed, 1278 insertions(+), 202 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 741d325..14afbbb 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -48,7 +48,15 @@ with a correction to the analysis, not merely a ruling**: dynamic reported laten for VST3 instruments and REAPER handles it as a matter of course; what makes the mandated restart expensive *here* is self-inflicted (`setActive(true)` calls `reloadInstrument`), so the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed -around. **No track in this plan carries an unanswered [Daniel]-class question.** +around. + +**Γ-F3 was subsequently REVERSED and a seventh fork opened, both by Daniel's later rulings +of 2026-08-01.** Γ-F3 (*"the stage-time ceiling stays 2.0 s"*) is replaced by *"extend the +stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. And **Γ-F7 is OPEN**: the VST3 +parameter *order* (signal flow, or the editor's visual rows), a forever commitment that +cannot be closed by proposal at review. It blocks nothing until **Γ-W4 dispatches**, which is +the last track of the last wave of the phase. **Exactly one track in this plan carries an +unanswered [Daniel]-class question: Γ-W4-T1, and it is Γ-F7.** ### Flagged for awareness — not blocking, but decision-grade @@ -62,23 +70,39 @@ around. **No track in this plan carries an unanswered [Daniel]-class question.** 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. -2. **Phase Γ must land before Ξ-W2, and this is a correctness point, not a preference.** - Ξ-W2's settled reset scope enumerates parameters by name; Γ adds rate, pitch offset and - the limiter flag, so shipping the bake first means its reset list is incomplete on the - day it lands. Γ's product doc pre-classifies all three against the ratified rule (all - **reset**), so this costs no Daniel decision — only ordering. Second, weaker reason: Γ - owns params-payload v14 and v15, and Ξ-W3's programmed-signal persistence will want the - next rung; two phases contending for the ladder is the fight Θ's organizing constraint - exists to avoid. +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 + correct phase xi inside gamma. wasn't a choice."* Ξ-W2-T1 (`resample-bake-chain`) ran + ahead of this plan's sequencing claim, so the bake's settled reset scope — which + enumerates parameters **by name** — ships incomplete: it cannot name rate, pitch offset or + the limiter flag, none of which existed when it was written. -3. **The taper work inside Γ-W1-T1 is a one-way door with respect to automation.** Once - VST3 parameters exist, the taper *is* the host-facing normalization, and re-tapering - re-interprets every recorded automation point in project files we do not own and cannot - migrate. Re-tapering is free today and permanently expensive afterwards. See - `docs/product/parameter-automation.md` §4 — that doc is scoping only, nothing in it is - scheduled here. **The same door applies to the stage-time ceiling**, which Γ-F3 left at - 2.0 s with a 10 s ambition recorded in `docs/TODO.md`: if that ceiling is ever raised, it - wants to happen before the parameter system, not after. + **This is no longer a scheduling constraint to honour. It is a correction obligation with + a named owner: Γ-W3-T2 `bake-reset-amendment`.** The classification costs no Daniel + decision — `docs/product/instrument-control-surface.md` §3.4 pre-classifies all three + against Ξ-W2's own ratified rule (all **reset**) — but the amendment must be written + **against what Ξ-W2-T1 actually shipped, not against what this plan predicted it would + ship.** Ruling 1 adds a second correction of the same shape, homed on Γ-W4-T1 rather than + here: see item 3. + +3. **The one-way doors are now IN-PHASE, and the sweep for them is a delivered artifact.** + Ruling 1 (Daniel, 2026-08-01) schedules VST3 parameter reporting **inside Phase Γ**, as + Γ-W4-T1. Everything that participates in a parameter's normalization therefore freezes at + the end of this phase rather than at the start of some later one, and anything that ought + to move must move first. + + - **The taper** (Γ-W1-T1) — known, and the reason this phase was ordered as it was. + - **The stage-time ceiling 2.0 → 10.0 s** (Γ-W1-T1) — Γ-F3 **reversed** by Daniel's + *"extend the stage lengths to 10s."* A range endpoint is normalization exactly as much + as the curve between the endpoints is. + - **Two further doors that need action, both new**, both landing on Γ-W1-T1: every + default must have an **exact normalized preimage** (a host's reset-to-default has no + `resetDeckParam` bypass to use), and the filter's four `*Norm` controls **must not be + re-tapered** (their laws are already wire-frozen in payload v9). + - **Six more constants freeze without needing to change**, and three are already frozen + for unrelated reasons; the complete sweep, with dispositions and with what was checked, + is `docs/product/parameter-automation.md` §8. **That doc is no longer scoping-only — + §§6–10 are the specification Γ-W4-T1 is built from.** ## Phase-wide acceptance criteria @@ -400,6 +424,12 @@ stays musical-division-only, and an offset stores the denomination it was entere computable (T1) and the programmed-note record the offline pass renders (T2). **Also depends on all of Phase Θ** — see the phase note above. +**This wave ran AHEAD of Phase Γ, and that was not a choice.** The plan asserted Γ must land +first so the bake's reset list would be complete on the day it shipped; Ξ-W2-T1 was already +live. The consequence is owned, not absorbed: **Γ-W3-T2 `bake-reset-amendment`** completes +the list afterwards, and **Γ-W4-T1** adds the host-notification obligation once parameters +exist. Neither is this wave's work, and neither is a defect report against it. + **One track.** The bake is one gesture and one chain; the architecture decision at its head governs every step after it. @@ -567,6 +597,11 @@ DEGRADED, escalate rather than improvise — the fallback is (1c) + (2a). review. Not a new Daniel call. (Θ adds: curve exponents → reset; spline contours → reset, already named; the filter's velocity/key-tracking mod → reset with the filter; loop crossfade → reset with the loop points; the Staged/Spline mode flag → classify.) + **Phase Γ's additions are NOT this track's** — this track ran ahead of Γ, so the values Γ + introduces (rate, pitch offset, the limiter flag, the loop enable) are amended in by + **Γ-W3-T2 `bake-reset-amendment`** afterwards, against what this track actually shipped. + Γ-W4-T1 then adds the host-notification obligation over the completed list. Nothing here + needs to anticipate either. - **Naming and lineage [propose, jointly with Ξ-W1-T1's lineage-record question].** When add-distinct fires, the new capture needs a display name (derived from the original?), and the bank some way to read iteration lineage across repeated bakes. One proposal, @@ -644,23 +679,31 @@ must be closed. **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, a fix for staged contour traces drawing straight, and a -re-approached loop/crossfade marker UX under an explicit chrome-row loop enable. +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`**. 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. +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. -**Fork state — all six forks are ruled; nothing in this phase awaits Daniel.** Indexed at -spec §8, folded into the tracks below: +**Fork state — six ruled, ONE OF THEM LATER REVERSED, and one 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** — the stage-time ceiling stays **2.0 s** in this phase. The 10 s ambition is - carried in `docs/TODO.md` with its rationale and its prerequisites. +- **Γ-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 @@ -669,6 +712,26 @@ spec §8, folded into the tracks below: 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 — OPEN, and it is the only unanswered [Daniel] question in this plan.** The VST3 + parameter **order**: signal-flow order (the deck's own documented rule, layout-independent) + or the editor's visual row order after the reflow. Same membership, different sequence, + frozen forever the day parameters ship. **Recommendation: signal flow**, because the visual + layout has already moved twice and this phase moves it again. Reasoning and both arguments: + `docs/product/parameter-automation.md` §6.4. **Blocks nothing until Γ-W4 dispatches**; it + cannot be closed by proposal at review. + +**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. Three 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 (§6.2, §6.3); and +the exposed list is **derived from the three-state commit predicate**, never hand-maintained +(§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; @@ -682,27 +745,38 @@ ours to reduce if it ever matters, and the reduction is decoupling reload from a not abandoning dynamic latency.** That improvement is filed as a `docs/TODO.md` entry with its trigger condition; it is not scheduled in this phase. -**Sequencing against Phase Ξ — Γ runs BEFORE Ξ-W2.** Two reasons, both the same shape as -Ξ's own stated gate: -1. **The bake bakes the control surface.** Ξ-W2's settled reset scope enumerates parameters - by name; Γ adds rate, pitch offset and the limiter flag. Shipping Ξ-W2 first means its - reset list is incomplete on the day it lands. (Γ's doc §3.4 pre-classifies all three - against the ratified rule — all **reset** — so this is a sequencing point, not a new - Daniel question.) -2. **One params-payload ladder.** Γ takes v14 and v15. Ξ-W2 does not currently bump the - payload, but Ξ-W3's programmed-signal persistence will, and two phases contending for the - ladder is exactly the fight Θ's organizing constraint calls out. +**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: -**The organizing constraint.** Five surfaces are single-writer and dictate the wave shape: -`ui/deck_values.cpp` (the taper law, then the two new controls), `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), 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. +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. -**Resequenced 2026-08-01 (Daniel), two changes.** The prior four-wave shape put the reflow at -W3 and the Preserve stretcher at W4; both moved. +**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, @@ -718,13 +792,51 @@ W3 and the Preserve stretcher at W4; both moved. 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 params-payload ladder is unchanged by the resequencing, and re-checked against the new -membership.** Exactly one bump per wave, owned by exactly one track: **W1-T2 owns v14** (the -limiter flag), **W2-T1 owns v15** (rate + pitch offset), and **every other track in the phase -owns no rung** — W1-T4 changes no persisted field, W1-T5 adds no parameter, W2-T2's loop -enable maps onto the already-persisted `SampleLoop::hasLoop`, and W3-T1 is layout only. The -ordering still works because v14 lands a whole wave before v15, and neither of the two tracks -that moved touches the codec. +**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 `isLiveDeckParam` 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 **13** (`map/component_state_io.h:154`) 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 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. + +**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 @@ -738,11 +850,37 @@ exact interim layout; do not "fix" it in a track that does not own it. `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 (Γ-W1-T1) changes needle angles only — the payload stores raw - engine doubles, so saved values reload bit-identical. + 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. + 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.** Γ-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` (Γ-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. +- **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. +- **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. +- **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 `isLiveDeckParam` / `liveCommitFor`, and no list that can drift from it. - **The window floor is 1190 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 sets it, in wave 1; no other track in the phase may move it**, and from that point every track is authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun. @@ -766,23 +904,43 @@ against this membership rather than carried over from the four-wave shape: | Track | Owns | |---|---| -| **T1** `knob-interaction-law` | `ui/deck_values`, `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, the shared modifier helper in `editor_internal.h` | -| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**v14**) | -| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **pure** tessellation helper | +| **T1** `knob-interaction-law` | a **new pure taper module** under `core/instrument/ui/`, `ui/deck_values`, `ui/envelope_overlay` + `ui/envelope_edit` (the AHDSR schematic axis and its drag inverse), `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, `editor_controls.cpp`'s `envClampBounds` only, the shared modifier helper in `editor_internal.h` | +| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**the wave's payload rung**) | +| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **new pure** tessellation module | | **T4** `editor-floor-and-row-law` | `ui/sample_bands.h` (the floor), `ui/knob_deck.h` (budget constants + two invalidated header notes), `ui/deck_groups` (the row predicate **only**), five test fixtures | | **T5** `preserve-time-stretch` | `engine/pitch_shift` + a new pure stretcher module, `engine/voice.{h,cpp}`'s Preserve read path | -**One shared file in the wave, named rather than discovered at merge: -`src/core/instrument/engine/CMakeLists.txt`.** T2 declares two new pure libraries and their -test targets there; T5 declares one. Both are append-only additions in separate blocks — -**textual merge adjacency, not semantic contention.** Whichever lands second rebases. +**Two shared files in the wave, named rather than discovered at merge.** +`src/core/instrument/engine/CMakeLists.txt` — T2 declares two new pure libraries and their +test targets there, T5 declares one. **And, newly, `src/core/instrument/ui/CMakeLists.txt`** — +T1 declares the taper module and its test target, T3 declares the tessellation module and +its. All four are append-only additions in separate blocks — **textual merge adjacency, not +semantic contention.** Whichever lands second rebases. -**Two near-misses that are avoided by construction, and must stay avoided.** (a) T3's -tessellation helper **lands pure** (`ui/envelope_overlay` or a new pure module), *not* in -`editor_internal.h`, which T1 is editing — this also satisfies the phase's geometry-stays-pure -criterion, so it costs nothing. (b) T4 touches `deck_groups` but adds **only** the new row -predicate; it does not touch `sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and -it does not touch `deck_values`, which is T1's. +**Three near-misses that are avoided by construction, and must stay avoided.** + +(a) **T3's tessellation helper lands in a NEW pure module — explicitly NOT +`ui/envelope_overlay`, which T1 now owns**, and not in `editor_internal.h`, which T1 is also +editing. The prior wording offered `envelope_overlay` as an option; Ruling 2 removed it, +because T1's schematic-axis work rewrites that module's whole time→x map. This still +satisfies the phase's geometry-stays-pure criterion, so it costs nothing. + +(b) **T1 and T3 are disjoint by file but coupled by data, and the coupling has a stated +resolution.** T1 owns where an AHDSR's vertices *land*; T3 owns the stroke *between* +vertices. T3's tessellation is over φ across a segment's pixel span, so the tapered axis +changes nothing about the curve it draws — **but T3's tests must assert against the returned +vertices, not against absolute pixel literals**, or they break when T1 lands. Whichever +track lands second rebases; expressing T3's assertions relatively makes that rebase free. + +(c) **T4 touches `deck_groups` but adds only the new row predicate**; it does not touch +`sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and it does not touch +`deck_values`, which is T1's. + +**Two consumption boundaries worth stating, because they look like collisions and are not.** +T1 **consumes** `engine/master_gain`'s dB taper for its whole-dB snap and does not edit it; +T2 does not touch it either. And T2's payload rung is the wave's only format change — +T1's taper and ceiling changes are persistence-neutral by construction (the payload stores +raw engine doubles). **Both of the phase's DSP unknowns are in this wave** — T2's limiter and T5's stretcher. That is deliberate: they are the two tracks whose gate can fail, and failing in wave 1 is @@ -791,16 +949,38 @@ recoverable in a way that failing in the last wave is not. #### Γ-W1-T1 — `knob-interaction-law` **Goal.** One consistent, unit-category-driven interaction and taper rule across every -variable control, landed **before** any new control is added so the new ones are authored -into it rather than retro-fitted. +variable control, **over a stage-time range raised 2 s → 10 s**, landed **before** any new +control is added so the new ones are authored into it rather than retro-fitted — and before +any parameter is declared, so the law is what the host is handed rather than something the +host has to be reconciled with later. -**Spec:** `docs/product/instrument-control-surface.md` §4. +**Spec:** `docs/product/instrument-control-surface.md` §4, **§4.3.1 (the 10 s ceiling and the +overlay-legibility design — new, read it before scoping this track)**, and +`docs/product/parameter-automation.md` §8 (the one-way-door sweep this track discharges). -**Surface boundary — owns:** `core/instrument/ui/deck_values` (the taper maps, the -snap-unit table, `resetDeckParam`), `core/instrument/ui/param_slider` (the drag law), -`shell/instrument/editor_input_*` (modifier read + re-anchor), and the modifier-reading -helper the three input paths share. **Does not own** any deck descriptor, any parameter, or -the waveform painter. +**Surface boundary — owns:** a **new pure taper module** under `core/instrument/ui/` (the +ms/semitone/exponent maps, extracted so they have one home), +`core/instrument/ui/deck_values` (the bindings, the snap-unit table, `resetDeckParam`), +`core/instrument/ui/envelope_overlay` (the ceiling constant **and** the AHDSR schematic +axis) and `core/instrument/ui/envelope_edit` (its drag inverse), +`core/instrument/ui/param_slider` (the drag law), `shell/instrument/editor_input_*` +(modifier read + re-anchor), `shell/instrument/editor_controls.cpp`'s `envClampBounds` +**only** (it reads `kEnvTimeMaxSeconds`), and the modifier-reading helper the three input +paths share. **Does not own** any deck descriptor, any parameter, the waveform painter, or +`engine/master_gain` (consumed, not edited). + +**Why this track does NOT split, asked and answered.** Ruling 2 makes it materially bigger — +tapers, modifiers, re-anchor, reset bypass, the ceiling, and the overlay's schematic scale. +Two splits were considered and both are **serial, not parallel**, so neither buys any +concurrency: an *interaction* half (modifiers, snap, re-anchor) needs the *domain* half's +taper and snap-unit table to exist first; and a standalone *overlay-axis* track needs the +taper module and the final ceiling before it can define a stage's slot width. Splitting +would therefore cost a wave and gain nothing, while putting the single most +identity-critical function in the phase across a wave boundary — the same function the host +will normalize against three waves later. **The seam that matters is internal and is a +deliverable: the taper is extracted into its own pure module**, which is what makes "the +taper IS the host-facing normalization" structurally true rather than a comment. The +~600-line ceiling is a per-file bar, and the extraction is what keeps every file under it. **Behavior.** - **Shift snaps to whole numbers in the control's displayed unit**; **Ctrl scales the drag by @@ -817,9 +997,47 @@ the waveform painter. `(1 − 0.05) ×` the accumulated delta. - **Millisecond knobs become log-scaled.** Exactly 0 s at norm 0 and exactly `kEnvTimeMaxSeconds` at norm 1, monotone throughout; **10 ms lands within 0.12–0.20 of - travel and 100 ms within 0.42–0.52**. The ceiling stays **2.0 s** — it reads - `kGateStageMaxSeconds`, which the AHDSR overlay's schematic scale is derived from, and the - two must agree. + travel and 100 ms within 0.42–0.52**. +- **The stage-time ceiling moves 2.0 s → 10.0 s** (Daniel, reversing Γ-F3): + `kGateStageMaxSeconds` (`envelope_overlay.h:85`) and, through it, `kEnvTimeMaxSeconds` + (`deck_values.h:22`). **The two move together or not at all** — `deck_values.h` reads the + overlay's constant rather than restating it precisely so they cannot drift + (`deck_values.h:19-22`). The taper's landmarks above are fit against the **new** ceiling, + which is why the ceiling cannot be a follow-up: fitting the taper twice is the only other + way to get there. +- **`resetDeckParam`'s bypass becomes MANDATORY rather than merely required-anyway.** + `deck_values.h:42-46` records that exact default recovery depends on the ceiling being a + power of two; **2.0 is, 10.0 is not**, and the log taper compounds it. Nothing here may be + "simplified" back into a norm round-trip under any circumstance. +- **NEW, and the sharpest requirement in the track: every default must have an EXACT + normalized preimage under its taper.** `ParameterInfo::defaultNormalizedValue` (Γ-W4-T1) + is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)` — and **the + host has no `resetDeckParam` bypass to use**. The bypass fixes the editor's reset and + cannot fix the host's; only exactness in the map itself makes the two land on the same + value. This binds the taper's *shape*, so it belongs here and cannot be handed forward. + Master gain's unity (≈ 0.714 norm) is the case where a hair off is audible. +- **The AHDSR overlay's schematic axis becomes the taper — the ceiling's real cost, and it + is design work, not a constant change.** Each of the four timed stages gets an equal slot + and today maps seconds across it linearly (`gatePxPerSecond`, + `envelope_overlay.cpp:33-34`). At 2 s a 30 ms attack is 1.5 % of its stage's domain; **at + 10 s it is 0.3 %, under a pixel at the floor width.** The fix: a stage's slot width becomes + `slotPx × taperNorm(seconds)` instead of `slotPx × seconds / ceiling`, so a node's position + within its slot **is** its knob's needle position. Legibility becomes ceiling-independent by + construction; the one-model invariant gets stronger rather than strained; and **the drawn + curve is unaffected**, because the taper decides only where a stage's end node lands while φ + still runs linearly across the stage's pixel span — so Γ-W1-T3's φ^p trace composes with it + rather than fighting it. **The AHD policy is untouched**: an AHD maps 1:1 onto the + waveform's own PCM-aligned time axis and stays linear in seconds. Two alternatives + (content-fit auto-scale; a minimum drawn stage width) were considered and rejected — spec + §4.3.1 names why, and neither is to be reintroduced as a "simplification." +- **`envelope_edit`'s drag inverse must remain the EXACT inverse of the draw.** Both read the + same taper module; a node dragged to a pixel and the knob's value at that pixel are one + number, not two that agree. +- **The taper is EXTRACTED into its own pure module**, with its own `_tests` target, + because it now has three consumers in two different dependency layers: `deck_values` (which + sits above `envelope_overlay`), `envelope_overlay`/`envelope_edit` (which sit below it), + and — from Γ-W4-T1 — the host. Leaving it inside `deck_values` would force an inverted + include edge. **Do not solve that by copying the map.** - **Semitone knobs become log2/centre-expanded.** Symmetric, exactly 0 at centre, exactly ±`kPitchDepthMaxSemis` at the ends, monotone; **±7 st reached at 50–58 % of each half-travel**. @@ -842,17 +1060,48 @@ the waveform painter. - Double-clicking any knob (outer ring and inner dial independently) lands **exactly** on its default at every taper, verified against a default-constructed `PlaySeconds` rather than a round trip. -- The log/log2 landmark positions above are asserted in `deck_values`' own tests. +- The log/log2 landmark positions above are asserted in the **taper module's** own tests, and + hold at the **10 s** ceiling — the fit is against the new ceiling, not the old one. +- **Every default round-trips exactly through `norm → value`**, asserted per unit category + against a default-constructed `PlaySeconds` and against `master_gain`'s unity. This is the + criterion Γ-W4-T1 will declare `defaultNormalizedValue` from; it fails here, not there. +- **A stage time of several seconds is reachable by hand with no loss of resolution below + 100 ms**, and `kEnvTimeMaxSeconds == kGateStageMaxSeconds` is asserted, not assumed. +- **A project saved at the 2 s ceiling reloads with identical stored seconds and identical + audio** — the ceiling change is persistence-neutral for the same reason the taper is. +- **The AHDSR overlay reads legibly at both ends of the new range**: a default 3 ms attack is + a visible, grabbable node at the floor width, and a 10 s decay still lands its end node at + its slot's edge. Assert the node separation, then judge the result by eye in the DAW. +- **The overlay's drag inverse is the exact inverse of its draw** at the tapered axis — + `nodeAtPoint` / `resolveNodeDrag` and `buildEnvelopePolyline` round-trip. +- **The AHD 1:1 policy is unchanged**, asserted: a sustain-less envelope's x-axis stays + wall-clock over the waveform. +- **The taper module is pure, CTest-covered, and is the ONLY definition of each map** — a + grep finds no second copy in `deck_values`, `envelope_overlay`, or the shell. +- **The filter's four `*Norm` controls are untouched by the taper pass** — cutoff, Q, morph + and drive are already wire-frozen in payload v9; a regression baseline proves their audio + is unchanged. - One shared modifier-read helper serves all drag surfaces; no second modifier grammar exists. -**Open questions.** None **[Daniel]** — fork Γ-F3 is ruled: **the ceiling stays 2.0 s.** The -10 s ambition Daniel described (*"a horrifically long decay with tight exp"*) is carried as a -`docs/TODO.md` entry, and **this track lands both of its prerequisites**: the log taper (which -is what makes a higher ceiling usable at the low end rather than unusable) and the reset -bypass (which retires the power-of-two dependency — 2.0 is a power of two, 10.0 is not). -**Neither is optional on that basis alone** — they are already required by this track — but -the engineer should know the reset bypass is doing double duty, and should not "simplify" it -back into a norm round-trip. +**Open questions.** +- **No [Daniel] questions.** Fork **Γ-F3 is REVERSED**: the ceiling moves to **10.0 s, in this + track.** Daniel's *"a horrifically long decay with tight exp"* is the case it serves, and + the `docs/TODO.md` entry that carried it is discharged rather than deferred again. **The + reversal's cause is Ruling 1** — a range endpoint is host-facing normalization, free to + move now and permanently expensive after Γ-W4-T1. Both of the prerequisites the deferred + entry named are in this track anyway: the log taper (which is what makes a higher ceiling + usable at the low end rather than unusable) and the reset bypass (which retires the + power-of-two dependency — 2.0 is a power of two, 10.0 is not). The engineer should know the + reset bypass is now doing triple duty and must not be "simplified" back into a norm + round-trip under any circumstance. +- **[propose at review]** the exact shape of the taper, subject to the landmark bounds **and** + the exact-default-preimage requirement. Those two together are tighter than either alone, + and the second is easy to satisfy by accident and easy to lose in a refactor — **assert it, + do not observe it.** +- **[propose at review]** whether the tapered schematic axis wants a visible tick or + gradation cue, now that it is no longer linear in time. The plan's lean is **no** — the ms + labels carry the number and the editor's no-decoration policy stands — but a reader who + finds the axis illegible in the DAW should say so rather than silently adding one. #### Γ-W1-T2 — `master-bus-audio` @@ -867,9 +1116,12 @@ first; it was rewritten when Γ-F6 closed, so an older reading of it is wrong)** **Surface boundary — owns:** a new pure limiter module and a new pure meter-ballistics module under `core/instrument/engine/` (each with its own `_tests` target), `shell/instrument/reasampler_processor` (the chain, the published block state, **and the -`getLatencySamples` / `restartComponent(kLatencyChanged)` path**), and **params payload v14** -(the limiter enable flag). **Does not own** MASTER's deck geometry or any drawing — that is -Γ-W3-T1. +`getLatencySamples` / `restartComponent(kLatencyChanged)` path**), and **the phase's FIRST +params-payload rung** (the limiter enable flag). **Does not own** MASTER's deck geometry or +any drawing — that is Γ-W3-T1. **Read `kParamsPayloadVersion` on `dev` and take the next rung +above it rather than assuming the number** — Phase Ξ ran ahead of this plan's sequencing, so +the plan is not the record of what the ladder currently carries. On `dev` as of 2026-08-01 +that resolves to **v14**. **Behavior.** - **Chain:** `voice mixer → master gain (existing ramped multiply) → limiter (bypassable) → @@ -936,8 +1188,8 @@ module under `core/instrument/engine/` (each with its own `_tests` targe - **Meter ballistics (pure, unit-tested):** instantaneous rise; **fall 20 dB/s**; peak-hold latched at the running max, **held 1.5 s**, then falling at the same rate; scale **linear in dB over −60…+6 dBFS**; clip latches at block peak ≥ 0 dBFS and is cleared on request. -- **`ComponentState` payload v14** appends the limiter flag as a strict suffix on the existing - discipline; a v13 blob is a strict prefix and lifts to bypassed. +- **The `ComponentState` payload rung** appends the limiter flag as a strict suffix on the + existing discipline; the preceding version's blob is a strict prefix and lifts to bypassed. **Acceptance criteria.** - **With the limiter bypassed the rendered output is byte-identical to the pre-change build**, @@ -982,8 +1234,16 @@ mid-segment knot stops floating off its own trace. **Spec:** `docs/product/instrument-control-surface.md` §5. **Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s staged-envelope -trace and any pure tessellation helper it needs. **Does not own** the loop/crossfade marks -(Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace. +trace and a **new pure tessellation module** for it. **Does not own** the loop/crossfade +marks (Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace. + +**The helper's home is now constrained, not a choice.** `ui/envelope_overlay` was previously +offered as a candidate home for the tessellation helper; **Γ-W1-T1 now owns that module** +(the AHDSR schematic axis, per Ruling 2), so the helper lands in a **new** pure module under +`core/instrument/ui/`. T1 also owns where an AHDSR's vertices land — this track owns only the +stroke *between* vertices, and tessellates over φ across a segment's pixel span, so the +tapered axis changes nothing about the curve drawn. **Express this track's assertions against +the returned vertices, not against absolute pixel literals**, and the rebase onto T1 is free. **Behavior.** The defect is verified: `editor_paint_waveform.cpp:218` drops knots (`if (v.knot) continue;`) and joins the remaining vertices with straight strokes, and @@ -1218,10 +1478,18 @@ are otherwise single-writer surfaces, so it is stated rather than discovered at track lands second rebases onto the first. No *new* in-wave adjacency was created by the resequencing: T2 touches neither `deck_values` nor `deck_groups` nor `voice`. +**One cross-wave hand-off, new with Ruling 2 and NOT a contention.** Rate's taper — linear in +semitones over ±12, the stated exception to the centre-expansion law — belongs in the **taper +module Γ-W1-T1 extracts**, since that module is the one home of every map. T1 therefore +appends a law to a module a previous wave created. Serial across waves by construction, the +same shape as its `engine/voice.{h,cpp}` hand-off from W1-T5. **What would be wrong is a +second taper defined inside `deck_values`' binding** — one home, appended to, not forked. + **The format ladder stays clean.** The loop enable maps onto the existing `SampleLoop::hasLoop`, which is already persisted and whose `start`/`end` are already written -unconditionally — **no new field, no version bump** — so T1 keeps sole ownership of payload v15 -exactly as specced. +unconditionally — **no new field, no version bump** — so T1 keeps sole ownership of **the +phase's second payload rung** exactly as specced (v15 on `dev` as of 2026-08-01; read the +ladder rather than assuming the number). #### Γ-W2-T1 — `pitch-rate-deck` @@ -1232,7 +1500,8 @@ existing Varisp|Presrv toggle, with both new controls wired through the engine. **Surface boundary — owns:** `core/instrument/engine/play_params.h` + `core/instrument/map/play_seconds.h` (the two new fields), -`core/instrument/map/component_state_io` + `params_payload` (**payload v15**), +`core/instrument/map/component_state_io` + `params_payload` (**the phase's SECOND payload +rung** — v15 on `dev` as of 2026-08-01; read the ladder, do not assume the number), `core/instrument/engine/voice.{h,cpp}` (the compounding and the note-on latch), `core/instrument/ui/deck_groups` (the PITCH/RATE descriptor **and** the three-state live predicate), `core/instrument/ui/deck_values` (the two new bindings). **Does not own** the @@ -1273,8 +1542,12 @@ by the time this track runs). caption row overtakes the 180 px knob row and the group exceeds 192). If the text will not fit at 80, narrow the `Varisp|Presrv` segments 48 → 44 (ceiling becomes 88) — **do not widen the group**. -- **Payload v15** appends both fields as a strict suffix; a v14 blob lifts to rate 100 % / - pitch 0 st, bit-identical playback. +- **The payload rung** appends both fields as a strict suffix; the preceding version's blob + lifts to rate 100 % / pitch 0 st, bit-identical playback. +- **Both new `DeckParam`s are classified in the three-state predicate, and that classification + is what puts them in the VST3 parameter list three waves later** — Rate `NoteOnLatched`, + Pitch `Live`. Γ-W4-T1 derives the exposed set from this predicate rather than from a list + of its own, so a mis-classification here is a mis-declared parameter there. **Acceptance criteria.** - Rate at 50 % plays an octave down and half speed under Varispeed; at 200 %, an octave up and @@ -1293,7 +1566,8 @@ by the time this track runs). waveform marker. - The PITCH/RATE group measures **exactly 192 px**; adding the two `DeckParam`s produces a compile error in `isLiveDeckParam`'s exhaustive switch until they are classified. -- A v14 project reopens at rate 100 % / pitch 0 st and sounds identical. +- A project saved at the preceding payload version reopens at rate 100 % / pitch 0 st and + sounds identical. **Open questions.** - **None [Daniel].** @@ -1449,21 +1723,40 @@ track changes drawing, hit-testing and one editor-state retention rule — no fo --- -### Γ-W3 — The reflow +### Γ-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. +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. **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 this track **consumes rather than re-derives**. +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. -**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. +**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. #### Γ-W3-T1 — `deck-reflow` @@ -1572,6 +1865,226 @@ waveform band. 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). +#### Γ-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. + +**Consolidates:** nothing from the seventeen. It is a **correction obligation**, not a +feature (see "Flagged for awareness" item 2). + +**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.** + +**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.** + +**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. + +**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. + +**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. + +--- + +### Γ-W4 — VST3 parameters + +**Depends on every earlier wave, and each dependency is a hard prerequisite rather than a +courtesy:** + +1. **← W1-T1.** The taper and the 10 s ceiling **are** the host-facing normalization, and + W1-T1 is also what extracts them into the one pure module the host reads through. Declaring + parameters against a taper that is still moving is the one-way door this whole phase is + ordered around. +2. **← W1-T2 and W2-T1.** Every control that could be a parameter must exist before the list + is declared. The list is derived from the control inventory; an inventory still growing + produces a list that has to be re-frozen, and it cannot be. +3. **← W2-T1 specifically.** `isLiveDeckParam` becoming three-valued is the *prerequisite* of + the classification, not an incidental: the exposed set is exactly `Live ∪ NoteOnLatched`. +4. **← W3-T1.** MASTER's inventory (limiter toggle, GR bubble, reserved cell) is the last + change to what controls exist at all. +5. **← W3-T2.** The bake's reset list must already be complete, so this track adds the + host-notification obligation once rather than amending an amendment. + +**One track.** The storage decision governs every part of the work — the projection rule, the +migration path, what `getParamNormalized` returns, and what the bake's reset must do — exactly +as Ξ-W2-T1's crossing decision governs its chain. Every candidate split (a pure +model/classification half and a host-wiring half) is **serial**, so it buys no concurrency and +puts the decision on one side of a boundary and its consequences on the other. + +#### Γ-W4-T1 — `vst3-parameter-set` + +**Goal.** The instrument reports its automatable parameters to the host, under a frozen id +contract and a logical order — which also hands it REAPER's whole per-parameter modulation +block (LFO, envelope follower, MIDI link, parameter linking) for free. + +**Consolidates:** nothing from the seventeen. **Ruling 1** (Daniel, 2026-08-01): *"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."* + +**Spec:** `docs/product/parameter-automation.md` — **§§6–10 are the specification; §§1–5 are +the analysis behind it.** Read §6.1 (storage), §6.3 (the freeze), §7 (the classification) and +§8 (the one-way-door sweep) before scoping. **Today the plugin has zero parameters:** +`ReaSamplerProcessor::initialize` (`reasampler_processor.cpp:56-73`) never populates +`SingleComponentEffect::parameters`, so `getParameterCount()` returns the SDK default 0. This +track introduces the whole surface. + +**Surface boundary — owns:** a **new pure parameter-identity module** (the frozen id table, +the `DeckParam` ↔ `ParamID` mapping, the derived exposed set, the unit assignment — with its +own `_tests` target), `shell/instrument/reasampler_processor` + `processor_state` (the +`IEditController` parameter surface and the `IParameterChanges` read), +`shell/instrument/editor_controls.cpp` and the editor's drag-commit sites (the +`beginEdit`/`performEdit`/`endEdit` bracketing), and the bake's reset step **for the +notification path only**. **Does not own** the taper (W1-T1's module, consumed), any control's +value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's). + +**Behavior.** +- **The blob stays authoritative; a parameter is a THIRD SURFACE onto the one model** — + a peer of the deck knob and the overlay node, not a second copy of the value. + `docs/product/parameter-automation.md` §6.1 states the load, host→plugin, plugin→host and + save rules, and the two verified findings that closed the fork: this plugin is a + `SingleComponentEffect`, where the SDK itself collapses `IComponent::setState` and + `IEditController::setState` (`vstsinglecomponenteffect.h:41-47`), so there is one state and + §3.3's drift hazard describes a split-component design we do not use; and the blob is a + **cross-artifact contract** the extension's `instrument_drop` writes, which + parameters-as-truth would silently make partial. +- **`ParamID` is an independent, hand-assigned, FOREVER-FROZEN table** — blocks of 100 per + deck group, steps of 10 within a block, a curve dial at its outer knob's id + 1, blocks + starting at 1000. §6.2 for the layout and why hand-assignment beats derivation; **§6.3 for + the freeze invariant, which is to be stated in the table's header with the same force as + the command-id strings, the class UIDs and the payload field order.** +- **The exposed set is DERIVED from `isLiveDeckParam` / `liveCommitFor`, never + hand-maintained** — a control is a parameter iff its class is `Live` or `NoteOnLatched`. + **44 parameters** at the end of Γ-W3, enumerated by group in §7.1. +- **Everything else is OMITTED from the list entirely**, not exposed-and-flagged: the reload + and rebuild tiers, all structural state, and the limiter enable (§3.8, settled). §7.2 + states why omission beats `kIsReadOnly`, and names the limitation plainly — the user cannot + automate filter on/off, play mode, the pitch engine, Staged↔Spline or polyphony, and the + unlock is to give the control a live path first. +- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` / + `getParamValueByString` route through W1-T1's taper module and the editor's own + formatters.** Three functions that agree today is a defect; the host's normalization, the + needle angle and the overlay node must be the same function. +- **`IParameterChanges` is observed at BLOCK boundaries, stated in the header** — the last + point in a block wins. Sample-accurate application would put a per-sample "did anything + change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids. +- **A host parameter change takes the control's existing commit tier and no other.** Nothing + on the automation path may reach `reloadInstrument` or `rebuildVoiceEngine` — which §7.2's + omissions guarantee structurally rather than by care. +- **`IUnitInfo`: one unit per deck group.** Order per **Γ-F7** (§6.4) — **recommendation + signal flow; Daniel's call, and it must be closed before this track dispatches.** +- **No `kIsBypass` on anything.** The plugin is an instrument and exposes no bypass + parameter; the limiter is a safety device, not a bypass, and binding it there would hand + the host a control that restarts the component. +- **The bake's reset gains a notification obligation** (§9): every internal writer of a value + that is an exposed parameter must go through the one `beginEdit`/`performEdit`/`endEdit` + path, and the bake's reset is the codebase's first non-gesture writer. **Enumerating those + sites is part of this track**, not a follow-up. +- **Two adjacent SDK surfaces are assessed, with dispositions, so they are not re-surveyed:** + `IMidiMapping` is **in scope and nearly free** (a CC → `ParamID` map, one function); + `IParameterFunctionName` is **not implemented** (its vocabulary is compressor/panner + semantics that name nothing here); `IAutomationState` is **not implemented** (it reports the + host's automation mode for the whole plug-in, not per parameter, so it cannot answer the one + question §9 would have wanted it for). + +**Acceptance criteria.** +- **The host lists exactly the derived set, in the ruled order, with no parameter the + predicate does not classify `Live` or `NoteOnLatched`** — asserted against the predicate, + not against a literal count. +- **Every id in the table is asserted unique, in its group's block, and on its step** — and a + test fails if any id changes value, which is what makes the freeze mechanical rather than + cultural. +- **Every parameter round-trips `plain → normalized → plain` exactly at its default**, so a + host's reset-to-default and the editor's double-click land on the same value. If this fails, + it is a W1-T1 defect surfacing here, not a defect of this track. +- **`getParamStringByValue` prints what the editor prints**, unit for unit, at the same + values. +- **A host automation lane moving a Live parameter moves a sounding note; a lane moving a + NoteOnLatched parameter takes effect on the next note and does NOT trigger a reload or an + engine rebuild** — assert the tier, not just the sound. +- **No automation path reaches `reloadInstrument` or `rebuildVoiceEngine`.** +- **A project saved before this change opens with every parameter reading the blob's value + and sounds identical**; a project saved by this build opens in an older binary with its + sound intact; and a project with automation drawn, saved and reopened, replays against the + same plain values. +- **`process()` takes no new indirection and no new per-sample work** — the parameter read is + a block-boundary act, on the existing live-publish path. +- **The bake's reset notifies the host**, verified by the host's displayed value following it + rather than snapping back on next touch. +- **The double-processing limitation is documented, not discovered** — a bake whose + reset-class parameter carries a host lane is a named boundary of the bake's fidelity claim + (§9), stated in the product doc and in this track's review. + +**Open questions.** +- **[Daniel] — Γ-F7, the parameter order.** Signal flow (recommended) or the editor's visual + rows. **This is the only unanswered [Daniel]-class question in this plan.** It blocks + nothing until this track dispatches, and it cannot be closed by proposal at review: a + forever commitment is Daniel's. §6.4. +- **[verify, FIRST]** that REAPER calls `setState` (not `setComponentState`) on a + single-component plug-in, and the ordering of `setState` against the first + `IParameterChanges` block after a project load. §6.1 is built on the SDK's own + name-collapse; **verify it in the DAW before wiring, and do not build on the paragraph + alone.** +- **[propose at review]** promoting **key-track** and **Trigger length** from `Reload` to + `NoteOnLatched` (§7.4). Both are excluded from the live set *for the note-on-latch reason* + in the predicate's own words, so the promotion aligns routing with documented semantics — + and it is what makes them automatable at all. **If either promotion is refused, that + control simply drops out of the parameter list.** The list follows the predicate; the + predicate is never bent to fill the list. +- **[propose at review]** whether to ship a default `IMidiMapping` CC table here or leave MIDI + control to REAPER's host-side learn. Either is defensible; **skipping it silently is not.** +- **[propose at review]** whether this track spends the reserved payload rung. §6.1 says + nothing new is persisted and therefore it should not; if the `setState` verification says + otherwise, it takes the reserved rung and says so. +- **Closed, do not reopen:** Rate lifted from latched to live (§3.5 records the cost); the + limiter enable made automatable (§3.8 — its one reopening condition is the `docs/TODO.md` + reload/activation decoupling, and the answer is to do that first, not to re-litigate the + classification). + --- ## Traceability — all seventeen items @@ -1613,13 +2126,20 @@ 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-*`). Eight tracks across three waves, from a direct interview with - Daniel (2026-08-01), 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 automation scoping it defers in - `docs/product/parameter-automation.md`. Γ-W3-T1 additionally **discharges** the - `docs/TODO.md` deck-rework entry, whose original "one row of taller decks with - within-deck stacking" shape Daniel explicitly superseded. +- **All of Phase Γ** (`pg-*`). **Ten tracks across four waves**, 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` + entries are discharged by this phase, not deferred again:** Γ-W3-T1 discharges the + deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape + Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above + 2 s"** (Γ-F3 reversed). +- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list + for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this + 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. ### Deliberate compressions @@ -1681,26 +2201,37 @@ Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W W1 Consolidated tracking, and the programmed-note model T1 tracking-consolidation .................. 17 T2 note-program-model ...................... 15 (model) - W2 The bake chain [requires all of Phase Θ, and Phase Γ before it] + 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) -Phase Γ — The instrument's control surface (none of the seventeen; runs before Ξ-W2) +Phase Γ — The instrument's control surface (none of the seventeen; ends with VST3 params) W1 Foundations [5 tracks, disjoint by surface] - T1 knob-interaction-law ....... modifiers + ms/semitone tapers + reset bypass - T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [payload v14] + T1 knob-interaction-law ....... modifiers + ONE taper module + reset bypass + + 10 s ceiling + AHDSR schematic axis [Ruling 2] + T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1] T3 contour-trace-curves ....... staged traces draw curved, knot on its trace T4 editor-floor-and-row-law ... floor 1190x680 + budget constants + row predicate T5 preserve-time-stretch ...... real stretcher [measure-and-report gate] W2 New controls, and the overlay's marks [2 tracks] - T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [payload v15] + 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 [1 track] + W3 The reflow, and the bake correction [2 tracks] 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] + 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] + [OPEN: Gamma-F7, the parameter order — Daniel] - Resequenced 2026-08-01: the reflow split canvas (W1-T4) from arrangement (W3-T1), and - preserve-time-stretch moved W4 -> W1-T5, which retires Rate's interim stand-in. - Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5) and editor_session.cpp - (W2-T1 | W2-T2) — both textual adjacency, not semantic contention. + 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 — read kParamsPayloadVersion on dev and take the + next three above it. On dev at 2026-08-01 that is v14 / v15 / v16-reserved. + 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. ``` diff --git a/docs/TODO.md b/docs/TODO.md index c27411e..43f19d5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -208,21 +208,15 @@ Forward-looking follow-ups. Deferred by decision, not oversight — each entry r **Done looks like.** Switching projects mid-record leaves the recorded file with a ledger record in the project it belongs to, so a later prune of that project can reclaim it normally. -## Raise the stage-time ceiling above 2 s for long-decay sound design +## ~~Raise the stage-time ceiling above 2 s for long-decay sound design~~ — SCHEDULED, no longer deferred -**Context (Daniel, 2026-08-01 — Phase Γ fork Γ-F3).** Every stage-time knob spans `[0, kEnvTimeMaxSeconds]` (`core/instrument/ui/deck_values.h:22`), which reads the AHDSR overlay's schematic scale `kGateStageMaxSeconds = 2.0` (`core/instrument/ui/envelope_overlay.h:85`). Γ-F3 asked whether Phase Γ's log taper should also raise that ceiling. **Ruled: not in Phase Γ — the ceiling stays 2.0 s.** But the ambition is real and is recorded here rather than dropped: Daniel *"can foresee wanting 10-second stages"* for certain sound-design cases, his example being **"a horrifically long decay with tight exp[onent]"** — a very long stage whose curve exponent puts almost all the motion at one end. That shape is unreachable at a 2 s ceiling. +**This entry is discharged into `docs/PLAN.md` at Γ-W1-T1 and is retained only as a pointer.** Daniel reversed Γ-F3 the same day he ruled it (2026-08-01): *"extend the stage lengths to 10s."* `kEnvTimeMaxSeconds` / `kGateStageMaxSeconds` move **2.0 → 10.0 in Γ-W1-T1**, beside the taper work rather than after it. -**Why it is deferred rather than done.** It is a musical-range question, not a correctness one, and the right time to answer it is with the new taper in the DAW under the hand — a 5× ceiling change judged from a spec is a guess. +**Why the reversal, since the deferral's reasoning was sound.** The deferral said the right time to judge a 5× range change is with the new taper in the DAW under the hand. What changed is not that judgement but the **cost of waiting**: Ruling 1 schedules VST3 parameters inside the same phase (Γ-W4-T1), and a range endpoint is part of the host-facing normalization exactly as much as the curve between the endpoints is. Raising the ceiling is free this wave and permanently expensive four waves later — the same one-way door `docs/product/parameter-automation.md` §4 states for the taper itself, and §8 sweeps for exhaustively. -**Two things landing in Phase Γ make it cheap afterwards.** -1. **The log taper (Γ-W1-T1) is what makes a higher ceiling usable rather than unusable.** Under the current linear map, a 10 s ceiling would put 10 ms at 0.001 of travel — one pixel of a 128 px drag — so the ceiling and the low-end resolution were in direct conflict. Under the log taper the low end keeps its resolution regardless of where the ceiling sits (Γ-W1-T1's landmarks: 10 ms within 0.12–0.20 of travel, 100 ms within 0.42–0.52). **The taper is the prerequisite, and it is why this is a follow-up rather than a dead end.** -2. **`resetDeckParam` bypassing the taper (also Γ-W1-T1) removes the power-of-two blocker.** `deck_values.h:42-46` records that exact default recovery currently depends on the ceiling being a power of two — `x/2^n*2^n` is lossless, an arbitrary ceiling is not. **2.0 is a power of two; 10.0 is not**, so under today's norm round-trip a 10 s ceiling would land every stage-time reset a mantissa bit off its own default. Γ-W1-T1 makes reset write the default value directly, which retires that dependency outright. +**What this entry contributed, and where it now lives.** Its two prerequisites (the log taper; `resetDeckParam` bypassing the taper, since 2.0 is a power of two and 10.0 is not) were already in Γ-W1-T1 and are now load-bearing rather than incidental. Its named hard part — *"the constant change is trivial; keeping the drawing legible is not"* — is now in-scope design work, specified at `docs/product/instrument-control-surface.md` §4.3.1: at 10 s a 30 ms attack is 0.3 % of the AHDSR schematic's stage domain, and the answer is to make the schematic axis **be** the taper, so a node's position within its stage slot is its knob's needle position. -**The constraint the change MUST handle.** `kEnvTimeMaxSeconds` and `kGateStageMaxSeconds` **move together, or not at all.** The AHDSR overlay's schematic scale is derived from the ceiling so that a maxed knob lands exactly at the canvas edge — the agreement requirement is documented at `deck_values.h:19-22`, and `deck_values.h` reads the overlay's constant rather than restating it precisely so the two cannot drift. Raising the ceiling therefore also stretches every drawn AHDSR: at 10 s, a 30 ms attack occupies 0.3% of the schematic's stage domain and becomes visually indistinguishable from zero. **Whether the overlay's schematic scale should stay linear in seconds at a 10 s ceiling, or take a taper of its own, is the real design question underneath this entry** — the constant change is trivial; keeping the drawing legible is not. - -**Priority / risk.** Low / deferred by ruling. No correctness risk: the persisted payload stores raw seconds, so a ceiling change re-tapers needle angles only and every saved project reloads bit-identical (the same persistence-neutrality Γ-W1-T1's own taper changes are held to). **One timing risk that is NOT low:** if VST3 automation parameters ship before this, the ceiling becomes part of the host-facing normalization and moving it silently re-interprets every recorded automation point — the same one-way door `docs/product/parameter-automation.md` §4 states for the taper itself. **If this is wanted, it wants to happen before the parameter system, not after.** - -**Done looks like.** A stage time of several seconds is reachable by hand with no loss of resolution below 100 ms; the AHDSR overlay still reads legibly at both ends of the range; `kEnvTimeMaxSeconds` and `kGateStageMaxSeconds` still agree; and a project saved at the old ceiling reloads with identical stored seconds and identical audio. +**Nothing here is actionable as a TODO.** Delete this entry when Γ-W1-T1 lands. ## Decouple the instrument reload from VST3 activation diff --git a/docs/product/instrument-control-surface.md b/docs/product/instrument-control-surface.md index 892eae9..3b206a4 100644 --- a/docs/product/instrument-control-surface.md +++ b/docs/product/instrument-control-surface.md @@ -57,6 +57,16 @@ own width formula, not carried over from a prior measurement. The stale geometry sits under it. The core finding behind the re-approach: three identical bars draw a *point* and the two ends of a *span* in the same ink, and the crossfade is painted where its *ingredient* lives rather than where the *event* is heard. §6. +- **VST3 automation parameters SHIP IN THIS PHASE, as its last track** (Ruling 1, Daniel + 2026-08-01). The specification is `docs/product/parameter-automation.md` §§6–10: 44 + exposed parameters derived from the three-state commit predicate, a hand-assigned + FOREVER-FROZEN id table in blocks of 100 with steps of 10, and the blob left + authoritative with parameters as a third surface onto the one model. +- **The stage-time ceiling moves 2 s → 10 s, in wave 1** (Daniel, reversing Γ-F3), + *because* parameters now ship in-phase — a range endpoint is host-facing normalization, + free to move now and permanently expensive afterwards. Its real cost is not the constant + but keeping the AHDSR overlay legible when a 30 ms attack is 0.3 % of the schematic + domain; the answer is to make the schematic axis *be* the taper. §4.3.1. --- @@ -668,10 +678,24 @@ reduction is applied.** ### 3.4 Where the master controls sit in the reset scope Phase Ξ-W2's resample reset scope is settled by rule ("reset what the bake baked in"). -Derived against that rule, surfaced for Ξ-W2's review rather than as a Daniel call: -**rate → reset**, **pitch offset → reset**, **limiter enabled → reset** (master gain is -already on the reset list, so the bake includes the master stage, so the limiter's effect -is in the audio). +Derived against that rule — **no new Daniel call**: **rate → reset**, **pitch offset → +reset**, **limiter enabled → reset** (master gain is already on the reset list, so the bake +includes the master stage, so the limiter's effect is in the audio). + +**This is now a CORRECTION, not a sequencing note.** The original plan required Phase Γ to +land before Ξ-W2 so the bake's reset list would be complete on the day it shipped. **That +ordering was never Daniel's choice and it is already violated** — Ξ-W2-T1 was underway +before this phase was scoped (Daniel: *"xi was started before I spun you up, we'll have to +correct phase xi inside gamma. wasn't a choice."*). So the three classifications above are +an amendment Phase Γ **owes** to a shipped bake, and Γ-W3-T2 is the track that pays it. The +amendment is verified **against what Ξ-W2-T1 actually shipped**, never against what this +document predicted it would ship. + +**A second correction of the same shape arrives with Ruling 1**, and it is Γ-W4-T1's, not +this one's: once these values are exposed as VST3 parameters, 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. Full analysis and the disposition: +`docs/product/parameter-automation.md` §9. ### 3.5 Assessment: the `temp_cortex/` limiter reference @@ -775,16 +799,87 @@ shape): - Exactly **0 s at norm 0** and exactly `kEnvTimeMaxSeconds` at norm 1. Monotone and continuous throughout. - **10 ms lands within 0.12 … 0.20 of travel; 100 ms within 0.42 … 0.52.** -- `kEnvTimeMaxSeconds` is **2.0 s** (`deck_values.h:22`, reading `kGateStageMaxSeconds` at - `envelope_overlay.h:85`) and **does not move in this phase** — settled, Γ-F3. The AHDSR - overlay's schematic scale is derived from it and the two must agree so a maxed knob lands - exactly at the canvas edge; the agreement requirement is documented at - `deck_values.h:19-22`. **Daniel foresees wanting a 10 s ceiling** for sound-design cases - — his example, *"a horrifically long decay with tight exp"* — and that ambition is - carried as a `docs/TODO.md` entry rather than dropped. Two things make it cheap later and - both land in this phase: the log taper is precisely what makes a higher ceiling *usable* - rather than unusable at the low end, and the reset-bypasses-the-taper change below - removes the power-of-two dependency that would otherwise block a 10.0 s ceiling outright. +- `kEnvTimeMaxSeconds` **moves 2.0 s → 10.0 s IN THIS PHASE, in this track** + (`deck_values.h:22`, which reads `kGateStageMaxSeconds` at `envelope_overlay.h:85` — the + two move together or not at all). **This reverses Γ-F3** (Daniel, 2026-08-01: + *"extend the stage lengths to 10s"*), and §4.3.1 below is the design work it pulls in. + +#### 4.3.1 The 10 s ceiling — why it moved, and what it costs + +**Γ-F3 was ruled "not in this phase" and is now reversed.** The reversal is not a change of +mind about the musical range; it is a consequence of Ruling 1 — VST3 parameters now ship at +the end of this same phase (§4.4, `docs/product/parameter-automation.md` §8). The moment +parameters exist, a range endpoint is part of the host-facing normalization exactly as much +as the curve between the endpoints is: re-ceiling re-interprets every recorded automation +point in project files we do not own and cannot migrate. **Raising the ceiling is free this +wave and permanently expensive four waves later.** The `docs/TODO.md` entry that carried the +ambition is discharged here rather than deferred again. + +**Three constraints carry forward unchanged.** + +1. **`kEnvTimeMaxSeconds` and `kGateStageMaxSeconds` move together.** `deck_values.h` reads + the overlay's constant rather than restating it precisely so the two cannot drift, and the + agreement requirement is documented at `deck_values.h:19-22`. +2. **The reset bypass is now MANDATORY, not merely required-anyway.** `resetDeckParam`'s + exact-default recovery depends on the ceiling being a power of two + (`deck_values.h:42-46`); **2.0 is, 10.0 is not**, and the log taper compounds it. The + bypass below was already required by this track — it is now also the only thing that + makes the new ceiling correct, so it is not a candidate for "simplification" back into a + norm round-trip under any circumstance. +3. **A new, harder correctness case arrives with the parameters, and it is NOT solved by the + bypass.** `ParameterInfo::defaultNormalizedValue` is normalized; a host's reset-to-default + arrives back as `toPlain(defaultNorm)`, and **the host has no bypass to offer**. The taper + must therefore be designed so **every default has an exact normalized preimage** — see + `docs/product/parameter-automation.md` §8 door 3. This binds the taper's *shape*, so it + belongs to this track and cannot be handed forward. + +**The real design problem is legibility, and it is in scope here.** The AHDSR overlay's +schematic gives each of the four timed stages an equal slot and maps seconds across it +linearly (`gatePxPerSecond`, `envelope_overlay.cpp:33-34`). At a 2 s ceiling a 30 ms attack +occupies 1.5 % of its stage's domain — small but drawn. **At 10 s it occupies 0.3 %, under a +pixel at the floor width, and becomes visually indistinguishable from zero.** A 5× ceiling +that makes the default attack invisible is not a feature. + +**Three directions were considered.** + +1. **Content-fit auto-scale** — the schematic's domain follows the largest current stage, so + short envelopes draw large. Rejected: the axis moves under the hand while you drag, every + node shifts when any node moves, and it breaks the documented anchor that a maxed knob + lands exactly at the canvas edge. +2. **A minimum drawn stage width** — every stage gets at least *n* px regardless of value. + Rejected: it decouples the drawn position from the value, so `envelope_edit`'s drag + inverse can no longer be the exact inverse of the draw — which is the one property the + node/knot/knob "surfaces onto ONE model" invariant rests on. +3. **The schematic axis BECOMES the taper — recommended.** A stage's slot width is + `slotPx × taperNorm(seconds)` instead of `slotPx × seconds / ceiling`. The node's position + within its slot then *is* its knob's needle position, drawn a second way. + +**Recommended: direction 3.** It is the smallest change that is also the most principled one: + +- **Legibility becomes ceiling-independent by construction.** The taper's own landmarks + (10 ms within 0.12–0.20 of travel, 100 ms within 0.42–0.52) are landmarks on the overlay + too, at any ceiling this or a future phase picks. +- **It strengthens the one-model invariant rather than straining it.** A node and its knob + become the same normalized quantity; today they are two maps that happen to agree. +- **It costs the drawn curve nothing.** The taper decides only *where a stage's end node + lands*. Within a stage, φ still runs linearly across the stage's pixel span, so a φ^p + segment draws as φ^p exactly as Γ-W1-T3 specifies — the two tracks compose rather than + fight. +- **The AHD policy is untouched.** An AHD maps 1:1 onto the waveform's own time axis and is + PCM-aligned; it must stay linear in seconds, and nothing here changes it. **Only the AHDSR + schematic is tapered**, and it was already documented as schematic-not-time-aligned. + +**The cost, named:** within-stage horizontal extent stops being proportional to time, so two +stages can no longer be compared by eye at a 10× ratio the way they can at 2×. The ms labels +Θ-W6-T1 landed carry the actual number, which is what that comparison is actually made +against; and the alternative — a 30 ms attack drawn as zero — loses the comparison entirely. + +**The structural consequence, and it is the important one.** The taper is now read by three +consumers: the knob (`deck_values`), the overlay (`envelope_overlay` + `envelope_edit`), and +— from Γ-W4-T1 — the host (`normalizedParamToPlain`). **It must be extracted into one pure +module** rather than living inside `deck_values`, which sits above `envelope_overlay` in the +dependency order. That extraction is what makes "the taper IS the host-facing normalization" +structurally true instead of a comment somebody has to remember. **Semitone knobs become log2-scaled.** More resolution across −7 … +7 st while still allowing the extremes: @@ -818,11 +913,17 @@ allowing the extremes: ### 4.4 Sequencing — why item D goes first Item D lands **before** the two new PITCH/RATE knobs, so Rate and Pitch are authored into -the finished law rather than retro-fitted into it. It lands **well before** any VST3 -parameter work, for a much sharper reason: once parameters are exposed, the taper *is* the -host-facing normalization, and re-tapering silently re-interprets every recorded automation -point in every saved project. **Taper changes are free now and permanently expensive -later.** See `docs/product/parameter-automation.md` §4. +the finished law rather than retro-fitted into it. It lands **three waves before Γ-W4-T1's +VST3 parameter work**, for a much sharper reason: once parameters are exposed, the taper +*is* the host-facing normalization, and re-tapering silently re-interprets every recorded +automation point in every saved project. **Taper changes are free now and permanently +expensive later.** See `docs/product/parameter-automation.md` §4. + +That argument used to justify Γ running ahead of a future phase; since Ruling 1 it governs +**wave order inside this one**, which is strictly better — the taper and the parameters that +freeze it are now reviewed against each other rather than across a phase boundary. It is +also why the ceiling moved (§4.3.1) and why §8's one-way-door sweep in the automation doc is +a deliverable rather than a caution. --- @@ -1251,18 +1352,20 @@ shape is deliberate and its reasoning is documented at the call site. --- -## 8. Forks — all six ruled, none open +## 8. Forks — six ruled (one later reversed), one open ### 8.1 Ruled by Daniel, 2026-08-01 -Every fork this doc opened is closed. The rulings are folded into the sections that -depend on them; this table is the index, not a second copy of the reasoning. +The rulings are folded into the sections that depend on them; this table is the index, not a +second copy of the reasoning. **Γ-F3 was ruled and then REVERSED the same day** — the row +below carries both, because a reader who acts on the first ruling would ship the wrong +ceiling. | Fork | Question | Ruling | Where it landed | |---|---|---|---| | **Γ-F1** | Does `kEditorMinHeight` move 680 → 720? | **No — stays 680.** The reflow's 112 px goes entirely to the waveform. | §1.2 / §1.5, unchanged | | **Γ-F2** | Limiter lookahead, or zero-latency? | **Lookahead with DYNAMIC reported latency** — zero when off, the lookahead when on, reported to the host's PDC. *Overrides this doc's zero-lookahead recommendation.* | **§3.1.1** (new), §7.10 | -| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **Not in this phase — stays 2.0 s.** The 10 s ambition is preserved as a `docs/TODO.md` entry with its rationale. | §4.3, `docs/TODO.md` | +| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **REVERSED, same day. Ruled first "not in this phase — stays 2.0 s"; then Daniel: _"extend the stage lengths to 10s."_ The ceiling moves 2.0 → 10.0 in Γ-W1-T1.** The reversal's cause is Ruling 1: parameters now ship in-phase, so the ceiling is a one-way door that has to be walked through *before* them. | **§4.3.1** (new), §4.3; `docs/TODO.md` entry discharged | | **Γ-F4** | Explicit loop enable? | **Yes — on the CHROME ROW.** Not a deck cell; loop is a waveform-overlay concept and has no deck. | **§6.4** (new), §6.5, §7.9 | | **Γ-F5** | MASTER's reserved slot: one cell or two? | **One cell.** Two would spend 60 of the 90 px headroom on an unnamed control and freeze row 1 forever. | **§1.6** (new), §1.4 | | **Γ-F6** | Is the `kLatencyChanged` deactivate/reactivate acceptable as the cost of the toggle? | **Yes — ship dynamic latency as ruled.** No constant-latency fallback, no measurement gate. *Corrected this doc's analysis: the cost is self-inflicted, not SDK-imposed.* | **§3.1.1** (rewritten), §7.10, §7.11, `docs/TODO.md` | @@ -1322,18 +1425,39 @@ than just counting: `restartComponent`), not about this flag. 5. **The reduction is filed**, with a trigger condition, in `docs/TODO.md`. -*No fork in this phase is open. Nothing in §§1–7 awaits a Daniel answer.* +### 8.3 Γ-F7 — OPEN. The parameter order + +**Opened 2026-08-01 by Ruling 1** (*"Make the parameter order logical"*), because "logical" +resolves two ways and the choice is frozen forever the day parameters ship. + +> **Signal-flow order** — PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → +> VOICE → MASTER, the deck's own documented ordering rule, layout-independent. +> **OR the editor's visual row order** after the Γ-W3 reflow — row 1 then row 2 then MASTER, +> matching what the user's eye scans. + +Same membership, different sequence; the recommendation, both arguments, and why the +grouping (`IUnitInfo`, one unit per deck group) is settled either way are in +`docs/product/parameter-automation.md` §6.4. **Recommendation: signal flow**, because the +visual layout has moved twice already and this phase moves it again, and freezing a forever +identity to a thing that moves is the wrong coupling. + +**Urgency: low, but not zero.** Three waves sit in front of Γ-W4-T1 and nothing before it +depends on the answer. It must close **before Γ-W4 dispatches**, and it cannot be closed by +proposal at review — a forever commitment is a Daniel call. + +*Every other fork in this phase is ruled. Nothing in §§1–7 awaits a Daniel answer.* --- ## 9. Build shape -Sequenced into `docs/PLAN.md` as **Phase Γ** (worktree slug prefix `pg-`), **three waves** -(resequenced by Daniel, 2026-08-01 — see below): +Sequenced into `docs/PLAN.md` as **Phase Γ** (worktree slug prefix `pg-`), **four waves** +(resequenced by Daniel twice on 2026-08-01 — see below): ``` Γ-W1 Foundations [5 tracks, disjoint by surface] - T1 knob-interaction-law ............ item D (editor input + deck_values tapers) + T1 knob-interaction-law ............ item D (editor input + the ONE taper module + + 10 s ceiling + AHDSR schematic scale) T2 master-bus-audio ................ item C (pure limiter + meter ballistics + processor + LATENCY REPORTING) T3 contour-trace-curves ............ item E (waveform painter) @@ -1344,11 +1468,17 @@ Sequenced into `docs/PLAN.md` as **Phase Γ** (worktree slug prefix `pg-`), **th T1 pitch-rate-deck ................. item A (params + engine + deck descriptor) T2 loop-crossfade-ux ............... item F (waveform painter + pure marker geometry + the chrome-row loop enable) -Γ-W3 The reflow [1 track] +Γ-W3 The reflow, and the bake correction [2 tracks] T1 deck-reflow ..................... item B's ARRANGEMENT half + C's UI half + T2 bake-reset-amendment ............ the Phase Ξ correction Γ owns (§3.4) +Γ-W4 VST3 parameters [1 track] + T1 vst3-parameter-set .............. Ruling 1 (parameter-automation.md §§6-10) ``` -**Two resequencing decisions, both Daniel's (2026-08-01).** +**Three resequencing decisions, all Daniel's (2026-08-01).** The third is Ruling 1: the +parameter system moves from "a future phase" into **Γ-W4**, which is what put the 10 s +ceiling into W1 (§4.3.1) and turned the Ξ ordering constraint into an owned correction +(§3.4). The first two: 1. **Item B splits: canvas early, arrangement late.** The window floor, the width budget it derives from, and each group's row membership land in W1-T4 so every other UI track is diff --git a/docs/product/parameter-automation.md b/docs/product/parameter-automation.md index 8627119..1b8a1f8 100644 --- a/docs/product/parameter-automation.md +++ b/docs/product/parameter-automation.md @@ -1,15 +1,24 @@ -# VST3 automation parameters — scoping, not scheduling +# VST3 automation parameters — the Phase Γ specification > Daniel, 2026-08-01: *"we need to scope the whole parameter system for automation, which > will require redesigning the existing wiring."* And, on Rate being latched at note-on: > *"we will need to analyze ALL the controls for LIVE use/automation use."* -**This is NOT Phase Γ work.** It is framing and open questions, written now because Phase Γ -makes two decisions whose cost changes permanently the day parameters ship, and because -knowing that is what makes Γ's ordering correct rather than arbitrary. Nothing here is -scheduled; nothing here is settled. +**Superseding ruling, Daniel 2026-08-01 (later the same day):** *"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."* -The one thing this doc *does* assert is a sequencing constraint — §4. +**This IS Phase Γ work.** It ships as **Γ-W4-T1 `vst3-parameter-set`**, the phase's last +track. This doc was written as scoping and has been promoted in place: §§1–5 are the +original analysis (still correct, still the reasoning), and **§§6–10 are the specification** +— the ID scheme and its freeze invariant, the per-control classification, the one-way-door +sweep, the resample interaction, and the build shape. + +Two things that were open in the scoping pass are now **decided** rather than deferred: +the storage architecture (§3.3, decided in §6.1) and the ID space (§3.1, decided in §6.2). +One thing is **open and wants Daniel** before Γ-W4 dispatches: **Γ-F7**, the parameter +*order* (§6.4). It is not urgent — three waves sit in front of it — but it is a forever +commitment, so it is not mine to close silently. --- @@ -61,11 +70,12 @@ class UIDs already carry, and the same discipline the params-payload version lad wants to renumber, regroup or reorder that enum has to happen first. This is not a theoretical concern: Phase Γ adds two entries to it. -Open: is `ParamID` the `DeckParam` value directly, or an independent id space with an -explicit mapping table? Direct is simpler and tempting; an independent space is what lets -the enum keep being a UI-ordering convenience. **Lean: independent id space with an explicit -frozen table**, on the same reasoning the command-id family already uses — the display order -and the wire identity should not be the same number. +**DECIDED — §6.2: an independent id space with an explicit frozen table.** Direct +`DeckParam` values were simpler and were rejected: they would freeze an enum whose own +header declares it *"runtime-only… free to change"*, and they cannot express the cases +where the two spaces genuinely differ (a `DeckParam` that is not a parameter, a parameter +with no deck cell). The reasoning is the command-id family's: display order and wire +identity should not be the same number. ### 3.2 The taper becomes the host-facing contract @@ -101,8 +111,11 @@ Two coherent answers, both viable, neither obviously right: blob contract intact; risks exactly the drift class above if the precedence is not airtight. -Open. This is the load-bearing architectural decision of that phase and it should be made -first, the way Ξ-W2-T1's crossing decision was. +**DECIDED — (b), the blob stays authoritative. See §6.1 for the reasoning, the two +findings that settled it, and the precedence rule that closes the drift risk.** This was +going to be handed to the track as its opening architecture decision, the way Ξ-W2-T1's +crossing decision was. It is not, because two verified facts collapse the fork rather +than balancing it — a fork with a dominated option in it is not a fork. ### 3.4 Which controls can be parameters at all — three classes @@ -212,7 +225,7 @@ decoupling first, not to re-litigate the classification. --- -## 4. The sequencing assertion — the one thing this doc claims +## 4. The sequencing assertion — now satisfied inside one phase **The knob-taper work (Phase Γ item D) must land before the parameter system. Not alongside; before.** @@ -232,30 +245,438 @@ update; doing it after parameters ship costs a frozen-id decision. the parameter system touch `deck_values.cpp` and `editor_controls.cpp`, and the ordering is not a coin flip: item D first, by a wide margin. +**What changed when the parameter system moved INTO Phase Γ.** The assertion above was +written to justify Γ running before a later phase. It now governs **wave order inside one +phase**: Γ-W1-T1 lands the taper, Γ-W4-T1 lands the parameters, and two whole waves of new +controls sit between them so that every parameter exists before any parameter is declared. +The argument is unchanged; only its blast radius shrank, which is strictly better — the two +halves are now reviewed against each other rather than across a phase boundary. + +**And it grew a second clause.** Daniel's ruling *"extend the stage lengths to 10 s"* +reverses Γ-F3 and moves `kEnvTimeMaxSeconds` 2.0 → 10.0 into **Γ-W1-T1, beside the taper**, +for exactly the reason stated here: a range endpoint is part of the host-facing +normalization every bit as much as the curve between the endpoints is. §8 sweeps for every +other constant with that property. + --- -## 5. Open questions, collected +## 5. Open questions — status after the promotion -Not one of these is a Phase Γ blocker. They are what a future phase's first wave answers. +The scoping pass collected eight. Seven are closed below; one is open and is a Daniel call. -1. **Blob vs. parameters as the source of truth** (§3.3) — the architectural decision, made - first, before any wiring. -2. **`ParamID` space** — direct `DeckParam` values, or an independent frozen table (§3.1). - *Lean: independent.* -3. **The discrete/rebuild-tier controls** (§3.4) — for each of voice count, Poly/Mono, - Retrig/Legato, Gate/Trigger, Staged/Spline, pitch engine, filter law: gain a live path, - expose as non-automatable, or omit from the parameter list entirely. -4. **Per-parameter live/latched classification** (§3.5) — the three-state predicate Phase Γ - opens must be filled in for every exposed parameter, including whether Rate is lifted to - live and at what cost. -5. **Unit tree shape** (§3.6) — mirror the deck's visual grouping, or the signal flow. -6. **Sample-accurate vs. block-boundary** (§3.7). *Lean: block boundary, stated explicitly.* -7. **Does the extension's `instrument_drop` need to know?** It writes the same - `ComponentState` bytes. If parameters become the source of truth for automatable values - (option (a)), a blob the extension writes would no longer fully determine the instrument's - sound. That is a cross-artifact contract change and it is the sharpest hidden cost of - option (a). -8. **Migration.** Every saved project predates parameters. On first load a parameter's value - comes from the blob; after that the host owns it. Confirm that path is lossless, and - confirm what happens to a project saved *by* the new build and opened by an older one - (the blob still carries everything, so it should be fine — verify, do not assume). +| # | Question | Status | +|---|---|---| +| 1 | Blob vs. parameters as the source of truth (§3.3) | **CLOSED — (b), blob authoritative.** §6.1 | +| 2 | `ParamID` space (§3.1) | **CLOSED — independent frozen table.** §6.2 | +| 3 | The discrete/rebuild-tier controls (§3.4) | **CLOSED — omitted from the list entirely**, not exposed-and-flagged. §7.2 | +| 4 | Per-parameter live/latched classification (§3.5) | **CLOSED — the list is DERIVED from the three-state predicate**, never hand-maintained. §7 | +| 5 | Unit tree shape (§3.6) | **CLOSED — the deck's groups, in signal-flow order**, layout-independent. §6.4 | +| 6 | Sample-accurate vs. block-boundary (§3.7) | **CLOSED — block boundary, stated explicitly.** §6.5 | +| 7 | Does the extension's `instrument_drop` need to know? | **CLOSED — no.** It was the sharpest cost of option (a), and (a) is not taken. The blob a drop writes still fully determines the sound. §6.1 | +| 8 | Migration | **CLOSED as a design question, OPEN as a verification.** §6.6 | +| **Γ-F7** | **Parameter ORDER — signal flow, or the editor's visual rows?** | **OPEN — Daniel.** Recommendation and reasoning at §6.4. Must close before Γ-W4 dispatches; blocks nothing before then. | + +--- + +## 6. The specification — architecture, identity, order + +### 6.1 Storage: the blob stays authoritative; parameters are a projection (option (b)) + +**Decided, not deferred.** Two verified facts collapse §3.3's fork: + +1. **This plugin is a `SingleComponentEffect`, so there is only ONE state.** The SDK's own + header `#define`s `setState`/`getState` to `setEditorState`/`getEditorState` around its + include of `vsteditcontroller.h`, *"work around for the name clash of + IComponent::setState and IEditController::setState"* + (`public.sdk/source/vst/vstsinglecomponenteffect.h:41-47`). `setComponentState` exists to + feed a **separate** controller in a split-component plug-in; we have no separate + controller. §3.3's "a value that lives in both can drift, depending on call order" + describes a hazard of a design we do not use. **The drift class it warns about is + substantially smaller than the scoping pass assumed.** +2. **The blob is a cross-artifact contract.** The extension's `instrument_drop` writes the + identical bytes to build a transient `.vstpreset` (`map/component_state_io.h`). Under + option (a) a blob the extension writes would no longer fully determine the sound — a + shipped seam would silently become partial. That is not a trade, it is a regression. + +Add the repo's own invariant — *"the instrument holds ONE loaded capture and ONE set of +playback parameters… every playback parameter edits in exactly one place"* — and option (a), +which splits the parameter set across two storage mechanisms, is arguing against the +invariant Θ-W1-T1 was run to establish. + +**The rule, stated so it cannot drift:** + +> **`InstrumentParams` / `PlaySeconds` is THE model. The `ComponentState` blob is its only +> persistence. A VST3 parameter is a THIRD SURFACE onto that model — a peer of the deck +> knob and the overlay node, not a second copy of the value.** +> +> - **Load:** the blob restores the model; every exposed parameter's normalized value is +> then *derived* from the model and pushed to the controller. The SDK is explicit that +> this push must not be reflected back to the host: *"The controller must never pass this +> value-change back to the host via the IComponentHandler. It should update the according +> GUI element(s) only"* (`ivsteditcontroller.h:430-433`). +> - **Host → plugin:** a parameter change writes the model through the **existing commit +> tiers** (`shell/instrument/CLAUDE.md`) — live publish or note-on latch. It never takes a +> fourth route. +> - **Plugin → host:** an editor gesture writes the model AND notifies the host, bracketed +> `beginEdit` / `performEdit` / `endEdit` (`ivsteditcontroller.h:175-185`), on the UI +> thread as the SDK requires. +> - **Save:** `getState` serializes the model. Nothing new is persisted. +> +> **The residual risk is not drift, it is authority:** a host automation lane, once written, +> outranks anything the plugin sets, because the host replays it. That is inherent to +> automation and is not a defect to design away — but it has one sharp consequence for the +> resample bake, and that is §9. + +**[verify] at the track, before wiring:** that REAPER calls `setState` (not +`setComponentState`) on a single-component plug-in, and the ordering of `setState` against +the first `IParameterChanges` block after a project load. Verify against the vendored SDK +and in the DAW — do not build on the paragraph above without it. + +### 6.2 The ID space: hand-assigned constants in one frozen table + +**`ParamID` is an independent space from `DeckParam`, defined by a hand-written table of +named constants in one pure module.** Not derived, not computed, not an enum cast. + +**Why hand-assigned rather than derived.** A derived id is a function of something else — +enum position, group index, a hash of a name — and every one of those inputs is a thing +that must then never change. Deriving does not remove the freeze; it hides it behind a +mechanism, and the day someone reorders the source enum the ids move silently. A literal +table makes the freeze **visible at the point of change**: you cannot renumber it by +accident, because renumbering it means editing the numbers. + +**The layout — blocks of 100 per deck group, steps of 10 within a block:** + +| Block | Group | Reserved | +|---|---|---| +| `1000` | PITCH/RATE | 1000–1099 | +| `1100` | PITCH ENV | 1100–1199 | +| `1200` | FILTER | 1200–1299 | +| `1300` | FILTER ENV | 1300–1399 | +| `1400` | AMP ENVELOPE | 1400–1499 | +| `1500` | VELOCITY | 1500–1599 (reserved; empty today — §7.3) | +| `1600` | VOICE | 1600–1699 (reserved; empty today — §7.2) | +| `1700` | MASTER | 1700–1799 | + +- **Within a block, ids step by 10** in the group's own left-to-right cell order. Nine free + slots sit between every pair of neighbours, so **a control added later lands numerically + adjacent to its siblings instead of appended at the end of the whole table.** That is the + concrete content of "make the parameter order logical": the order is a property of the + numbering scheme, maintained by construction, not by remembering to re-sort. +- **A curve-exponent inner dial takes its outer knob's id + 1.** The dial is a property of + the knob it sits inside — the codebase already says so (`curveParamFor`, "each is the + INNER DIAL of the stage knob it shapes") — so its id says so too. This is why the step is + 10 and not 2: it leaves room for the dial *and* for a future sibling knob. +- **Blocks start at 1000, not 0.** `0` is a plausible accident (a default-initialised + `ParamID`), and a scheme whose first legitimate id is also its most likely bug value is a + scheme that cannot tell them apart. +- **An empty block is still reserved.** VELOCITY and VOICE expose nothing today. Their + blocks exist so that if either ever gains an automatable control it lands in its own + group's range rather than in whatever range happened to be free. + +### 6.3 The freeze invariant + +State this in the ID table's header, with the same force as its three siblings. + +> **THE PARAMETER-ID TABLE IS FOREVER-FROZEN.** A VST3 `ParamID` is recorded by the host +> into automation lanes inside project files this repo does not own and cannot migrate. +> From the first shipped build that exposes parameters: +> +> - **No id is ever reassigned, reused, or re-pointed.** If a control's meaning genuinely +> changes, it takes a NEW id and the old one is **retired** — marked dead in the table, +> never re-issued. +> - **No exposed parameter's normalization ever changes** — not its taper, not either range +> endpoint, not its `stepCount`. The normalization *is* the meaning of every recorded +> automation point. §8 enumerates every constant this now freezes. +> - **A parameter's meaning never depends on a mode.** The Gate-face and Trigger-face stage +> times are separate stored fields and take separate ids; an id whose plain value means +> one thing in Gate and another in Trigger is unautomatable by construction. +> - **A new control takes the next free slot inside its own group's block**, never the next +> number at the end of the table. +> +> This sits on the same footing as the extension's `"STABLE_FOREVER_STRING"` command ids +> (root `CLAUDE.md`, "REAPER extension contract"), the two VST3 class UIDs +> (`core/wire/reasampler_uid.h`, FOREVER-FROZEN), and the params-payload field order +> (`map/component_state_io.h`, "EVERY wire format below is FROZEN"). It is the fourth +> member of that family, not a new kind of rule. + +**One property the table BUYS, and it is the reason to prefer it over deriving:** +`DeckParam`'s header says the enum is *"runtime-only… free to change."* With an independent +table that stays true — the reflow, a future regrouping, a new deck can all reorder the enum +freely. Deriving ids from it would have quietly repealed that sentence. + +### 6.4 Order and grouping — Γ-F7, OPEN + +Two orders exist and they are not the same thing: + +- **Identity order** — the id numbers, frozen by §6.3. +- **Presentation order** — the sequence `getParameterInfo(index)` reports, which is what a + host's FX-parameter list and every automation-lane picker display. + +They should agree (a list whose numbering runs backwards against its display is a trap), so +this is one decision, and it is a forever one on both counts. + +**Recommendation: signal-flow order — PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → +VELOCITY → VOICE → MASTER**, which is the deck's own documented ordering rule +(`sampleDeckGroups`, *"left to right, in SIGNAL-FLOW order: pitch → filter → amp, then the +two instance-wide groups"*), with each group's cells in their own left-to-right order. + +**The alternative is the editor's visual rows** after Γ-W3-T1 — row 1 PITCH/RATE, FILTER, +VELOCITY, VOICE; row 2 PITCH ENV, FILTER ENV, AMP; MASTER spanning. Same membership, +different sequence. + +**Why the recommendation goes to signal flow:** the visual layout is a canvas-constrained +artifact that has already moved twice (Θ-W6-T1 grew the floor 840 → 980, Γ-W3-T1 takes it to +1190 and re-rows every group), and within-row order is decided by *width fitting*, not by +meaning. Freezing the host-facing order to a layout that demonstrably moves is the wrong +coupling; signal flow has never moved and is the deck's own stated rule. §3.6 asked whether +the unit tree mirrors the visual grouping or the signal flow and noted "they currently +agree" — the reflow is exactly what makes them stop agreeing, so the question has to be +answered rather than deferred again. + +**Why it is nonetheless Daniel's:** the counter-argument is real — a user hunting a +parameter in REAPER's flat list finds it fastest if the list reads in the order their eyes +scan the plugin window. If Daniel weights find-it-on-screen over layout-independence, visual +order is the right answer and it is not a mistake; it just also means the parameter order +stops being free the next time the deck is re-laid. + +**Grouping is settled either way: `IUnitInfo`, one unit per deck group**, mirroring the +group inventory (not the rows). Only the sequence is at issue. + +### 6.5 What the plugin must implement, as behaviour + +Not architecture — the engineer's call is *how*. These are the observable contracts: + +- **`getParameterCount` / `getParameterInfo`** report exactly the §7 list, in §6.4's order, + each with: the frozen `id`; a `title` that survives truncation and a distinct + `shortTitle`; `units` (`"ms"`, `"st"`, `"%"`, `"dB"`, `""` for exponents) matching what the + editor prints, so the host's readout and the knob's label never disagree; `stepCount = 0` + (every exposed parameter is continuous — §7.2 is why); `defaultNormalizedValue` per §8's + exactness rule; the group's `unitId`; and `kCanAutomate`. +- **`kIsBypass` is not set on anything.** The limiter is a safety device, not a bypass, and + binding it to `kIsBypass` would hand the host a control that restarts the component + (§3.8). The plugin exposes **no** bypass parameter — it is an instrument. +- **`setParamNormalized` / `getParamNormalized`** are the model projection of §6.1, not a + cached shadow value. +- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` / + `getParamValueByString` all route through the ONE taper module Γ-W1-T1 extracts.** This is + the structural point of that extraction: the host's normalization, the knob's needle + angle, and the overlay node's position must be the *same function*, not three functions + that agree today. `getParamStringByValue` reuses the editor's own formatters + (`formatEnvTimeMs` and its peers) for the same reason. +- **`IParameterChanges` is observed at BLOCK boundaries** — the last point in the block wins + for that block (§3.7). Sample-accurate application would put a per-sample "did anything + change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids. + **State it in the header rather than leaving it to be discovered**; revisit only if + audible stepping is reported on a fast curve. +- **A host parameter change takes the control's existing commit tier and no other.** Live + parameters publish; note-on-latched parameters publish and are read by `snapLive`. Nothing + on the automation path may reach `reloadInstrument` or `rebuildVoiceEngine` — which is + precisely what §7.2's omissions guarantee. +- **`IMidiMapping` is in scope and nearly free** (`ivsteditcontroller.h:558-568`): declaring + a CC → `ParamID` map costs one function and gives every exposed parameter MIDI control. + **[propose at review]** whether to ship a default map in this track or leave it to REAPER's + host-side MIDI learn; do not silently skip it without saying so. +- **`IParameterFunctionName` is assessed and NOT implemented** + (`ivstparameterfunctionname.h`). Its vocabulary is compressor/panner/dry-wet semantics + (`Comp:GainReduction`, `DryWetMix`, `Randomize`); none of it names anything this + instrument exposes. Recorded so it is not re-surveyed. +- **`IAutomationState` is assessed and NOT implemented** + (`ivstautomationstate.h`). It reports the host's automation mode for the **whole plug-in** + (`kNoAutomation` / `kReadState` / `kWriteState`), not per parameter — so it cannot answer + "is THIS parameter automated," which is the only question §9 would have wanted it for. + +### 6.6 Migration — the verification, not a design + +Every saved project predates parameters, and under §6.1 that is a non-event: the blob +restores the model and the parameters are derived from it. Three things are nonetheless +**[verify], not assume**: + +1. A pre-parameter project opens with every parameter reading the blob's value, and sounds + identical. +2. A project saved by the new build and opened by an **older** binary still restores fully — + the blob carries everything, so it should; prove it. +3. A project with automation drawn on a parameter, saved and reopened, replays that + automation against the same plain values — the round trip through the frozen taper is + exact at both ends. + +--- + +## 7. The classification — which controls become parameters + +**The list is DERIVED, never hand-maintained.** Its rule is one line: + +> **A control is an exposed VST3 parameter if and only if its commit class is `Live` or +> `NoteOnLatched`.** Everything else is omitted from the parameter list entirely. + +That makes `isLiveDeckParam` / `liveCommitFor` — already *"THE home for why each excluded +control is excluded"* — the single source for the parameter list too, which is the standing +rule (`core/instrument/CLAUDE.md`: *"which controls are live is ONE decision, recorded in ONE +place"*) applied once more rather than a second table opened beside it. + +### 7.1 Exposed — 44 parameters + +Counts are against the control inventory as it stands at the end of Γ-W3; the track asserts +the count against the predicate rather than against this table. + +| Group | Parameters | Class | +|---|---|---| +| **PITCH/RATE** (3) | Rate; Pitch offset; Key-track | Rate **NoteOnLatched** (Γ, settled); Pitch **Live**; Key-track **NoteOnLatched** (see §7.4) | +| **PITCH ENV** (6) | attack, hold-fraction, decay, depth + attack/decay curve exponents | **Live** | +| **FILTER** (7) | morph, cutoff, Q, drive, mod amount, velocity amount, key-track | **Live** | +| **FILTER ENV** (13) | AHDSR A/H/D/S/R; Trigger AHD A/H/D; five curve exponents | **Live** | +| **AMP ENVELOPE** (14) | AHDSR A/H/D/S/R; Trigger AHD A/H/D; Trigger length; five curve exponents | **Live**, except Trigger length **NoteOnLatched** (§7.4) | +| **VELOCITY** (0) | — | its three cells are curve popups, not scalars (§7.3) | +| **VOICE** (0) | — | rebuild tier (§7.2) | +| **MASTER** (1) | Master gain | **Live** | + +**Both play-mode faces are exposed simultaneously**, because both are stored simultaneously +and always have been. A Trigger stage time is a real parameter whose effect is silent while +Gate is selected — which is the same relationship a disabled filter's cutoff already has, and +far better than an id whose meaning depends on a mode (§6.3). + +### 7.2 Omitted — the reload and rebuild tiers, and why "omit" beats "expose read-only" + +**Discrete / reload tier:** play mode (Gate|Trigger), pitch engine (Varispeed|Preserve), +filter enable, filter morph law, pitch-envelope enable, the three Staged|Spline mode +toggles. **Rebuild tier:** voice count, Poly|Mono, Retrigger|Legato. **Latency-changing:** +the limiter enable (§3.8 — settled, and its one reopening condition is named there). + +The scoping pass left three options (gain a live path / expose non-automatable / omit). +**Omit.** An exposed-but-not-automatable parameter is still listed in the host's generic +panel and is still *settable from it*, which would let a generic-panel drag reach +`reloadInstrument` from a host thread — strictly worse than the control being absent. VST3's +`kIsReadOnly` would prevent that, at the cost of a visible row the user cannot move, in a +list whose whole purpose is things you can move. Neither beats absence. + +**Named limitation, stated rather than discovered:** the user cannot automate filter on/off, +play mode, the pitch engine, Staged↔Spline, or polyphony. The unlock is written down and is +not this track's: **give the control a live path first, then it qualifies by the same +rule that excluded it.** Nothing about the parameter system needs to change to admit it. + +**One door this closes by omission** — if voice count were exposed, its 1–32 ceiling would +become a frozen `stepCount` and could never be raised. It is not exposed, so it is not +frozen; a future "expose voice count" proposal must accept that freeze as part of the price. + +### 7.3 Omitted — structural state + +The loaded capture and `SampleRefs`; root note; loop start/end, the loop crossfade, and +**the new loop enable** (Γ-W2-T2, user-owned from that track on); start frame; channel mode; +the three velocity transfer curves; the three spline contours; the three overlay-select +radios (view state, never persisted); preview velocity (a utility, not a musical parameter). + +None of these are scalars with a taper, and several are not scalars at all. They stay in the +blob, which is exactly what §6.1's split is for. + +### 7.4 Two promotions the three-state predicate makes available + +Γ-W2-T1 introduces `NoteOnLatched` for Rate. Two controls that are excluded from the live +set **for the note-on-latch reason** currently route through the reload tier, and the new +state fits them exactly: + +- **Key-track** — `isLiveDeckParam`'s header already says it *"feed[s] values a voice + latches at note-on by design (the pitch ratio…), so live delivery would retune… a note + already struck."* That sentence describes `NoteOnLatched`, not `Reload`. +- **Trigger length** — *"resolves `playEnd_`, a fact about the note, not a setting of it."* + Same shape. + +**[propose at review, Γ-W4-T1]** promote both. The promotion aligns the routing with the +predicate's own stated semantics — and it is what makes them automatable, since today they +would re-decode a WAV per automation point. **If either promotion is refused, that control +simply drops out of the parameter list.** The list follows the predicate; the predicate is +never bent to fill the list. + +**Not promoted, and not proposed for promotion: Rate to Live.** §3.5 records the cost; +that paragraph is the first thing to read if it is ever proposed. + +--- + +## 8. One-way doors — the sweep + +**Deliverable, run 2026-08-01 against `dev`.** Once parameters ship, anything that +participates in a parameter's normalization is frozen as hard as the id itself. This is the +complete list of such constants, with a disposition for each. **Two were already known; five +are new; three are already frozen for an unrelated reason and are recorded so they are not +"tidied" later.** + +**Scheduled ahead of the parameter track — action required:** + +| # | Constant / property | Disposition | +|---|---|---| +| 1 | **The ms and semitone tapers** (`ui/deck_values`) | Γ-W1-T1. Known. The taper *is* the host normalization. | +| 2 | **`kEnvTimeMaxSeconds` / `kGateStageMaxSeconds` 2.0 → 10.0** | Γ-W1-T1, per Daniel's ruling. A range endpoint is normalization. Γ-F3 reversed. | +| 3 | **Every default must have an EXACT normalized preimage** — NEW | Γ-W1-T1 acceptance criterion. `ParameterInfo::defaultNormalizedValue` is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)`. Γ-W1-T1 makes `resetDeckParam` **bypass** the taper, which fixes the editor's reset and **cannot fix the host's** — the host has no bypass. The taper must therefore be designed so every default round-trips exactly, or the host's reset lands a hair off and the "double-click lands exactly on its default" criterion holds on one surface and not the other. **This is the sharpest new finding in the sweep.** | +| 4 | **Do NOT re-taper the filter's four normalized controls** — NEW | Γ-W1-T1 exclusion. Cutoff/Q/morph/drive persist as `cutoffNorm`/`resonanceNorm`/`morphNorm`/`driveNorm` in the **v9 payload** — their laws are *already* wire-frozen today, and re-mapping them would re-tune every saved project independently of automation. §4.2's snap-unit table names them, which is a display concern; nothing in the taper pass may touch their norm↔plain law. | + +**Frozen by this work — record only, no change wanted:** + +| # | Constant | Note | +|---|---|---| +| 5 | `kKeyTrackMax` = 2.0 (0–200 %) | NEW. Becomes the frozen normalization of both key-track parameters. If 0–200 % is ever going to be thought wrong, now is when it is free. Recommendation: **no change.** | +| 6 | The curve-exponent domain 0.1 … 10 and its log travel (`core/util/curve_law.h`) | NEW. Twelve exposed inner dials. Recommendation: **no change.** | +| 7 | Master gain's dB taper, −∞ … +24 dB, unity ≈ 0.714 norm (`engine/master_gain`) | NEW. Already a shared pure module, which is why the needle and the audio cannot drift; the host joins as a third consumer. Unity is a default → it is a **#3 case**, and the sharpest one, because a host reset that lands a hair off unity is audible as a gain error. | +| 8 | `kPitchDepthMaxSemis` / `kVelocityPitchRangeSemitones` = 24.0 | Already frozen — the v12 wire format stores the pitch curve's y as a fraction of it. Now doubly frozen. The plan already forbids minting a second ±24 constant. | +| 9 | Rate's 50–200 % (±12 st linear) and Pitch's ±24 st centre-expanded laws | Set at birth in Γ-W2-T1, two waves before parameters. **Closed by ordering** — recorded so nobody "tunes Rate's range" in a later phase. | +| 10 | The parameter INDEX order and the `IUnitInfo` unit ids | NEW, softer than the id freeze but user-facing and cached by some hosts. Set once, at Γ-W4-T1. §6.4. | + +**Swept and clear — not doors:** + +- **The params-payload ladder.** Under §6.1 the blob stays authoritative, so payload growth + continues on its own independent axis exactly as before. Automation adds no constraint to + it. +- **`stepCount`.** No discrete parameter is exposed (§7.2), so no step count is frozen. +- **Hold fractions, sustain level, Trigger length.** Natural `[0,1]`, identity taper, nothing + to freeze beyond the identity. +- **The limiter's −0.3 dBTP ceiling, `kMaxCurvePoints` = 128, the editor floor/budget + constants, loop and crossfade frame domains.** None participate in a parameter + normalization. + +**Nothing further was found.** The sweep covered: every constant `ui/deck_values` reads or +writes; every `*Norm` field in the payload ladder; `core/util/curve_law.h`; +`engine/master_gain`; `engine/play_params.h`'s range constants; and the two range constants +Phase Γ itself introduces. + +--- + +## 9. What automation does to the resample bake — and it is not small + +Ξ-W2's bake ends by **resetting the baked-in parameters to default**, "destructive to the +dialled settings by design." Exposing those same values as VST3 parameters changes that step +in two ways. Both are **Γ-W4-T1's** to carry, because Γ-W4-T1 is what creates them. + +1. **The reset must notify the host.** After §6.1, writing the model without a + `beginEdit`/`performEdit`/`endEdit` bracket leaves the host displaying — and, on the next + touch, re-imposing — the pre-bake value. **Every internal writer of a model value that is + an exposed parameter must go through the one notification path**, and the bake's reset is + the first non-gesture writer in the codebase. Enumerating those sites is part of the + track, not a follow-up. + +2. **A host automation lane outranks the reset, and we cannot clear it.** An automation lane + lives in the host's project data. If a reset-class parameter carries one, the lane replays + its curve onto audio that already has that processing baked in — **double processing**, + and Ξ-W2-T1's own acceptance criterion ("after the click… sounds as the dialled instrument + sounded just before the click") does not hold in that case. + + **There is no detection available.** `IAutomationState` reports the host's automation mode + for the whole plug-in, not per parameter (`ivstautomationstate.h:43-53`), and nothing else + in the SDK's edit-controller surface answers "is this parameter automated." So "refuse the + bake when an automated parameter would be reset" and "reset only the un-automated ones" + are both unbuildable, not merely unattractive. + + **Disposition: a named, documented limitation.** The bake resets the plugin's values; it + cannot clear the host's lanes, and the user's remedy is to remove the lane. Stated in the + product doc and in the track's acceptance criteria as a *known* boundary of the bake's + fidelity claim — **not discovered later as a bug report against Ξ.** + +--- + +## 10. Build shape + +**Γ-W4-T1 `vst3-parameter-set`** — one track, the last in Phase Γ, specified in +`docs/PLAN.md`. It depends on all three earlier waves: on Γ-W1-T1 for the taper module that +becomes the host normalization, on Γ-W2-T1 for the three-state predicate and the two new +controls, and on Γ-W3 for the final control inventory and a bake whose reset list is already +complete (`docs/PLAN.md`, Γ-W3-T2). + +**It takes no rung of the params-payload ladder under §6.1** — nothing new is persisted. A +rung is nonetheless **reserved** for it in case the architecture verification (§6.1's +`[verify]`) forces a persisted field; if unspent, the rung falls through to the next phase.