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.
```
+5 -11
View File
@@ -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.120.20 of travel, 100 ms within 0.420.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
+159 -29
View File
@@ -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` §§610: 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.120.20 of travel, 100 ms within 0.420.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 §§17 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 §§17 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
+458 -37
View File
@@ -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: §§15 are the
original analysis (still correct, still the reasoning), and **§§610 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 | 10001099 |
| `1100` | PITCH ENV | 11001199 |
| `1200` | FILTER | 12001299 |
| `1300` | FILTER ENV | 13001399 |
| `1400` | AMP ENVELOPE | 14001499 |
| `1500` | VELOCITY | 15001599 (reserved; empty today — §7.3) |
| `1600` | VOICE | 16001699 (reserved; empty today — §7.2) |
| `1700` | MASTER | 17001799 |
- **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 132 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 (0200 %) | NEW. Becomes the frozen normalization of both key-track parameters. If 0200 % 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 50200 % (±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.