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:
@@ -1,15 +1,24 @@
|
||||
# VST3 automation parameters — scoping, not scheduling
|
||||
# VST3 automation parameters — the Phase Γ specification
|
||||
|
||||
> Daniel, 2026-08-01: *"we need to scope the whole parameter system for automation, which
|
||||
> will require redesigning the existing wiring."* And, on Rate being latched at note-on:
|
||||
> *"we will need to analyze ALL the controls for LIVE use/automation use."*
|
||||
|
||||
**This is NOT Phase Γ work.** It is framing and open questions, written now because Phase Γ
|
||||
makes two decisions whose cost changes permanently the day parameters ship, and because
|
||||
knowing that is what makes Γ's ordering correct rather than arbitrary. Nothing here is
|
||||
scheduled; nothing here is settled.
|
||||
**Superseding ruling, Daniel 2026-08-01 (later the same day):** *"correct the phase gamma
|
||||
plan to account for complying with the VST3 standard for parameter reporting… by the end of
|
||||
gamma we have the automatable params reported. Make the parameter order logical."*
|
||||
|
||||
The one thing this doc *does* assert is a sequencing constraint — §4.
|
||||
**This IS Phase Γ work.** It ships as **Γ-W4-T1 `vst3-parameter-set`**, the phase's last
|
||||
track. This doc was written as scoping and has been promoted in place: §§1–5 are the
|
||||
original analysis (still correct, still the reasoning), and **§§6–10 are the specification**
|
||||
— the ID scheme and its freeze invariant, the per-control classification, the one-way-door
|
||||
sweep, the resample interaction, and the build shape.
|
||||
|
||||
Two things that were open in the scoping pass are now **decided** rather than deferred:
|
||||
the storage architecture (§3.3, decided in §6.1) and the ID space (§3.1, decided in §6.2).
|
||||
One thing is **open and wants Daniel** before Γ-W4 dispatches: **Γ-F7**, the parameter
|
||||
*order* (§6.4). It is not urgent — three waves sit in front of it — but it is a forever
|
||||
commitment, so it is not mine to close silently.
|
||||
|
||||
---
|
||||
|
||||
@@ -61,11 +70,12 @@ class UIDs already carry, and the same discipline the params-payload version lad
|
||||
wants to renumber, regroup or reorder that enum has to happen first. This is not a
|
||||
theoretical concern: Phase Γ adds two entries to it.
|
||||
|
||||
Open: is `ParamID` the `DeckParam` value directly, or an independent id space with an
|
||||
explicit mapping table? Direct is simpler and tempting; an independent space is what lets
|
||||
the enum keep being a UI-ordering convenience. **Lean: independent id space with an explicit
|
||||
frozen table**, on the same reasoning the command-id family already uses — the display order
|
||||
and the wire identity should not be the same number.
|
||||
**DECIDED — §6.2: an independent id space with an explicit frozen table.** Direct
|
||||
`DeckParam` values were simpler and were rejected: they would freeze an enum whose own
|
||||
header declares it *"runtime-only… free to change"*, and they cannot express the cases
|
||||
where the two spaces genuinely differ (a `DeckParam` that is not a parameter, a parameter
|
||||
with no deck cell). The reasoning is the command-id family's: display order and wire
|
||||
identity should not be the same number.
|
||||
|
||||
### 3.2 The taper becomes the host-facing contract
|
||||
|
||||
@@ -101,8 +111,11 @@ Two coherent answers, both viable, neither obviously right:
|
||||
blob contract intact; risks exactly the drift class above if the precedence is not
|
||||
airtight.
|
||||
|
||||
Open. This is the load-bearing architectural decision of that phase and it should be made
|
||||
first, the way Ξ-W2-T1's crossing decision was.
|
||||
**DECIDED — (b), the blob stays authoritative. See §6.1 for the reasoning, the two
|
||||
findings that settled it, and the precedence rule that closes the drift risk.** This was
|
||||
going to be handed to the track as its opening architecture decision, the way Ξ-W2-T1's
|
||||
crossing decision was. It is not, because two verified facts collapse the fork rather
|
||||
than balancing it — a fork with a dominated option in it is not a fork.
|
||||
|
||||
### 3.4 Which controls can be parameters at all — three classes
|
||||
|
||||
@@ -212,7 +225,7 @@ decoupling first, not to re-litigate the classification.
|
||||
|
||||
---
|
||||
|
||||
## 4. The sequencing assertion — the one thing this doc claims
|
||||
## 4. The sequencing assertion — now satisfied inside one phase
|
||||
|
||||
**The knob-taper work (Phase Γ item D) must land before the parameter system. Not
|
||||
alongside; before.**
|
||||
@@ -232,30 +245,438 @@ update; doing it after parameters ship costs a frozen-id decision.
|
||||
the parameter system touch `deck_values.cpp` and `editor_controls.cpp`, and the ordering is
|
||||
not a coin flip: item D first, by a wide margin.
|
||||
|
||||
**What changed when the parameter system moved INTO Phase Γ.** The assertion above was
|
||||
written to justify Γ running before a later phase. It now governs **wave order inside one
|
||||
phase**: Γ-W1-T1 lands the taper, Γ-W4-T1 lands the parameters, and two whole waves of new
|
||||
controls sit between them so that every parameter exists before any parameter is declared.
|
||||
The argument is unchanged; only its blast radius shrank, which is strictly better — the two
|
||||
halves are now reviewed against each other rather than across a phase boundary.
|
||||
|
||||
**And it grew a second clause.** Daniel's ruling *"extend the stage lengths to 10 s"*
|
||||
reverses Γ-F3 and moves `kEnvTimeMaxSeconds` 2.0 → 10.0 into **Γ-W1-T1, beside the taper**,
|
||||
for exactly the reason stated here: a range endpoint is part of the host-facing
|
||||
normalization every bit as much as the curve between the endpoints is. §8 sweeps for every
|
||||
other constant with that property.
|
||||
|
||||
---
|
||||
|
||||
## 5. Open questions, collected
|
||||
## 5. Open questions — status after the promotion
|
||||
|
||||
Not one of these is a Phase Γ blocker. They are what a future phase's first wave answers.
|
||||
The scoping pass collected eight. Seven are closed below; one is open and is a Daniel call.
|
||||
|
||||
1. **Blob vs. parameters as the source of truth** (§3.3) — the architectural decision, made
|
||||
first, before any wiring.
|
||||
2. **`ParamID` space** — direct `DeckParam` values, or an independent frozen table (§3.1).
|
||||
*Lean: independent.*
|
||||
3. **The discrete/rebuild-tier controls** (§3.4) — for each of voice count, Poly/Mono,
|
||||
Retrig/Legato, Gate/Trigger, Staged/Spline, pitch engine, filter law: gain a live path,
|
||||
expose as non-automatable, or omit from the parameter list entirely.
|
||||
4. **Per-parameter live/latched classification** (§3.5) — the three-state predicate Phase Γ
|
||||
opens must be filled in for every exposed parameter, including whether Rate is lifted to
|
||||
live and at what cost.
|
||||
5. **Unit tree shape** (§3.6) — mirror the deck's visual grouping, or the signal flow.
|
||||
6. **Sample-accurate vs. block-boundary** (§3.7). *Lean: block boundary, stated explicitly.*
|
||||
7. **Does the extension's `instrument_drop` need to know?** It writes the same
|
||||
`ComponentState` bytes. If parameters become the source of truth for automatable values
|
||||
(option (a)), a blob the extension writes would no longer fully determine the instrument's
|
||||
sound. That is a cross-artifact contract change and it is the sharpest hidden cost of
|
||||
option (a).
|
||||
8. **Migration.** Every saved project predates parameters. On first load a parameter's value
|
||||
comes from the blob; after that the host owns it. Confirm that path is lossless, and
|
||||
confirm what happens to a project saved *by* the new build and opened by an older one
|
||||
(the blob still carries everything, so it should be fine — verify, do not assume).
|
||||
| # | Question | Status |
|
||||
|---|---|---|
|
||||
| 1 | Blob vs. parameters as the source of truth (§3.3) | **CLOSED — (b), blob authoritative.** §6.1 |
|
||||
| 2 | `ParamID` space (§3.1) | **CLOSED — independent frozen table.** §6.2 |
|
||||
| 3 | The discrete/rebuild-tier controls (§3.4) | **CLOSED — omitted from the list entirely**, not exposed-and-flagged. §7.2 |
|
||||
| 4 | Per-parameter live/latched classification (§3.5) | **CLOSED — the list is DERIVED from the three-state predicate**, never hand-maintained. §7 |
|
||||
| 5 | Unit tree shape (§3.6) | **CLOSED — the deck's groups, in signal-flow order**, layout-independent. §6.4 |
|
||||
| 6 | Sample-accurate vs. block-boundary (§3.7) | **CLOSED — block boundary, stated explicitly.** §6.5 |
|
||||
| 7 | Does the extension's `instrument_drop` need to know? | **CLOSED — no.** It was the sharpest cost of option (a), and (a) is not taken. The blob a drop writes still fully determines the sound. §6.1 |
|
||||
| 8 | Migration | **CLOSED as a design question, OPEN as a verification.** §6.6 |
|
||||
| **Γ-F7** | **Parameter ORDER — signal flow, or the editor's visual rows?** | **OPEN — Daniel.** Recommendation and reasoning at §6.4. Must close before Γ-W4 dispatches; blocks nothing before then. |
|
||||
|
||||
---
|
||||
|
||||
## 6. The specification — architecture, identity, order
|
||||
|
||||
### 6.1 Storage: the blob stays authoritative; parameters are a projection (option (b))
|
||||
|
||||
**Decided, not deferred.** Two verified facts collapse §3.3's fork:
|
||||
|
||||
1. **This plugin is a `SingleComponentEffect`, so there is only ONE state.** The SDK's own
|
||||
header `#define`s `setState`/`getState` to `setEditorState`/`getEditorState` around its
|
||||
include of `vsteditcontroller.h`, *"work around for the name clash of
|
||||
IComponent::setState and IEditController::setState"*
|
||||
(`public.sdk/source/vst/vstsinglecomponenteffect.h:41-47`). `setComponentState` exists to
|
||||
feed a **separate** controller in a split-component plug-in; we have no separate
|
||||
controller. §3.3's "a value that lives in both can drift, depending on call order"
|
||||
describes a hazard of a design we do not use. **The drift class it warns about is
|
||||
substantially smaller than the scoping pass assumed.**
|
||||
2. **The blob is a cross-artifact contract.** The extension's `instrument_drop` writes the
|
||||
identical bytes to build a transient `.vstpreset` (`map/component_state_io.h`). Under
|
||||
option (a) a blob the extension writes would no longer fully determine the sound — a
|
||||
shipped seam would silently become partial. That is not a trade, it is a regression.
|
||||
|
||||
Add the repo's own invariant — *"the instrument holds ONE loaded capture and ONE set of
|
||||
playback parameters… every playback parameter edits in exactly one place"* — and option (a),
|
||||
which splits the parameter set across two storage mechanisms, is arguing against the
|
||||
invariant Θ-W1-T1 was run to establish.
|
||||
|
||||
**The rule, stated so it cannot drift:**
|
||||
|
||||
> **`InstrumentParams` / `PlaySeconds` is THE model. The `ComponentState` blob is its only
|
||||
> persistence. A VST3 parameter is a THIRD SURFACE onto that model — a peer of the deck
|
||||
> knob and the overlay node, not a second copy of the value.**
|
||||
>
|
||||
> - **Load:** the blob restores the model; every exposed parameter's normalized value is
|
||||
> then *derived* from the model and pushed to the controller. The SDK is explicit that
|
||||
> this push must not be reflected back to the host: *"The controller must never pass this
|
||||
> value-change back to the host via the IComponentHandler. It should update the according
|
||||
> GUI element(s) only"* (`ivsteditcontroller.h:430-433`).
|
||||
> - **Host → plugin:** a parameter change writes the model through the **existing commit
|
||||
> tiers** (`shell/instrument/CLAUDE.md`) — live publish or note-on latch. It never takes a
|
||||
> fourth route.
|
||||
> - **Plugin → host:** an editor gesture writes the model AND notifies the host, bracketed
|
||||
> `beginEdit` / `performEdit` / `endEdit` (`ivsteditcontroller.h:175-185`), on the UI
|
||||
> thread as the SDK requires.
|
||||
> - **Save:** `getState` serializes the model. Nothing new is persisted.
|
||||
>
|
||||
> **The residual risk is not drift, it is authority:** a host automation lane, once written,
|
||||
> outranks anything the plugin sets, because the host replays it. That is inherent to
|
||||
> automation and is not a defect to design away — but it has one sharp consequence for the
|
||||
> resample bake, and that is §9.
|
||||
|
||||
**[verify] at the track, before wiring:** that REAPER calls `setState` (not
|
||||
`setComponentState`) on a single-component plug-in, and the ordering of `setState` against
|
||||
the first `IParameterChanges` block after a project load. Verify against the vendored SDK
|
||||
and in the DAW — do not build on the paragraph above without it.
|
||||
|
||||
### 6.2 The ID space: hand-assigned constants in one frozen table
|
||||
|
||||
**`ParamID` is an independent space from `DeckParam`, defined by a hand-written table of
|
||||
named constants in one pure module.** Not derived, not computed, not an enum cast.
|
||||
|
||||
**Why hand-assigned rather than derived.** A derived id is a function of something else —
|
||||
enum position, group index, a hash of a name — and every one of those inputs is a thing
|
||||
that must then never change. Deriving does not remove the freeze; it hides it behind a
|
||||
mechanism, and the day someone reorders the source enum the ids move silently. A literal
|
||||
table makes the freeze **visible at the point of change**: you cannot renumber it by
|
||||
accident, because renumbering it means editing the numbers.
|
||||
|
||||
**The layout — blocks of 100 per deck group, steps of 10 within a block:**
|
||||
|
||||
| Block | Group | Reserved |
|
||||
|---|---|---|
|
||||
| `1000` | PITCH/RATE | 1000–1099 |
|
||||
| `1100` | PITCH ENV | 1100–1199 |
|
||||
| `1200` | FILTER | 1200–1299 |
|
||||
| `1300` | FILTER ENV | 1300–1399 |
|
||||
| `1400` | AMP ENVELOPE | 1400–1499 |
|
||||
| `1500` | VELOCITY | 1500–1599 (reserved; empty today — §7.3) |
|
||||
| `1600` | VOICE | 1600–1699 (reserved; empty today — §7.2) |
|
||||
| `1700` | MASTER | 1700–1799 |
|
||||
|
||||
- **Within a block, ids step by 10** in the group's own left-to-right cell order. Nine free
|
||||
slots sit between every pair of neighbours, so **a control added later lands numerically
|
||||
adjacent to its siblings instead of appended at the end of the whole table.** That is the
|
||||
concrete content of "make the parameter order logical": the order is a property of the
|
||||
numbering scheme, maintained by construction, not by remembering to re-sort.
|
||||
- **A curve-exponent inner dial takes its outer knob's id + 1.** The dial is a property of
|
||||
the knob it sits inside — the codebase already says so (`curveParamFor`, "each is the
|
||||
INNER DIAL of the stage knob it shapes") — so its id says so too. This is why the step is
|
||||
10 and not 2: it leaves room for the dial *and* for a future sibling knob.
|
||||
- **Blocks start at 1000, not 0.** `0` is a plausible accident (a default-initialised
|
||||
`ParamID`), and a scheme whose first legitimate id is also its most likely bug value is a
|
||||
scheme that cannot tell them apart.
|
||||
- **An empty block is still reserved.** VELOCITY and VOICE expose nothing today. Their
|
||||
blocks exist so that if either ever gains an automatable control it lands in its own
|
||||
group's range rather than in whatever range happened to be free.
|
||||
|
||||
### 6.3 The freeze invariant
|
||||
|
||||
State this in the ID table's header, with the same force as its three siblings.
|
||||
|
||||
> **THE PARAMETER-ID TABLE IS FOREVER-FROZEN.** A VST3 `ParamID` is recorded by the host
|
||||
> into automation lanes inside project files this repo does not own and cannot migrate.
|
||||
> From the first shipped build that exposes parameters:
|
||||
>
|
||||
> - **No id is ever reassigned, reused, or re-pointed.** If a control's meaning genuinely
|
||||
> changes, it takes a NEW id and the old one is **retired** — marked dead in the table,
|
||||
> never re-issued.
|
||||
> - **No exposed parameter's normalization ever changes** — not its taper, not either range
|
||||
> endpoint, not its `stepCount`. The normalization *is* the meaning of every recorded
|
||||
> automation point. §8 enumerates every constant this now freezes.
|
||||
> - **A parameter's meaning never depends on a mode.** The Gate-face and Trigger-face stage
|
||||
> times are separate stored fields and take separate ids; an id whose plain value means
|
||||
> one thing in Gate and another in Trigger is unautomatable by construction.
|
||||
> - **A new control takes the next free slot inside its own group's block**, never the next
|
||||
> number at the end of the table.
|
||||
>
|
||||
> This sits on the same footing as the extension's `"STABLE_FOREVER_STRING"` command ids
|
||||
> (root `CLAUDE.md`, "REAPER extension contract"), the two VST3 class UIDs
|
||||
> (`core/wire/reasampler_uid.h`, FOREVER-FROZEN), and the params-payload field order
|
||||
> (`map/component_state_io.h`, "EVERY wire format below is FROZEN"). It is the fourth
|
||||
> member of that family, not a new kind of rule.
|
||||
|
||||
**One property the table BUYS, and it is the reason to prefer it over deriving:**
|
||||
`DeckParam`'s header says the enum is *"runtime-only… free to change."* With an independent
|
||||
table that stays true — the reflow, a future regrouping, a new deck can all reorder the enum
|
||||
freely. Deriving ids from it would have quietly repealed that sentence.
|
||||
|
||||
### 6.4 Order and grouping — Γ-F7, OPEN
|
||||
|
||||
Two orders exist and they are not the same thing:
|
||||
|
||||
- **Identity order** — the id numbers, frozen by §6.3.
|
||||
- **Presentation order** — the sequence `getParameterInfo(index)` reports, which is what a
|
||||
host's FX-parameter list and every automation-lane picker display.
|
||||
|
||||
They should agree (a list whose numbering runs backwards against its display is a trap), so
|
||||
this is one decision, and it is a forever one on both counts.
|
||||
|
||||
**Recommendation: signal-flow order — PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP →
|
||||
VELOCITY → VOICE → MASTER**, which is the deck's own documented ordering rule
|
||||
(`sampleDeckGroups`, *"left to right, in SIGNAL-FLOW order: pitch → filter → amp, then the
|
||||
two instance-wide groups"*), with each group's cells in their own left-to-right order.
|
||||
|
||||
**The alternative is the editor's visual rows** after Γ-W3-T1 — row 1 PITCH/RATE, FILTER,
|
||||
VELOCITY, VOICE; row 2 PITCH ENV, FILTER ENV, AMP; MASTER spanning. Same membership,
|
||||
different sequence.
|
||||
|
||||
**Why the recommendation goes to signal flow:** the visual layout is a canvas-constrained
|
||||
artifact that has already moved twice (Θ-W6-T1 grew the floor 840 → 980, Γ-W3-T1 takes it to
|
||||
1190 and re-rows every group), and within-row order is decided by *width fitting*, not by
|
||||
meaning. Freezing the host-facing order to a layout that demonstrably moves is the wrong
|
||||
coupling; signal flow has never moved and is the deck's own stated rule. §3.6 asked whether
|
||||
the unit tree mirrors the visual grouping or the signal flow and noted "they currently
|
||||
agree" — the reflow is exactly what makes them stop agreeing, so the question has to be
|
||||
answered rather than deferred again.
|
||||
|
||||
**Why it is nonetheless Daniel's:** the counter-argument is real — a user hunting a
|
||||
parameter in REAPER's flat list finds it fastest if the list reads in the order their eyes
|
||||
scan the plugin window. If Daniel weights find-it-on-screen over layout-independence, visual
|
||||
order is the right answer and it is not a mistake; it just also means the parameter order
|
||||
stops being free the next time the deck is re-laid.
|
||||
|
||||
**Grouping is settled either way: `IUnitInfo`, one unit per deck group**, mirroring the
|
||||
group inventory (not the rows). Only the sequence is at issue.
|
||||
|
||||
### 6.5 What the plugin must implement, as behaviour
|
||||
|
||||
Not architecture — the engineer's call is *how*. These are the observable contracts:
|
||||
|
||||
- **`getParameterCount` / `getParameterInfo`** report exactly the §7 list, in §6.4's order,
|
||||
each with: the frozen `id`; a `title` that survives truncation and a distinct
|
||||
`shortTitle`; `units` (`"ms"`, `"st"`, `"%"`, `"dB"`, `""` for exponents) matching what the
|
||||
editor prints, so the host's readout and the knob's label never disagree; `stepCount = 0`
|
||||
(every exposed parameter is continuous — §7.2 is why); `defaultNormalizedValue` per §8's
|
||||
exactness rule; the group's `unitId`; and `kCanAutomate`.
|
||||
- **`kIsBypass` is not set on anything.** The limiter is a safety device, not a bypass, and
|
||||
binding it to `kIsBypass` would hand the host a control that restarts the component
|
||||
(§3.8). The plugin exposes **no** bypass parameter — it is an instrument.
|
||||
- **`setParamNormalized` / `getParamNormalized`** are the model projection of §6.1, not a
|
||||
cached shadow value.
|
||||
- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` /
|
||||
`getParamValueByString` all route through the ONE taper module Γ-W1-T1 extracts.** This is
|
||||
the structural point of that extraction: the host's normalization, the knob's needle
|
||||
angle, and the overlay node's position must be the *same function*, not three functions
|
||||
that agree today. `getParamStringByValue` reuses the editor's own formatters
|
||||
(`formatEnvTimeMs` and its peers) for the same reason.
|
||||
- **`IParameterChanges` is observed at BLOCK boundaries** — the last point in the block wins
|
||||
for that block (§3.7). Sample-accurate application would put a per-sample "did anything
|
||||
change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids.
|
||||
**State it in the header rather than leaving it to be discovered**; revisit only if
|
||||
audible stepping is reported on a fast curve.
|
||||
- **A host parameter change takes the control's existing commit tier and no other.** Live
|
||||
parameters publish; note-on-latched parameters publish and are read by `snapLive`. Nothing
|
||||
on the automation path may reach `reloadInstrument` or `rebuildVoiceEngine` — which is
|
||||
precisely what §7.2's omissions guarantee.
|
||||
- **`IMidiMapping` is in scope and nearly free** (`ivsteditcontroller.h:558-568`): declaring
|
||||
a CC → `ParamID` map costs one function and gives every exposed parameter MIDI control.
|
||||
**[propose at review]** whether to ship a default map in this track or leave it to REAPER's
|
||||
host-side MIDI learn; do not silently skip it without saying so.
|
||||
- **`IParameterFunctionName` is assessed and NOT implemented**
|
||||
(`ivstparameterfunctionname.h`). Its vocabulary is compressor/panner/dry-wet semantics
|
||||
(`Comp:GainReduction`, `DryWetMix`, `Randomize`); none of it names anything this
|
||||
instrument exposes. Recorded so it is not re-surveyed.
|
||||
- **`IAutomationState` is assessed and NOT implemented**
|
||||
(`ivstautomationstate.h`). It reports the host's automation mode for the **whole plug-in**
|
||||
(`kNoAutomation` / `kReadState` / `kWriteState`), not per parameter — so it cannot answer
|
||||
"is THIS parameter automated," which is the only question §9 would have wanted it for.
|
||||
|
||||
### 6.6 Migration — the verification, not a design
|
||||
|
||||
Every saved project predates parameters, and under §6.1 that is a non-event: the blob
|
||||
restores the model and the parameters are derived from it. Three things are nonetheless
|
||||
**[verify], not assume**:
|
||||
|
||||
1. A pre-parameter project opens with every parameter reading the blob's value, and sounds
|
||||
identical.
|
||||
2. A project saved by the new build and opened by an **older** binary still restores fully —
|
||||
the blob carries everything, so it should; prove it.
|
||||
3. A project with automation drawn on a parameter, saved and reopened, replays that
|
||||
automation against the same plain values — the round trip through the frozen taper is
|
||||
exact at both ends.
|
||||
|
||||
---
|
||||
|
||||
## 7. The classification — which controls become parameters
|
||||
|
||||
**The list is DERIVED, never hand-maintained.** Its rule is one line:
|
||||
|
||||
> **A control is an exposed VST3 parameter if and only if its commit class is `Live` or
|
||||
> `NoteOnLatched`.** Everything else is omitted from the parameter list entirely.
|
||||
|
||||
That makes `isLiveDeckParam` / `liveCommitFor` — already *"THE home for why each excluded
|
||||
control is excluded"* — the single source for the parameter list too, which is the standing
|
||||
rule (`core/instrument/CLAUDE.md`: *"which controls are live is ONE decision, recorded in ONE
|
||||
place"*) applied once more rather than a second table opened beside it.
|
||||
|
||||
### 7.1 Exposed — 44 parameters
|
||||
|
||||
Counts are against the control inventory as it stands at the end of Γ-W3; the track asserts
|
||||
the count against the predicate rather than against this table.
|
||||
|
||||
| Group | Parameters | Class |
|
||||
|---|---|---|
|
||||
| **PITCH/RATE** (3) | Rate; Pitch offset; Key-track | Rate **NoteOnLatched** (Γ, settled); Pitch **Live**; Key-track **NoteOnLatched** (see §7.4) |
|
||||
| **PITCH ENV** (6) | attack, hold-fraction, decay, depth + attack/decay curve exponents | **Live** |
|
||||
| **FILTER** (7) | morph, cutoff, Q, drive, mod amount, velocity amount, key-track | **Live** |
|
||||
| **FILTER ENV** (13) | AHDSR A/H/D/S/R; Trigger AHD A/H/D; five curve exponents | **Live** |
|
||||
| **AMP ENVELOPE** (14) | AHDSR A/H/D/S/R; Trigger AHD A/H/D; Trigger length; five curve exponents | **Live**, except Trigger length **NoteOnLatched** (§7.4) |
|
||||
| **VELOCITY** (0) | — | its three cells are curve popups, not scalars (§7.3) |
|
||||
| **VOICE** (0) | — | rebuild tier (§7.2) |
|
||||
| **MASTER** (1) | Master gain | **Live** |
|
||||
|
||||
**Both play-mode faces are exposed simultaneously**, because both are stored simultaneously
|
||||
and always have been. A Trigger stage time is a real parameter whose effect is silent while
|
||||
Gate is selected — which is the same relationship a disabled filter's cutoff already has, and
|
||||
far better than an id whose meaning depends on a mode (§6.3).
|
||||
|
||||
### 7.2 Omitted — the reload and rebuild tiers, and why "omit" beats "expose read-only"
|
||||
|
||||
**Discrete / reload tier:** play mode (Gate|Trigger), pitch engine (Varispeed|Preserve),
|
||||
filter enable, filter morph law, pitch-envelope enable, the three Staged|Spline mode
|
||||
toggles. **Rebuild tier:** voice count, Poly|Mono, Retrigger|Legato. **Latency-changing:**
|
||||
the limiter enable (§3.8 — settled, and its one reopening condition is named there).
|
||||
|
||||
The scoping pass left three options (gain a live path / expose non-automatable / omit).
|
||||
**Omit.** An exposed-but-not-automatable parameter is still listed in the host's generic
|
||||
panel and is still *settable from it*, which would let a generic-panel drag reach
|
||||
`reloadInstrument` from a host thread — strictly worse than the control being absent. VST3's
|
||||
`kIsReadOnly` would prevent that, at the cost of a visible row the user cannot move, in a
|
||||
list whose whole purpose is things you can move. Neither beats absence.
|
||||
|
||||
**Named limitation, stated rather than discovered:** the user cannot automate filter on/off,
|
||||
play mode, the pitch engine, Staged↔Spline, or polyphony. The unlock is written down and is
|
||||
not this track's: **give the control a live path first, then it qualifies by the same
|
||||
rule that excluded it.** Nothing about the parameter system needs to change to admit it.
|
||||
|
||||
**One door this closes by omission** — if voice count were exposed, its 1–32 ceiling would
|
||||
become a frozen `stepCount` and could never be raised. It is not exposed, so it is not
|
||||
frozen; a future "expose voice count" proposal must accept that freeze as part of the price.
|
||||
|
||||
### 7.3 Omitted — structural state
|
||||
|
||||
The loaded capture and `SampleRefs`; root note; loop start/end, the loop crossfade, and
|
||||
**the new loop enable** (Γ-W2-T2, user-owned from that track on); start frame; channel mode;
|
||||
the three velocity transfer curves; the three spline contours; the three overlay-select
|
||||
radios (view state, never persisted); preview velocity (a utility, not a musical parameter).
|
||||
|
||||
None of these are scalars with a taper, and several are not scalars at all. They stay in the
|
||||
blob, which is exactly what §6.1's split is for.
|
||||
|
||||
### 7.4 Two promotions the three-state predicate makes available
|
||||
|
||||
Γ-W2-T1 introduces `NoteOnLatched` for Rate. Two controls that are excluded from the live
|
||||
set **for the note-on-latch reason** currently route through the reload tier, and the new
|
||||
state fits them exactly:
|
||||
|
||||
- **Key-track** — `isLiveDeckParam`'s header already says it *"feed[s] values a voice
|
||||
latches at note-on by design (the pitch ratio…), so live delivery would retune… a note
|
||||
already struck."* That sentence describes `NoteOnLatched`, not `Reload`.
|
||||
- **Trigger length** — *"resolves `playEnd_`, a fact about the note, not a setting of it."*
|
||||
Same shape.
|
||||
|
||||
**[propose at review, Γ-W4-T1]** promote both. The promotion aligns the routing with the
|
||||
predicate's own stated semantics — and it is what makes them automatable, since today they
|
||||
would re-decode a WAV per automation point. **If either promotion is refused, that control
|
||||
simply drops out of the parameter list.** The list follows the predicate; the predicate is
|
||||
never bent to fill the list.
|
||||
|
||||
**Not promoted, and not proposed for promotion: Rate to Live.** §3.5 records the cost;
|
||||
that paragraph is the first thing to read if it is ever proposed.
|
||||
|
||||
---
|
||||
|
||||
## 8. One-way doors — the sweep
|
||||
|
||||
**Deliverable, run 2026-08-01 against `dev`.** Once parameters ship, anything that
|
||||
participates in a parameter's normalization is frozen as hard as the id itself. This is the
|
||||
complete list of such constants, with a disposition for each. **Two were already known; five
|
||||
are new; three are already frozen for an unrelated reason and are recorded so they are not
|
||||
"tidied" later.**
|
||||
|
||||
**Scheduled ahead of the parameter track — action required:**
|
||||
|
||||
| # | Constant / property | Disposition |
|
||||
|---|---|---|
|
||||
| 1 | **The ms and semitone tapers** (`ui/deck_values`) | Γ-W1-T1. Known. The taper *is* the host normalization. |
|
||||
| 2 | **`kEnvTimeMaxSeconds` / `kGateStageMaxSeconds` 2.0 → 10.0** | Γ-W1-T1, per Daniel's ruling. A range endpoint is normalization. Γ-F3 reversed. |
|
||||
| 3 | **Every default must have an EXACT normalized preimage** — NEW | Γ-W1-T1 acceptance criterion. `ParameterInfo::defaultNormalizedValue` is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)`. Γ-W1-T1 makes `resetDeckParam` **bypass** the taper, which fixes the editor's reset and **cannot fix the host's** — the host has no bypass. The taper must therefore be designed so every default round-trips exactly, or the host's reset lands a hair off and the "double-click lands exactly on its default" criterion holds on one surface and not the other. **This is the sharpest new finding in the sweep.** |
|
||||
| 4 | **Do NOT re-taper the filter's four normalized controls** — NEW | Γ-W1-T1 exclusion. Cutoff/Q/morph/drive persist as `cutoffNorm`/`resonanceNorm`/`morphNorm`/`driveNorm` in the **v9 payload** — their laws are *already* wire-frozen today, and re-mapping them would re-tune every saved project independently of automation. §4.2's snap-unit table names them, which is a display concern; nothing in the taper pass may touch their norm↔plain law. |
|
||||
|
||||
**Frozen by this work — record only, no change wanted:**
|
||||
|
||||
| # | Constant | Note |
|
||||
|---|---|---|
|
||||
| 5 | `kKeyTrackMax` = 2.0 (0–200 %) | NEW. Becomes the frozen normalization of both key-track parameters. If 0–200 % is ever going to be thought wrong, now is when it is free. Recommendation: **no change.** |
|
||||
| 6 | The curve-exponent domain 0.1 … 10 and its log travel (`core/util/curve_law.h`) | NEW. Twelve exposed inner dials. Recommendation: **no change.** |
|
||||
| 7 | Master gain's dB taper, −∞ … +24 dB, unity ≈ 0.714 norm (`engine/master_gain`) | NEW. Already a shared pure module, which is why the needle and the audio cannot drift; the host joins as a third consumer. Unity is a default → it is a **#3 case**, and the sharpest one, because a host reset that lands a hair off unity is audible as a gain error. |
|
||||
| 8 | `kPitchDepthMaxSemis` / `kVelocityPitchRangeSemitones` = 24.0 | Already frozen — the v12 wire format stores the pitch curve's y as a fraction of it. Now doubly frozen. The plan already forbids minting a second ±24 constant. |
|
||||
| 9 | Rate's 50–200 % (±12 st linear) and Pitch's ±24 st centre-expanded laws | Set at birth in Γ-W2-T1, two waves before parameters. **Closed by ordering** — recorded so nobody "tunes Rate's range" in a later phase. |
|
||||
| 10 | The parameter INDEX order and the `IUnitInfo` unit ids | NEW, softer than the id freeze but user-facing and cached by some hosts. Set once, at Γ-W4-T1. §6.4. |
|
||||
|
||||
**Swept and clear — not doors:**
|
||||
|
||||
- **The params-payload ladder.** Under §6.1 the blob stays authoritative, so payload growth
|
||||
continues on its own independent axis exactly as before. Automation adds no constraint to
|
||||
it.
|
||||
- **`stepCount`.** No discrete parameter is exposed (§7.2), so no step count is frozen.
|
||||
- **Hold fractions, sustain level, Trigger length.** Natural `[0,1]`, identity taper, nothing
|
||||
to freeze beyond the identity.
|
||||
- **The limiter's −0.3 dBTP ceiling, `kMaxCurvePoints` = 128, the editor floor/budget
|
||||
constants, loop and crossfade frame domains.** None participate in a parameter
|
||||
normalization.
|
||||
|
||||
**Nothing further was found.** The sweep covered: every constant `ui/deck_values` reads or
|
||||
writes; every `*Norm` field in the payload ladder; `core/util/curve_law.h`;
|
||||
`engine/master_gain`; `engine/play_params.h`'s range constants; and the two range constants
|
||||
Phase Γ itself introduces.
|
||||
|
||||
---
|
||||
|
||||
## 9. What automation does to the resample bake — and it is not small
|
||||
|
||||
Ξ-W2's bake ends by **resetting the baked-in parameters to default**, "destructive to the
|
||||
dialled settings by design." Exposing those same values as VST3 parameters changes that step
|
||||
in two ways. Both are **Γ-W4-T1's** to carry, because Γ-W4-T1 is what creates them.
|
||||
|
||||
1. **The reset must notify the host.** After §6.1, writing the model without a
|
||||
`beginEdit`/`performEdit`/`endEdit` bracket leaves the host displaying — and, on the next
|
||||
touch, re-imposing — the pre-bake value. **Every internal writer of a model value that is
|
||||
an exposed parameter must go through the one notification path**, and the bake's reset is
|
||||
the first non-gesture writer in the codebase. Enumerating those sites is part of the
|
||||
track, not a follow-up.
|
||||
|
||||
2. **A host automation lane outranks the reset, and we cannot clear it.** An automation lane
|
||||
lives in the host's project data. If a reset-class parameter carries one, the lane replays
|
||||
its curve onto audio that already has that processing baked in — **double processing**,
|
||||
and Ξ-W2-T1's own acceptance criterion ("after the click… sounds as the dialled instrument
|
||||
sounded just before the click") does not hold in that case.
|
||||
|
||||
**There is no detection available.** `IAutomationState` reports the host's automation mode
|
||||
for the whole plug-in, not per parameter (`ivstautomationstate.h:43-53`), and nothing else
|
||||
in the SDK's edit-controller surface answers "is this parameter automated." So "refuse the
|
||||
bake when an automated parameter would be reset" and "reset only the un-automated ones"
|
||||
are both unbuildable, not merely unattractive.
|
||||
|
||||
**Disposition: a named, documented limitation.** The bake resets the plugin's values; it
|
||||
cannot clear the host's lanes, and the user's remedy is to remove the lane. Stated in the
|
||||
product doc and in the track's acceptance criteria as a *known* boundary of the bake's
|
||||
fidelity claim — **not discovered later as a bug report against Ξ.**
|
||||
|
||||
---
|
||||
|
||||
## 10. Build shape
|
||||
|
||||
**Γ-W4-T1 `vst3-parameter-set`** — one track, the last in Phase Γ, specified in
|
||||
`docs/PLAN.md`. It depends on all three earlier waves: on Γ-W1-T1 for the taper module that
|
||||
becomes the host normalization, on Γ-W2-T1 for the three-state predicate and the two new
|
||||
controls, and on Γ-W3 for the final control inventory and a bake whose reset list is already
|
||||
complete (`docs/PLAN.md`, Γ-W3-T2).
|
||||
|
||||
**It takes no rung of the params-payload ladder under §6.1** — nothing new is persisted. A
|
||||
rung is nonetheless **reserved** for it in case the architecture verification (§6.1's
|
||||
`[verify]`) forces a persisted field; if unspent, the rung falls through to the next phase.
|
||||
|
||||
Reference in New Issue
Block a user