docs: schedule VST3 parameters into Phase Γ

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.
This commit is contained in:
2026-08-01 17:26:18 -04:00
parent 256216d670
commit 2fa55658c1
4 changed files with 1278 additions and 202 deletions
+656 -125
View File
@@ -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 —
§§610 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` §§610**. Read §1.2 (the layout
table) and §7 (collisions) before dispatching any track here — every number in this phase is
derived there, and `docs/TODO.md`'s old deck-rework geometry is superseded.
**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**: §§15 are the original analysis, **§§610 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.120.20 of
travel and 100 ms within 0.420.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.420.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 `<module>_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 5058 % 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 `<module>_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 `<module>_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` — **§§610 are the specification; §§15 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 `<module>_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` §§610. **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.
```