docs: close Γ-F7 to signal-flow order and spec real units at the host boundary

The 44-id table stated in full. Adds the plain-value layer, a per-category
unit/precision table, the one-formatter invariant, a stepCount sweep, and the
filter read-side resolution.
This commit is contained in:
2026-08-01 18:18:09 -04:00
parent 9f17df1420
commit 0a7778b396
3 changed files with 556 additions and 108 deletions
+148 -48
View File
@@ -50,13 +50,20 @@ restart expensive *here* is self-inflicted (`setActive(true)` calls `reloadInstr
the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed
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.**
**Γ-F3 was subsequently REVERSED and a seventh fork opened AND CLOSED, all 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. **Γ-F7** (the VST3 parameter
*order*) is **RULED: signal flow***"signal flow order."* **There is now NO unanswered
[Daniel]-class question anywhere in this plan.**
**Ruling 3 (Daniel, 2026-08-01) — real units at the host boundary.** *"The parameter values
exposed to the VST host should be in real units, such that the host automation lanes report
usable values."* Satisfied through VST3's **plain-value layer**, not its wire format (which is
normalized and cannot be otherwise): `toPlain`/`toNormalized`, `getParamStringByValue` and
`ParameterInfo::units`. Specified at **`docs/product/parameter-automation.md` §6.7** — the
per-category unit/precision table, the one-formatter invariant, the `stepCount` sweep, and the
resolution of the apparent conflict with the filter's re-taper prohibition. It lands entirely
in Γ-W4-T1 and changes no wave boundary.
### Flagged for awareness — not blocking, but decision-grade
@@ -107,7 +114,7 @@ unanswered [Daniel]-class question: Γ-W4-T1, and it is Γ-F7.**
## Phase-wide acceptance criteria
These bind every track in all three phases and are stated once here rather than repeated
per track. **Phase Γ adds five of its own**, stated in its phase header.
per track. **Phase Γ adds a set of its own**, stated in its phase header.
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
@@ -534,7 +541,7 @@ system's is in **`docs/product/parameter-automation.md` §§610**. Read §1.2
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 — six ruled, ONE OF THEM LATER REVERSED, and one open.** Indexed at spec §8,
**Fork state — SEVEN ruled, ONE OF THEM LATER REVERSED, NONE 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,
@@ -554,24 +561,31 @@ 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.
- **Γ-F7 — RULED: SIGNAL FLOW.** Daniel, 2026-08-01: *"signal flow order."* The VST3 parameter
order — both the frozen id numbering and the `getParameterInfo` presentation index — is
**PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE → MASTER**, the
deck's own `sampleDeckGroups` rule, with each group's cells in the semantic order the id
table freezes. The editor's visual rows after the reflow were the rejected alternative.
**The reason, because a future reader will ask why the id order does not match the screen:**
the editor's layout 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 settled by width fitting, not by
meaning — so **binding a permanently-frozen id order to a demonstrably mobile layout
guarantees the two drift apart**, after which the order is neither logical nor matching.
Signal flow is the axis that does not move. Full argument and the accepted residual cost:
`docs/product/parameter-automation.md` §6.4; **the resulting 44-id table is stated at §6.2.**
**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
specification** Γ-W4-T1 is built from. Four 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`
(§6.1); the id space is an independent, hand-assigned, FOREVER-FROZEN table, **now stated in
full as 44 numbered rows in signal-flow order** (§6.2, §6.3); the exposed list is **derived
from the three-state commit predicate**, never hand-maintained (§7); and, under Ruling 3,
every parameter's **plain unit, range and display precision** (§6.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.
@@ -700,21 +714,51 @@ exact interim layout; do not "fix" it in a track that does not own it.
load-bearing in the v12 wire format. The new Pitch knob **reads** it; it does not mint a
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.
- **The taper has exactly ONE home and three consumers, and from Γ-W4-T1 the taper IS the
host's `toPlain`/`toNormalized`.** Γ-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` / `plainParamToNormalized` (Γ-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. Under Ruling 3 this stops being an analogy: `toNormalized` is
not *like* the taper, it *is* the taper (`docs/product/parameter-automation.md` §6.7.3).
- **ONE formatter per unit category, and the editor and the host are both its callers.** The
formatter is pure and returns the **digits** of a plain value in that category's single
`units` string — no embedded unit, no magnitude-switched unit, no width-conditional
abbreviation, no caller-side branch. The editor's knob label and
`getParamStringByValue` read the same function. **The editor and the host printing
different text for the same stored value is a defect class, forbidden structurally rather
than caught at review** — same discipline, same reason, as the taper criterion above.
Consequences (existing formatters stop embedding their unit; cutoff's `k` abbreviation is
retired; the curve dial's `^` is static cell chrome, not value): spec §6.7.2.
- **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.
editor's reset and the host's reset land on the same value. **Ruling 3 tightens this twice
and adds one non-requirement** (§6.7.7): `defaultNormalizedValue` is **computed** as
`toNormalized(default)`, never written as a normalized literal; the assertion is made on
`toPlain(defaultNormalizedValue)`, the pair the host actually calls; and
`toNormalized(toPlain(n)) == n` at **arbitrary** n is explicitly NOT required — no log map
satisfies it in double, and demanding it would over-constrain the taper for nothing.
- **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.
automation. The snap-unit table names them; that is display, not law. **This does NOT
conflict with Ruling 3's real-unit requirement**, and the two must not be read as a
collision: `toPlain` is a pure read-side mapping that never touches the stored value, so
reporting Hz / Q / drive depth means **calling** `filterCutoffHzFromNorm` and its peers, not
replacing them — which the editor's own labels already do today. The prohibition forbids
*editing* those laws; the requirement is satisfied by *calling* them. One additive gap:
drive has no published inverse and `filterNormFromDriveDepth` must be added beside the two
that exist — the analytic inverse of a frozen law is not a change to it. Spec §6.7.5.
- **Shift-snap is a drag rule; `stepCount` is a parameter property; they are independent.**
The editor's snap grid must never be exposed as `ParameterInfo::stepCount` — that would
quantize the parameter itself, permanently and for the host's automation too, freezing the
grid into the forever contract and putting continuous cents out of reach from a lane.
**All 44 exposed parameters ship `stepCount = 0`**, swept and confirmed, and the coincidence
is structural: every discrete control is reload or rebuild tier and therefore omitted by the
predicate. Spec §6.7.6.
- **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
@@ -1835,10 +1879,12 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
**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.**
deck group **in signal-flow order** (Γ-F7), steps of 10 within a block, a curve dial at its
outer knob's id + 1, blocks starting at 1000. **The full 44-id assignment is stated at
§6.2** and is to be transcribed, not re-derived. §6.2 also for why hand-assignment beats
derivation and why the within-block order is seeded ONCE rather than tracked against
`cellIds`; **§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.
@@ -1847,18 +1893,41 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
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.
- **Every exposed parameter reports REAL UNITS to the host** (Ruling 3). Each declares a
**plain range**, a **`units` string**, and a **display precision**; the complete
eight-category table covering all 44 — ranges, units, precision, and which taper each
category carries — is `docs/product/parameter-automation.md` §6.7.1, and it is a
specification, not a suggestion. VST3's wire format stays normalized (it cannot be
otherwise); the requirement is met through the **plain-value layer** the SDK provides, whose
direct precedent in the vendored tree is `public.sdk/samples/vst/common/logscale.h:221-229`
overriding `toPlain`/`toNormalized` for a log law — the exact shape our log ms and log2
semitone knobs need.
- **`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.
`getParamValueByString` route through W1-T1's taper module and the ONE formatter per unit
category.** Three functions that agree today is a defect; the host's normalization, the
needle angle and the overlay node must be the same function. **`toNormalized` IS the
taper** — §6.7.3 — which is what makes the phase's one-way-door ordering structurally true
rather than a warning someone has to remember.
- **`stepCount = 0` on all 44, and the editor's shift-snap is never exposed as `stepCount`.**
Snapped drag and parameter continuity are independent axes; `stepCount` quantizes the
parameter permanently, including for the host's automation, and freezes into the forever
contract. The sweep is done and clean (§6.7.6) — every discrete control is reload or rebuild
tier and therefore already omitted, so continuity is structural rather than lucky.
- **`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.**
- **`IUnitInfo`: one unit per deck group**, mirroring the group inventory rather than the
editor's rows. **Order is signal flow — Γ-F7, RULED** (§6.4). Presentation index order is
ascending id, so identity and presentation agree by construction.
- **The filter's four report plain units WITHOUT being re-tapered.** `toPlain` is read-side
only; reporting Hz / Q / drive depth means calling `filterCutoffHzFromNorm`,
`filterQFromNorm` and `filterDriveDepthFromNorm` — the filter module's own frozen laws,
which `deckValueLabel` already calls today — not restating them. **The one additive piece:
`filterNormFromDriveDepth` does not exist and must be added in `filter_params`**, beside the
two inverses that do; the analytic inverse of a frozen law is not a change to it. §6.7.5.
- **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.
@@ -1879,12 +1948,24 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
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.
cultural. **The asserted values are §6.2's table verbatim**, including the signal-flow block
sequence; a test that recomputes the ids from `cellIds` would defeat the freeze it exists
to hold.
- **`toPlain(info.defaultNormalizedValue)` compares EXACTLY equal to the default**, per
parameter, against a default-constructed `PlaySeconds` (and against `master_gain`'s unity),
so a host's reset-to-default and the editor's double-click land on the same value.
**`defaultNormalizedValue` is computed as `toNormalized(default)`, not written as a
literal** — a grep finds no normalized default constant. If the exactness fails, it is a
W1-T1 defect surfacing here, not a defect of this track. **Round-trip exactness at
arbitrary values is NOT asserted** — it is not required (§6.7.7) and asserting it would
over-constrain the taper.
- **`getParamStringByValue` prints what the editor's knob label prints**, digit for digit, at
the same stored value, across every unit category — asserted by calling **the same
formatter** from both sides in one test, not by comparing two independently produced
strings. **A grep finds exactly one formatter per unit category** and no `snprintf` of a
parameter value outside it.
- **Every exposed parameter's `units` and plain range match §6.7.1**, asserted per parameter;
`stepCount` is asserted **zero on all 44**.
- **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.
@@ -1900,23 +1981,42 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
- **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.
- **The lane-linearity consequence is stated in the header, not left to be found** — under a
tapered parameter a straight line drawn in a host automation lane is **not** linear in the
plain unit (exponential in ms, linear in octaves on cutoff, linear in dB on master gain).
This is standard and desirable, it follows directly from Ruling 3 plus the taper, and §6.7.4
gives it per category. Writing it down is the acceptance criterion; changing the taper to
avoid it is not an option.
- **The filter's four are proven untouched**: a regression baseline shows their audio
unchanged, and their persisted `*Norm` values are byte-identical across a save/reload that
passes through the parameter surface. Reporting Hz/Q/depth changed display only.
**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.
- **No [Daniel] questions. Γ-F7 is RULED — signal flow** (2026-08-01, *"signal flow order."*),
and Ruling 3 (real units) arrived specified rather than forked. **There is no unanswered
[Daniel]-class question in this track or anywhere in this plan.**
- **[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.**
- **[verify]** whether REAPER renders `ParameterInfo::units` beside the string
`getParamStringByValue` returns, or shows the string alone. **We ship the SDK's own
convention** — digits in the string, unit carried separately, which is what
`RangeParameter::toString` and the `Parameter` constructor's signature both express. If
REAPER shows no unit at all, the fallback is to append the unit **inside the one formatter**:
a one-line change in one place, touching neither the frozen id table nor the editor, because
display strings are explicitly not frozen (§6.7.1). Do not discover this after shipping.
- **[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.
predicate is never bent to fill the list. **Consequence for the frozen table:** a refusal
drops ids 1000 and 1260 (key-track) or 1450 (Trigger length) and the count falls below 44.
Those slots are then simply **never issued** — not retired, since nothing shipped under
them — and remain available to the same control if it is promoted later. No other id moves;
that is what the block-and-step scheme buys.
- **[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