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:
+148
-48
@@ -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
|
the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed
|
||||||
around.
|
around.
|
||||||
|
|
||||||
**Γ-F3 was subsequently REVERSED and a seventh fork opened, both by Daniel's later rulings
|
**Γ-F3 was subsequently REVERSED and a seventh fork opened AND CLOSED, all by Daniel's later
|
||||||
of 2026-08-01.** Γ-F3 (*"the stage-time ceiling stays 2.0 s"*) is replaced by *"extend the
|
rulings of 2026-08-01.** Γ-F3 (*"the stage-time ceiling stays 2.0 s"*) is replaced by *"extend
|
||||||
stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. And **Γ-F7 is OPEN**: the VST3
|
the stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. **Γ-F7** (the VST3 parameter
|
||||||
parameter *order* (signal flow, or the editor's visual rows), a forever commitment that
|
*order*) is **RULED: signal flow** — *"signal flow order."* **There is now NO unanswered
|
||||||
cannot be closed by proposal at review. It blocks nothing until **Γ-W4 dispatches**, which is
|
[Daniel]-class question anywhere in this plan.**
|
||||||
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.**
|
**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
|
### 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
|
## Phase-wide acceptance criteria
|
||||||
|
|
||||||
These bind every track in all three phases and are stated once here rather than repeated
|
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)
|
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
|
||||||
|
|
||||||
@@ -534,7 +541,7 @@ system's is in **`docs/product/parameter-automation.md` §§6–10**. Read §1.2
|
|||||||
table) and §7 (collisions) before dispatching any track here — every number in this phase is
|
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.
|
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:
|
folded into the tracks below:
|
||||||
- **Γ-F1** — `kEditorMinHeight` stays **680**.
|
- **Γ-F1** — `kEditorMinHeight` stays **680**.
|
||||||
- **Γ-F2** — the limiter has **lookahead with DYNAMIC reported latency** (zero when off,
|
- **Γ-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
|
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
|
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.
|
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
|
- **Γ-F7 — RULED: SIGNAL FLOW.** Daniel, 2026-08-01: *"signal flow order."* The VST3 parameter
|
||||||
parameter **order**: signal-flow order (the deck's own documented rule, layout-independent)
|
order — both the frozen id numbering and the `getParameterInfo` presentation index — is
|
||||||
or the editor's visual row order after the reflow. Same membership, different sequence,
|
**PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE → MASTER**, the
|
||||||
frozen forever the day parameters ship. **Recommendation: signal flow**, because the visual
|
deck's own `sampleDeckGroups` rule, with each group's cells in the semantic order the id
|
||||||
layout has already moved twice and this phase moves it again. Reasoning and both arguments:
|
table freezes. The editor's visual rows after the reflow were the rejected alternative.
|
||||||
`docs/product/parameter-automation.md` §6.4. **Blocks nothing until Γ-W4 dispatches**; it
|
**The reason, because a future reader will ask why the id order does not match the screen:**
|
||||||
cannot be closed by proposal at review.
|
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
|
**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
|
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 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
|
parameter order logical."* `docs/product/parameter-automation.md` was written as scoping and
|
||||||
has been **promoted in place**: §§1–5 are the original analysis, **§§6–10 are the
|
has been **promoted in place**: §§1–5 are the original analysis, **§§6–10 are the
|
||||||
specification** Γ-W4-T1 is built from. Three things it decides that the scoping pass left
|
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
|
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
|
(§6.1); the id space is an independent, hand-assigned, FOREVER-FROZEN table, **now stated in
|
||||||
the exposed list is **derived from the three-state commit predicate**, never hand-maintained
|
full as 44 numbered rows in signal-flow order** (§6.2, §6.3); the exposed list is **derived
|
||||||
(§7). Today the plugin has **zero** parameters — `ReaSamplerProcessor::initialize`
|
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
|
(`reasampler_processor.cpp:56-73`) never populates `SingleComponentEffect::parameters`, so
|
||||||
`getParameterCount()` returns the SDK default 0.
|
`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
|
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
|
second ±24 constant. **From Γ-W4-T1 it is also a frozen host normalization** — one more
|
||||||
reason, not a new rule.
|
reason, not a new rule.
|
||||||
- **The taper has exactly ONE home and three consumers.** Γ-W1-T1 extracts it into a pure
|
- **The taper has exactly ONE home and three consumers, and from Γ-W4-T1 the taper IS the
|
||||||
module; the knob's needle (`deck_values`), the AHDSR overlay's schematic axis
|
host's `toPlain`/`toNormalized`.** Γ-W1-T1 extracts it into a pure module; the knob's needle
|
||||||
(`envelope_overlay` + `envelope_edit`), and the host's `normalizedParamToPlain` (Γ-W4-T1)
|
(`deck_values`), the AHDSR overlay's schematic axis (`envelope_overlay` + `envelope_edit`),
|
||||||
all call the same function. **Three functions that agree today is a defect, not an
|
and the host's `normalizedParamToPlain` / `plainParamToNormalized` (Γ-W4-T1) all call the
|
||||||
implementation choice** — the failure it prevents is a host automation lane that means one
|
same function. **Three functions that agree today is a defect, not an implementation
|
||||||
value and a needle that draws another.
|
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
|
- **Every default value has an EXACT normalized preimage under its own taper.** Binds
|
||||||
Γ-W1-T1 (which designs the taper) and Γ-W4-T1 (which declares
|
Γ-W1-T1 (which designs the taper) and Γ-W4-T1 (which declares
|
||||||
`ParameterInfo::defaultNormalizedValue`). `resetDeckParam` bypasses the taper; **a host's
|
`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
|
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,
|
- **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
|
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
|
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
|
- **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
|
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
|
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
|
**cross-artifact contract** the extension's `instrument_drop` writes, which
|
||||||
parameters-as-truth would silently make partial.
|
parameters-as-truth would silently make partial.
|
||||||
- **`ParamID` is an independent, hand-assigned, FOREVER-FROZEN table** — blocks of 100 per
|
- **`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
|
deck group **in signal-flow order** (Γ-F7), steps of 10 within a block, a curve dial at its
|
||||||
starting at 1000. §6.2 for the layout and why hand-assignment beats derivation; **§6.3 for
|
outer knob's id + 1, blocks starting at 1000. **The full 44-id assignment is stated at
|
||||||
the freeze invariant, which is to be stated in the table's header with the same force as
|
§6.2** and is to be transcribed, not re-derived. §6.2 also for why hand-assignment beats
|
||||||
the command-id strings, the class UIDs and the payload field order.**
|
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
|
- **The exposed set is DERIVED from `isLiveDeckParam` / `liveCommitFor`, never
|
||||||
hand-maintained** — a control is a parameter iff its class is `Live` or `NoteOnLatched`.
|
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.
|
**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
|
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
|
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.
|
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` /
|
- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` /
|
||||||
`getParamValueByString` route through W1-T1's taper module and the editor's own
|
`getParamValueByString` route through W1-T1's taper module and the ONE formatter per unit
|
||||||
formatters.** Three functions that agree today is a defect; the host's normalization, the
|
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.
|
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
|
- **`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
|
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.
|
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
|
- **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
|
on the automation path may reach `reloadInstrument` or `rebuildVoiceEngine` — which §7.2's
|
||||||
omissions guarantee structurally rather than by care.
|
omissions guarantee structurally rather than by care.
|
||||||
- **`IUnitInfo`: one unit per deck group.** Order per **Γ-F7** (§6.4) — **recommendation
|
- **`IUnitInfo`: one unit per deck group**, mirroring the group inventory rather than the
|
||||||
signal flow; Daniel's call, and it must be closed before this track dispatches.**
|
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
|
- **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
|
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 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.
|
not against a literal count.
|
||||||
- **Every id in the table is asserted unique, in its group's block, and on its step** — and a
|
- **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
|
test fails if any id changes value, which is what makes the freeze mechanical rather than
|
||||||
cultural.
|
cultural. **The asserted values are §6.2's table verbatim**, including the signal-flow block
|
||||||
- **Every parameter round-trips `plain → normalized → plain` exactly at its default**, so a
|
sequence; a test that recomputes the ids from `cellIds` would defeat the freeze it exists
|
||||||
host's reset-to-default and the editor's double-click land on the same value. If this fails,
|
to hold.
|
||||||
it is a W1-T1 defect surfacing here, not a defect of this track.
|
- **`toPlain(info.defaultNormalizedValue)` compares EXACTLY equal to the default**, per
|
||||||
- **`getParamStringByValue` prints what the editor prints**, unit for unit, at the same
|
parameter, against a default-constructed `PlaySeconds` (and against `master_gain`'s unity),
|
||||||
values.
|
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
|
- **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
|
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.
|
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
|
- **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
|
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.
|
(§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.**
|
**Open questions.**
|
||||||
- **[Daniel] — Γ-F7, the parameter order.** Signal flow (recommended) or the editor's visual
|
- **No [Daniel] questions. Γ-F7 is RULED — signal flow** (2026-08-01, *"signal flow order."*),
|
||||||
rows. **This is the only unanswered [Daniel]-class question in this plan.** It blocks
|
and Ruling 3 (real units) arrived specified rather than forked. **There is no unanswered
|
||||||
nothing until this track dispatches, and it cannot be closed by proposal at review: a
|
[Daniel]-class question in this track or anywhere in this plan.**
|
||||||
forever commitment is Daniel's. §6.4.
|
|
||||||
- **[verify, FIRST]** that REAPER calls `setState` (not `setComponentState`) on a
|
- **[verify, FIRST]** that REAPER calls `setState` (not `setComponentState`) on a
|
||||||
single-component plug-in, and the ordering of `setState` against the first
|
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
|
`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
|
name-collapse; **verify it in the DAW before wiring, and do not build on the paragraph
|
||||||
alone.**
|
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
|
- **[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*
|
`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 —
|
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
|
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
|
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
|
- **[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.**
|
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
|
- **[propose at review]** whether this track spends the reserved payload rung. §6.1 says
|
||||||
|
|||||||
@@ -60,8 +60,12 @@ own width formula, not carried over from a prior measurement. The stale geometry
|
|||||||
- **VST3 automation parameters SHIP IN THIS PHASE, as its last track** (Ruling 1, Daniel
|
- **VST3 automation parameters SHIP IN THIS PHASE, as its last track** (Ruling 1, Daniel
|
||||||
2026-08-01). The specification is `docs/product/parameter-automation.md` §§6–10: 44
|
2026-08-01). The specification is `docs/product/parameter-automation.md` §§6–10: 44
|
||||||
exposed parameters derived from the three-state commit predicate, a hand-assigned
|
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
|
FOREVER-FROZEN id table in blocks of 100 with steps of 10 **in signal-flow order**
|
||||||
authoritative with parameters as a third surface onto the one model.
|
(Γ-F7, ruled), and the blob left authoritative with parameters as a third surface onto the
|
||||||
|
one model. **They report REAL UNITS to the host** (Ruling 3) through VST3's plain-value
|
||||||
|
layer — a plain range, a unit string and a stated display precision per parameter, formatted
|
||||||
|
by the same pure function the knob label uses. §6.7 there; §8.4 here for what it binds in
|
||||||
|
this doc.
|
||||||
- **The stage-time ceiling moves 2 s → 10 s, in wave 1** (Daniel, reversing Γ-F3),
|
- **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,
|
*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
|
free to move now and permanently expensive afterwards. Its real cost is not the constant
|
||||||
@@ -783,6 +787,13 @@ model"* — a snap available on one and not the others would be a divergence.
|
|||||||
| **decibels** | master gain | whole **dB** |
|
| **decibels** | master gain | whole **dB** |
|
||||||
| **already integer** | voice count, preview velocity | no change |
|
| **already integer** | voice count, preview velocity | no change |
|
||||||
|
|
||||||
|
> **This table is a DRAG rule and must never become `ParameterInfo::stepCount`** (Ruling 3,
|
||||||
|
> §8.4). `stepCount` quantizes the parameter itself — permanently, and for the host's
|
||||||
|
> automation too — which would put continuous cents out of reach from a lane and freeze the
|
||||||
|
> grid into the forever contract. The two axes are already visibly independent here: filter
|
||||||
|
> cutoff *displays* in Hz, *snaps* in whole percent of its normalized position, and is
|
||||||
|
> *automated* continuously. All 44 exposed parameters ship `stepCount = 0`.
|
||||||
|
|
||||||
### 4.3 The tapers
|
### 4.3 The tapers
|
||||||
|
|
||||||
**Both taper changes are SAFE for persistence and require no format bump.** The
|
**Both taper changes are SAFE for persistence and require no format bump.** The
|
||||||
@@ -1352,7 +1363,7 @@ shape is deliberate and its reasoning is documented at the call site.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Forks — six ruled (one later reversed), one open
|
## 8. Forks — SEVEN ruled (one later reversed), NONE open
|
||||||
|
|
||||||
### 8.1 Ruled by Daniel, 2026-08-01
|
### 8.1 Ruled by Daniel, 2026-08-01
|
||||||
|
|
||||||
@@ -1369,6 +1380,7 @@ ceiling.
|
|||||||
| **Γ-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 |
|
| **Γ-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 |
|
| **Γ-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` |
|
| **Γ-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` |
|
||||||
|
| **Γ-F7** | VST3 parameter ORDER: signal flow, or the editor's visual rows? | **Signal flow** — *"signal flow order."* The frozen id numbering and the presentation index both follow the deck's own rule; the visual layout is too mobile to freeze against. | **§8.3**; `parameter-automation.md` §6.4 (argument) and §6.2 (the 44-id table) |
|
||||||
|
|
||||||
Three of these corrected this doc rather than confirming it, and all three corrections are
|
Three of these corrected this doc rather than confirming it, and all three corrections are
|
||||||
worth remembering as pattern:
|
worth remembering as pattern:
|
||||||
@@ -1425,27 +1437,43 @@ than just counting:
|
|||||||
`restartComponent`), not about this flag.
|
`restartComponent`), not about this flag.
|
||||||
5. **The reduction is filed**, with a trigger condition, in `docs/TODO.md`.
|
5. **The reduction is filed**, with a trigger condition, in `docs/TODO.md`.
|
||||||
|
|
||||||
### 8.3 Γ-F7 — OPEN. The parameter order
|
### 8.3 Γ-F7 — RULED: signal flow. The parameter order
|
||||||
|
|
||||||
**Opened 2026-08-01 by Ruling 1** (*"Make the parameter order logical"*), because "logical"
|
**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.
|
resolved two ways and the choice freezes forever the day parameters ship. **Closed the same
|
||||||
|
day by Daniel: *"signal flow order."***
|
||||||
|
|
||||||
> **Signal-flow order** — PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY →
|
> **PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE → MASTER**, the
|
||||||
> VOICE → MASTER, the deck's own documented ordering rule, layout-independent.
|
> deck's own documented ordering rule, layout-independent. Both the frozen id numbering and
|
||||||
> **OR the editor's visual row order** after the Γ-W3 reflow — row 1 then row 2 then MASTER,
|
> the `getParameterInfo` presentation index follow it; each group's cells take the semantic
|
||||||
> matching what the user's eye scans.
|
> order the id table freezes.
|
||||||
|
|
||||||
Same membership, different sequence; the recommendation, both arguments, and why the
|
**The rejected alternative** was the editor's visual row order after the Γ-W3 reflow — row 1,
|
||||||
grouping (`IUnitInfo`, one unit per deck group) is settled either way are in
|
then row 2, then MASTER, matching what the user's eye scans. **The reason it lost, recorded
|
||||||
`docs/product/parameter-automation.md` §6.4. **Recommendation: signal flow**, because the
|
because a reader will ask why the numbering does not match the screen:** the editor's layout
|
||||||
visual layout has moved twice already and this phase moves it again, and freezing a forever
|
has already moved twice and this phase moves it again, and within-row order is settled by
|
||||||
identity to a thing that moves is the wrong coupling.
|
width fitting rather than 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.
|
||||||
|
|
||||||
**Urgency: low, but not zero.** Three waves sit in front of Γ-W4-T1 and nothing before it
|
The full argument, the accepted residual cost (a user scanning REAPER's flat list), and the
|
||||||
depends on the answer. It must close **before Γ-W4 dispatches**, and it cannot be closed by
|
resulting 44-id table are in `docs/product/parameter-automation.md` §6.4 and §6.2.
|
||||||
proposal at review — a forever commitment is a Daniel call.
|
|
||||||
|
|
||||||
*Every other fork in this phase is ruled. Nothing in §§1–7 awaits a Daniel answer.*
|
*Every fork in this phase is now ruled, and nothing in this doc awaits a Daniel answer.*
|
||||||
|
|
||||||
|
### 8.4 Ruling 3 — real units at the host boundary
|
||||||
|
|
||||||
|
**Daniel, 2026-08-01:** *"The parameter values exposed to the VST host should be in real
|
||||||
|
units, such that the host automation lanes report usable values."* Not a fork — it arrived as
|
||||||
|
a requirement and is specified rather than decided. It lands entirely in Γ-W4-T1 and moves no
|
||||||
|
wave boundary.
|
||||||
|
|
||||||
|
It reaches back into this doc in exactly one place: **§4's interaction law and §4.2's
|
||||||
|
snap-unit table are DRAG rules and must never become `ParameterInfo::stepCount`.** Snapped
|
||||||
|
drag and parameter continuity are independent axes — this doc's own table already proves it,
|
||||||
|
since filter cutoff displays in Hz, snaps in whole percent of normalized position, and is
|
||||||
|
automated continuously. Full spec, including the unit/precision table, the one-formatter
|
||||||
|
invariant and the `stepCount` sweep: `docs/product/parameter-automation.md` §6.7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ original analysis (still correct, still the reasoning), and **§§6–10 are the
|
|||||||
— the ID scheme and its freeze invariant, the per-control classification, the one-way-door
|
— the ID scheme and its freeze invariant, the per-control classification, the one-way-door
|
||||||
sweep, the resample interaction, and the build shape.
|
sweep, the resample interaction, and the build shape.
|
||||||
|
|
||||||
Two things that were open in the scoping pass are now **decided** rather than deferred:
|
**Nothing in this doc is open to Daniel any longer.** The storage architecture (§3.3, decided
|
||||||
the storage architecture (§3.3, decided in §6.1) and the ID space (§3.1, decided in §6.2).
|
in §6.1), the ID space (§3.1, decided in §6.2) and **Γ-F7, the parameter order — RULED
|
||||||
One thing is **open and wants Daniel** before Γ-W4 dispatches: **Γ-F7**, the parameter
|
signal flow, 2026-08-01** (§6.4) are all settled. **Ruling 3 (real units at the host
|
||||||
*order* (§6.4). It is not urgent — three waves sit in front of it — but it is a forever
|
boundary) is specified at §6.7**, which also carries the one-formatter invariant, the
|
||||||
commitment, so it is not mine to close silently.
|
`stepCount` sweep, and the resolution of the apparent conflict between "report real units"
|
||||||
|
and "do not re-taper the filter."
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -92,6 +93,10 @@ Today, re-tapering is **free** — normalization exists only in `ui/deck_values.
|
|||||||
display/interaction layer, and the payload stores raw engine values as doubles. That is a
|
display/interaction layer, and the payload stores raw engine values as doubles. That is a
|
||||||
property worth spending while we still have it. See §4.
|
property worth spending while we still have it. See §4.
|
||||||
|
|
||||||
|
**Ruling 3 makes this literal rather than analogous:** `toPlain`/`toNormalized` are not merely
|
||||||
|
*like* our taper, they **are** it — one extracted pure module read by the knob, the overlay
|
||||||
|
and the host alike. §6.7.3.
|
||||||
|
|
||||||
### 3.3 Two sources of truth
|
### 3.3 Two sources of truth
|
||||||
|
|
||||||
VST3 saves parameter values *and* calls `setState`/`setComponentState`. A value that lives
|
VST3 saves parameter values *and* calls `setState`/`setComponentState`. A value that lives
|
||||||
@@ -262,7 +267,9 @@ other constant with that property.
|
|||||||
|
|
||||||
## 5. Open questions — status after the promotion
|
## 5. Open questions — status after the promotion
|
||||||
|
|
||||||
The scoping pass collected eight. Seven are closed below; one is open and is a Daniel call.
|
The scoping pass collected eight, and Γ-F7 was opened beside them. **All nine are closed**;
|
||||||
|
one (migration) survives as a verification rather than a question. Ruling 3 added a tenth
|
||||||
|
line, closed on arrival.
|
||||||
|
|
||||||
| # | Question | Status |
|
| # | Question | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -274,7 +281,8 @@ The scoping pass collected eight. Seven are closed below; one is open and is a D
|
|||||||
| 6 | Sample-accurate vs. block-boundary (§3.7) | **CLOSED — block boundary, stated explicitly.** §6.5 |
|
| 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 |
|
| 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 |
|
| 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. |
|
| **Γ-F7** | **Parameter ORDER — signal flow, or the editor's visual rows?** | **CLOSED — SIGNAL FLOW** (Daniel, 2026-08-01: *"signal flow order."*). The block table needed no renumbering; visual order would have. §6.4 |
|
||||||
|
| **Ruling 3** | **Real units at the host boundary** | **CLOSED on arrival** (Daniel, 2026-08-01). Satisfied through the plain-value layer, not the wire format. Unit/precision table, the one-formatter invariant, the `stepCount` sweep, and the filter resolution: §6.7 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -373,6 +381,62 @@ accident, because renumbering it means editing the numbers.
|
|||||||
blocks exist so that if either ever gains an automatable control it lands in its own
|
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.
|
group's range rather than in whatever range happened to be free.
|
||||||
|
|
||||||
|
**The block sequence above IS signal-flow order, and that is now the ruled order (§6.4).**
|
||||||
|
The table was authored in the deck's own `sampleDeckGroups` sequence, which today happens to
|
||||||
|
be both the signal flow and the pre-reflow visual order; Γ-W3-T1's reflow is what separates
|
||||||
|
them. Because Daniel ruled signal flow, **no block moves** — had the ruling gone to visual
|
||||||
|
rows, every block from 1100 up would have been renumbered before it was ever frozen.
|
||||||
|
|
||||||
|
**Within a block, the order is the group's own semantic order, seeded ONCE at the freeze and
|
||||||
|
never re-seeded.** Γ-F7's reasoning applies recursively: a group's `cellIds` order is exactly
|
||||||
|
as mobile as the row order is, and a future width fit can reorder cells inside a group as
|
||||||
|
easily as the reflow reorders groups. So the within-block sequence is a property of *this
|
||||||
|
table*, not a tracked function of `cellIds`. It is seeded from the cell order as it stands at
|
||||||
|
Γ-W3, which is semantic in every group today — envelope stages in temporal order, filter
|
||||||
|
cells in solve order — and that is what makes the seeding safe rather than arbitrary.
|
||||||
|
|
||||||
|
#### The table, stated
|
||||||
|
|
||||||
|
44 parameters. Curve dials take their outer knob's id + 1 (§6.2 above); a `+1` row is always
|
||||||
|
the inner dial of the row above it.
|
||||||
|
|
||||||
|
| ID | Parameter | `units` | ID | Parameter | `units` |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **1000** | Key-track (pitch) | `%` | **1300** | Filter env attack | `ms` |
|
||||||
|
| **1010** | Rate | `%` | 1301 | └ attack curve | — |
|
||||||
|
| **1020** | Pitch offset | `st` | **1310** | Filter env hold | `ms` |
|
||||||
|
| | | | **1320** | Filter env decay | `ms` |
|
||||||
|
| **1100** | Pitch env attack | `ms` | 1321 | └ decay curve | — |
|
||||||
|
| 1101 | └ attack curve | — | **1330** | Filter env sustain | `%` |
|
||||||
|
| **1110** | Pitch env hold | `%` | **1340** | Filter env release | `ms` |
|
||||||
|
| **1120** | Pitch env decay | `ms` | 1341 | └ release curve | — |
|
||||||
|
| 1121 | └ decay curve | — | **1350** | Filter trig attack | `ms` |
|
||||||
|
| **1130** | Pitch env depth | `st` | 1351 | └ attack curve | — |
|
||||||
|
| | | | **1360** | Filter trig hold | `%` |
|
||||||
|
| **1200** | Filter morph | `%` | **1370** | Filter trig decay | `ms` |
|
||||||
|
| **1210** | Filter cutoff | `Hz` | 1371 | └ decay curve | — |
|
||||||
|
| **1220** | Filter Q | — | | | |
|
||||||
|
| **1230** | Filter drive | — | **1400** | Amp attack | `ms` |
|
||||||
|
| **1240** | Filter mod amount | `%` | 1401 | └ attack curve | — |
|
||||||
|
| **1250** | Filter velocity amount | `%` | **1410** | Amp hold | `ms` |
|
||||||
|
| **1260** | Key-track (filter) | `%` | **1420** | Amp decay | `ms` |
|
||||||
|
| | | | 1421 | └ decay curve | — |
|
||||||
|
| **1700** | Master gain | `dB` | **1430** | Amp sustain | `%` |
|
||||||
|
| | | | **1440** | Amp release | `ms` |
|
||||||
|
| *1500–1599* | *VELOCITY — reserved, empty* | | 1441 | └ release curve | — |
|
||||||
|
| *1600–1699* | *VOICE — reserved, empty* | | **1450** | Trigger length | `%` |
|
||||||
|
| | | | **1460** | Amp trig attack | `ms` |
|
||||||
|
| | | | 1461 | └ attack curve | — |
|
||||||
|
| | | | **1470** | Amp trig hold | `%` |
|
||||||
|
| | | | **1480** | Amp trig decay | `ms` |
|
||||||
|
| | | | 1481 | └ decay curve | — |
|
||||||
|
|
||||||
|
**Presentation index order is ascending id** — 1000, 1010, 1020, 1100, 1101, 1110, … 1700.
|
||||||
|
Identity order and presentation order therefore agree by construction, which is §6.4's
|
||||||
|
requirement rather than a coincidence to be maintained. **The count is asserted against the
|
||||||
|
predicate, never against this table** (§7); the table is what the predicate's members are
|
||||||
|
*numbered* as, not what decides membership.
|
||||||
|
|
||||||
### 6.3 The freeze invariant
|
### 6.3 The freeze invariant
|
||||||
|
|
||||||
State this in the ID table's header, with the same force as its three siblings.
|
State this in the ID table's header, with the same force as its three siblings.
|
||||||
@@ -404,7 +468,7 @@ State this in the ID table's header, with the same force as its three siblings.
|
|||||||
table that stays true — the reflow, a future regrouping, a new deck can all reorder the enum
|
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.
|
freely. Deriving ids from it would have quietly repealed that sentence.
|
||||||
|
|
||||||
### 6.4 Order and grouping — Γ-F7, OPEN
|
### 6.4 Order and grouping — Γ-F7, CLOSED: signal flow
|
||||||
|
|
||||||
Two orders exist and they are not the same thing:
|
Two orders exist and they are not the same thing:
|
||||||
|
|
||||||
@@ -415,32 +479,46 @@ Two orders exist and they are not the same thing:
|
|||||||
They should agree (a list whose numbering runs backwards against its display is a trap), so
|
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.
|
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 →
|
> **RULED (Daniel, 2026-08-01): signal-flow order.** *"signal flow order."*
|
||||||
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,
|
**The order is PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE →
|
||||||
VELOCITY, VOICE; row 2 PITCH ENV, FILTER ENV, AMP; MASTER spanning. Same membership,
|
MASTER**, which is the deck's own documented ordering rule (`sampleDeckGroups`, *"left to
|
||||||
different sequence.
|
right, in SIGNAL-FLOW order: pitch → filter → amp, then the two instance-wide groups"*), with
|
||||||
|
each group's cells in the semantic order §6.2's table freezes. Identity order and
|
||||||
|
presentation order are the same sequence; the block table in §6.2 stands unchanged, because
|
||||||
|
it was already authored in this order.
|
||||||
|
|
||||||
**Why the recommendation goes to signal flow:** the visual layout is a canvas-constrained
|
**The rejected alternative was the editor's visual rows** after Γ-W3-T1 — row 1 PITCH/RATE,
|
||||||
artifact that has already moved twice (Θ-W6-T1 grew the floor 840 → 980, Γ-W3-T1 takes it to
|
FILTER, VELOCITY, VOICE; row 2 PITCH ENV, FILTER ENV, AMP; MASTER spanning. Same membership,
|
||||||
1190 and re-rows every group), and within-row order is decided by *width fitting*, not by
|
different sequence. Do not re-propose it: the id table is frozen from Γ-W4-T1, so re-ordering
|
||||||
meaning. Freezing the host-facing order to a layout that demonstrably moves is the wrong
|
is no longer a preference question at all.
|
||||||
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
|
**Why signal flow — the argument a future reader will come looking for.** The obvious
|
||||||
parameter in REAPER's flat list finds it fastest if the list reads in the order their eyes
|
objection to this table is that its numbering does not match what is on screen, and someone
|
||||||
scan the plugin window. If Daniel weights find-it-on-screen over layout-independence, visual
|
will eventually propose "fixing" that. The answer is that the two *cannot* both be satisfied
|
||||||
order is the right answer and it is not a mistake; it just also means the parameter order
|
forever, and only one of the two axes holds still:
|
||||||
stops being free the next time the deck is re-laid.
|
|
||||||
|
|
||||||
**Grouping is settled either way: `IUnitInfo`, one unit per deck group**, mirroring the
|
> **The editor's visual layout has already moved twice** — Θ-W6-T1 grew the window floor
|
||||||
group inventory (not the rows). Only the sequence is at issue.
|
> 840 → 980, and Γ-W3-T1 takes it to 1190 and re-rows every group into two categorical rows
|
||||||
|
> with a double-height MASTER. Within-row order is decided by *width fitting*, not by meaning.
|
||||||
|
> **Binding a permanently-frozen id order to a demonstrably mobile layout guarantees the two
|
||||||
|
> drift apart** — and after the first drift the order is neither logical *nor* matching, which
|
||||||
|
> is strictly worse than either. **Signal flow is the axis that does not move.** It is also
|
||||||
|
> the deck's own stated rule, so the order is not a new invention that has to be remembered;
|
||||||
|
> it is the one the codebase already documents.
|
||||||
|
|
||||||
|
The counter-argument, recorded because it was 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.
|
||||||
|
That is answered — partly — by `IUnitInfo` grouping, which lets a host present the parameters
|
||||||
|
under their group names rather than as one flat run. It is not fully answered, and that
|
||||||
|
residual cost is accepted knowingly.
|
||||||
|
|
||||||
|
§3.6 asked whether the unit tree mirrors the visual grouping or the signal flow and noted
|
||||||
|
"they currently agree"; the reflow is what makes them stop agreeing, and this ruling is the
|
||||||
|
answer.
|
||||||
|
|
||||||
|
**Grouping: `IUnitInfo`, one unit per deck group**, mirroring the group inventory (not the
|
||||||
|
rows) — unchanged by the ruling, which decided only the sequence.
|
||||||
|
|
||||||
### 6.5 What the plugin must implement, as behaviour
|
### 6.5 What the plugin must implement, as behaviour
|
||||||
|
|
||||||
@@ -448,21 +526,21 @@ Not architecture — the engineer's call is *how*. These are the observable cont
|
|||||||
|
|
||||||
- **`getParameterCount` / `getParameterInfo`** report exactly the §7 list, in §6.4's order,
|
- **`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
|
each with: the frozen `id`; a `title` that survives truncation and a distinct
|
||||||
`shortTitle`; `units` (`"ms"`, `"st"`, `"%"`, `"dB"`, `""` for exponents) matching what the
|
`shortTitle`; `units` and a plain range per **§6.7**, which is where Ruling 3's real-unit
|
||||||
editor prints, so the host's readout and the knob's label never disagree; `stepCount = 0`
|
contract lives; `stepCount = 0` on all 44 (§6.7's sweep); `defaultNormalizedValue`
|
||||||
(every exposed parameter is continuous — §7.2 is why); `defaultNormalizedValue` per §8's
|
**computed** per §6.7's tightening of §8's exactness rule; the group's `unitId`; and
|
||||||
exactness rule; the group's `unitId`; and `kCanAutomate`.
|
`kCanAutomate`.
|
||||||
- **`kIsBypass` is not set on anything.** The limiter is a safety device, not a bypass, and
|
- **`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
|
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.
|
(§3.8). The plugin exposes **no** bypass parameter — it is an instrument.
|
||||||
- **`setParamNormalized` / `getParamNormalized`** are the model projection of §6.1, not a
|
- **`setParamNormalized` / `getParamNormalized`** are the model projection of §6.1, not a
|
||||||
cached shadow value.
|
cached shadow value.
|
||||||
- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` /
|
- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` /
|
||||||
`getParamValueByString` all route through the ONE taper module Γ-W1-T1 extracts.** This is
|
`getParamValueByString` all route through the ONE taper module Γ-W1-T1 extracts and the ONE
|
||||||
the structural point of that extraction: the host's normalization, the knob's needle
|
formatter per unit category.** This is the structural point of that extraction: the host's
|
||||||
angle, and the overlay node's position must be the *same function*, not three functions
|
normalization, the knob's needle angle, and the overlay node's position must be the *same
|
||||||
that agree today. `getParamStringByValue` reuses the editor's own formatters
|
function*, not three functions that agree today. **§6.7 states both invariants in full** —
|
||||||
(`formatEnvTimeMs` and its peers) for the same reason.
|
it is the specification for this bullet, not a gloss on it.
|
||||||
- **`IParameterChanges` is observed at BLOCK boundaries** — the last point in the block wins
|
- **`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
|
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.
|
change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids.
|
||||||
@@ -499,6 +577,245 @@ restores the model and the parameters are derived from it. Three things are none
|
|||||||
automation against the same plain values — the round trip through the frozen taper is
|
automation against the same plain values — the round trip through the frozen taper is
|
||||||
exact at both ends.
|
exact at both ends.
|
||||||
|
|
||||||
|
### 6.7 Real units at the host boundary — Ruling 3
|
||||||
|
|
||||||
|
**Ruling numbering.** `docs/PLAN.md` numbers Daniel's Phase-Γ rulings in the order they
|
||||||
|
arrived: **Ruling 1** = VST3 parameters ship in Γ; **Ruling 2** = the 10 s ceiling and the
|
||||||
|
knob-interaction law it grew; **Ruling 3** = this one. The same numbers are used here.
|
||||||
|
|
||||||
|
> **Ruling 3 (Daniel, 2026-08-01):** *"The parameter values exposed to the VST host should be
|
||||||
|
> in real units, such that the host automation lanes report usable values."*
|
||||||
|
|
||||||
|
**This is satisfiable, and the mechanism matters, because the naive reading of it is
|
||||||
|
impossible.** VST3's automation *wire format* is normalized `0..1` and cannot be anything
|
||||||
|
else: `setParamNormalized` / `getParamNormalized` (`ivsteditcontroller.h:429-433`) are the
|
||||||
|
interface, and what the host writes into a lane and replays from it is a normalized double.
|
||||||
|
There is no VST3 in which a lane stores milliseconds.
|
||||||
|
|
||||||
|
What delivers the requirement — **usable values in the host's UI and lane readouts** — is the
|
||||||
|
**plain-value layer**, which the vendored SDK provides and which we are simply not using yet:
|
||||||
|
|
||||||
|
| Surface | Where | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `Parameter::toPlain` / `toNormalized` | `public.sdk/source/vst/vstparameters.h:88-90` | **virtual** — overriding them is how a non-linear law is expressed to the host |
|
||||||
|
| `RangeParameter` | `vstparameters.h:110-138` | carries plain min/max and does the *linear* mapping for free |
|
||||||
|
| `getParamStringByValue` | `ivsteditcontroller.h:418` | the display string the host shows in its readout |
|
||||||
|
| `ParameterInfo::units` | `ivsteditcontroller.h:52` | the unit string (`"ms"`, `"st"`, `"dB"`, `"%"`, `"Hz"`) |
|
||||||
|
| `normalizedParamToPlain` / `plainParamToNormalized` | `ivsteditcontroller.h:424-426` | the controller-level pair a host calls to read a lane in plain units |
|
||||||
|
|
||||||
|
**The direct precedent is in the vendored tree.** `public.sdk/samples/vst/common/logscale.h`
|
||||||
|
overrides exactly this pair for a logarithmic law — `toPlain` returns `logScale.scale(norm)`,
|
||||||
|
`toNormalized` returns `logScale.invscale(plain)` (`logscale.h:221-229`), with `toString`
|
||||||
|
printing the *plain* value. That is precisely the shape our log-tapered ms knobs and the
|
||||||
|
log2 semitone knobs need. We are not inventing a mechanism; we are using the one the SDK
|
||||||
|
ships and Steinberg's own sample demonstrates.
|
||||||
|
|
||||||
|
#### 6.7.1 Every exposed parameter declares a plain range, a unit, and a display precision
|
||||||
|
|
||||||
|
Eight unit categories, covering all 44. **Precision and formatting are a product decision
|
||||||
|
about what a user reads in a lane, and they are decided here.**
|
||||||
|
|
||||||
|
| Category | Parameters (count) | `units` | Plain range | Display | Taper (norm → plain) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **Time** | the 14 stage times | `ms` | 0 … 10000 | one decimal below 10 ms, integer at and above (`0.5`, `3`, `104`, `10000`) | log (Γ-W1-T1) |
|
||||||
|
| **Semitones** | Pitch offset, Pitch env depth (2) | `st` | −24 … +24 | one decimal, **always signed** (`+3.5`, `-12.0`, `+0.0`) | log2, centre-expanded (Γ-W1-T1) |
|
||||||
|
| **Percent, unipolar** | amp/filter sustain, three hold fractions, Trigger length, morph (7) | `%` | 0 … 100 | integer | identity ×100 |
|
||||||
|
| **Percent, 0–200** | key-track ×2 (1000, 1260) (2) | `%` | 0 … 200 | integer | identity ×200 |
|
||||||
|
| **Percent, bipolar** | filter mod amount, filter velocity amount (2) | `%` | −100 … +100 | integer, **always signed** | identity ×100 |
|
||||||
|
| **Percent, Rate** | Rate (1) | `%` | 50 … 200 | **one decimal** | exponential in ratio (linear in semitones, Γ-W2-T1) |
|
||||||
|
| **Decibels** | Master gain (1) | `dB` | −60 … +24 | one decimal, always signed; **norm 0 reads `-inf`** | linear in dB (`engine/master_gain`) |
|
||||||
|
| **Hertz** | Filter cutoff (1) | `Hz` | 20 … 20000 | integer | log Hz — **frozen v9 law, read-side only** (§6.7.5) |
|
||||||
|
| **Dimensionless** | 12 curve exponents, filter Q, filter drive (14) | *(empty)* | exponents 0.1 … 10; Q 0.1 … 10; drive 0 … 4 | two decimals | exponents: `curve_law` log travel. Q/drive: **frozen v9 laws, read-side only** |
|
||||||
|
|
||||||
|
14 + 2 + 7 + 2 + 2 + 1 + 1 + 1 + 14 = **44.**
|
||||||
|
|
||||||
|
**Four formatting decisions inside that table, each with its reason:**
|
||||||
|
|
||||||
|
1. **Time never switches to seconds**, so the ceiling reads `10000 ms` and not `10 s`. This
|
||||||
|
is the existing rule (`deck_values.h`: *"the editor reads in one unit so two stage times
|
||||||
|
are comparable at a glance"*) surviving the 10 s ceiling intact, and it is *forced* on the
|
||||||
|
host side anyway — `ParameterInfo::units` is a static string per parameter and cannot
|
||||||
|
change with magnitude.
|
||||||
|
2. **Rate reads to one decimal, not integer percent.** Its snap grid is whole semitones
|
||||||
|
(§4.2 of the control-surface spec), and semitone steps do not land on integer percent
|
||||||
|
(+1 st = 105.946 %). An integer display would print a snapped position as a value the
|
||||||
|
snap cannot actually produce.
|
||||||
|
3. **Cutoff's `k` abbreviation is RETIRED** — cutoff reads integer Hz on both surfaces
|
||||||
|
(`240`, `12800`), not `12.8k`. `units` is one static string per parameter, so a
|
||||||
|
magnitude-switching unit is not expressible; and the label band demonstrably fits eight
|
||||||
|
characters, since the 10 s ceiling already forces `10000 ms`. **The rejected alternative
|
||||||
|
was to keep `12.8k` in the editor and accept a host/editor divergence on cutoff alone** —
|
||||||
|
which is exactly the defect class §6.7.2 exists to forbid, for the sake of one idiom.
|
||||||
|
4. **The `^` on a curve exponent is static cell chrome, not part of the value.** The editor's
|
||||||
|
label keeps its caret; the value string is `1.00`. A constant prefix cannot diverge, and
|
||||||
|
`^` is not a unit.
|
||||||
|
|
||||||
|
**What is frozen here and what is not.** The **plain range** and the **norm↔plain law** are
|
||||||
|
frozen with the id (§6.3) — they are the meaning of every recorded automation point. The
|
||||||
|
`units` string, the `title`/`shortTitle`, and the display precision are **not frozen**: they
|
||||||
|
are what a user reads, not what a lane stores, and may be improved at any time.
|
||||||
|
|
||||||
|
#### 6.7.2 ONE formatter, two consumers — an invariant, not a review check
|
||||||
|
|
||||||
|
> **A unit category has exactly ONE pure formatter, and the editor and the host are both its
|
||||||
|
> callers.** The formatter returns the **digits** of a plain value in that category's one
|
||||||
|
> `units` string — no embedded unit, no magnitude-switched unit, no width-conditional
|
||||||
|
> abbreviation, and no caller-side branch that changes what the digits are. The editor's knob
|
||||||
|
> label renders `digits` (plus its own static chrome — the unit suffix, the `^` prefix); the
|
||||||
|
> host receives `digits` from `getParamStringByValue` and `units` from
|
||||||
|
> `ParameterInfo::units`. **There is no second implementation on either side.**
|
||||||
|
>
|
||||||
|
> **The editor and the host disagreeing about what a value reads as is a defect class, and it
|
||||||
|
> is forbidden structurally rather than caught at review** — the same discipline, and the same
|
||||||
|
> reason, as *"three functions that agree today is a defect"* for the taper.
|
||||||
|
|
||||||
|
This has a consequence for the two formatters that exist today: **they stop embedding their
|
||||||
|
unit.** `formatEnvTimeMs` (`core/instrument/ui/deck_values.h`) returns `"104 ms"` today and
|
||||||
|
must return `"104"`; `formatMasterGainLabel` (`core/instrument/engine/master_gain.h`) returns
|
||||||
|
`"-12.0dB"` and must return `"-12.0"` (and `"-inf"` at norm 0, which stays inside the
|
||||||
|
formatter so both surfaces show it). `deckValueLabel`
|
||||||
|
(`shell/instrument/editor_controls.cpp`) stops carrying `snprintf` format strings of its own
|
||||||
|
and becomes a dispatch to the per-category formatters — it is today the *second*
|
||||||
|
implementation for every category except time and dB, which is precisely the drift this
|
||||||
|
invariant closes.
|
||||||
|
|
||||||
|
**[verify] at the track:** whether REAPER renders `ParameterInfo::units` beside the string
|
||||||
|
from `getParamStringByValue`, or shows the string alone. The SDK's own convention is
|
||||||
|
digits-only in the string with `units` carried separately (`RangeParameter::toString` prints
|
||||||
|
the number; the `Parameter` constructor takes `units` as its own argument), so that is what
|
||||||
|
we ship. **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 table nor the
|
||||||
|
editor, because display strings are not frozen (§6.7.1).
|
||||||
|
|
||||||
|
#### 6.7.3 `toNormalized` IS the taper
|
||||||
|
|
||||||
|
**Γ-W1-T1's extracted taper module is not merely *consulted* by the parameter layer — on the
|
||||||
|
norm↔plain axis it *is* the parameter layer.** `toPlain` is the taper's forward map;
|
||||||
|
`toNormalized` is its inverse. **Three consumers, one law:**
|
||||||
|
|
||||||
|
1. the editor's knob drag (`ui/deck_values`),
|
||||||
|
2. the AHDSR overlay's schematic axis and its drag inverse (`ui/envelope_overlay` +
|
||||||
|
`ui/envelope_edit`),
|
||||||
|
3. the host's `normalizedParamToPlain` / `plainParamToNormalized` (this track).
|
||||||
|
|
||||||
|
**This is what makes the one-way-door reasoning structurally true rather than a warning.**
|
||||||
|
The plan says re-tapering after Γ-W4-T1 silently re-interprets every recorded automation
|
||||||
|
point; the reason that cannot be forgotten is that there is no second place a taper could be
|
||||||
|
changed. The host reads the same function the needle does.
|
||||||
|
|
||||||
|
#### 6.7.4 The consequence to know: a straight line in a lane is not linear in real units
|
||||||
|
|
||||||
|
Under a tapered parameter, **a linear ramp drawn in a host automation lane sweeps
|
||||||
|
non-linearly in the plain unit.** This is standard, generally desirable, and follows directly
|
||||||
|
from Ruling 3 combined with the taper ruling — so it is written down rather than discovered:
|
||||||
|
|
||||||
|
- **Time (log):** a straight lane ramp sweeps **exponentially in ms** — most of its travel is
|
||||||
|
spent in the first few hundred milliseconds, which is where a stage time is musically
|
||||||
|
decided.
|
||||||
|
- **Cutoff (log Hz):** a straight ramp is **linear in octaves** — the musically right filter
|
||||||
|
sweep, and the reason every synth taper looks like this.
|
||||||
|
- **Master gain (linear in dB):** a straight ramp is a **linear dB fade**, i.e. exponential in
|
||||||
|
amplitude — the musically right fade.
|
||||||
|
- **Rate (linear in semitones):** a straight ramp is a **linear pitch glide** and an
|
||||||
|
exponential sweep in ratio/percent.
|
||||||
|
- **Pitch offset, pitch env depth (log2, centre-expanded):** a straight ramp moves **slowly
|
||||||
|
near zero and fast at the extremes**, in semitones. This is the one category where the
|
||||||
|
taper is chosen for knob resolution rather than for musical linearity, and it is the one a
|
||||||
|
user is most likely to notice as unexpected.
|
||||||
|
- **Percent, dimensionless, curve exponents:** identity or near-identity — a straight ramp is
|
||||||
|
straight.
|
||||||
|
|
||||||
|
**The remedy, if a user wants a literal-units ramp, is the host's own curve tools**, not a
|
||||||
|
change to the taper. There is no version of this in which lanes are stored in plain units.
|
||||||
|
|
||||||
|
#### 6.7.5 Plain units for the filter's four is NOT re-tapering
|
||||||
|
|
||||||
|
The plan forbids re-mapping cutoff / Q / morph / drive: they persist as `*Norm` doubles in
|
||||||
|
payload v9, so their laws are already wire-frozen and re-tapering them would re-tune every
|
||||||
|
saved project independently of automation (§8, row 4). Ruling 3 requires them to report real
|
||||||
|
units. **An engineer will read those two as contradictory. They are not, and here is why:**
|
||||||
|
|
||||||
|
> **`toPlain` is a pure READ-side mapping.** It does not touch the stored value.
|
||||||
|
> `getParamNormalized` returns the stored `cutoffNorm` unchanged; `setParamNormalized` writes
|
||||||
|
> it unchanged; the payload writes the same double it always has. The *only* thing `toPlain`
|
||||||
|
> changes is what the host **displays**. The frozen law stays frozen precisely because
|
||||||
|
> `toPlain` is the law — reporting Hz means *calling* it, not replacing it.
|
||||||
|
|
||||||
|
The codebase already does exactly this and has since the filter shipped: `deckValueLabel`
|
||||||
|
prints cutoff via `filterCutoffHzFromNorm`, Q via `filterQFromNorm`, and drive via
|
||||||
|
`filterDriveDepthFromNorm` — the filter module's own laws
|
||||||
|
(`core/instrument/engine/filter/filter_params.h`). **The parameter layer calls those; it does
|
||||||
|
not restate them.** For morph, plain % is `norm × 100`.
|
||||||
|
|
||||||
|
**One gap, and it is additive.** `plainParamToNormalized` and `getParamValueByString` need
|
||||||
|
the inverse of each law. `filterNormFromCutoffHz` and `filterNormFromQ` already exist and are
|
||||||
|
documented as exact inverses; **`filterDriveDepthFromNorm` has no published inverse and one
|
||||||
|
must be added.** Adding the analytic inverse of a frozen law is not a change to the law — it
|
||||||
|
has exactly the standing the two existing inverses already have, and it belongs in
|
||||||
|
`filter_params`, beside them, not in the parameter module.
|
||||||
|
|
||||||
|
**Stated once so the two rules never read as a collision again:** the prohibition forbids
|
||||||
|
*editing* `filterCutoffHzFromNorm` and its peers; the requirement is satisfied by *calling*
|
||||||
|
them.
|
||||||
|
|
||||||
|
#### 6.7.6 Shift-snap must NOT become `stepCount` — and the sweep
|
||||||
|
|
||||||
|
**These are two different things and conflating them would freeze the wrong one forever:**
|
||||||
|
|
||||||
|
- **Shift-snap is a DRAG interaction** — a modifier held during a mouse gesture in the editor
|
||||||
|
(control-surface spec §4.1–4.2). It affects which values a *drag* produces and nothing else.
|
||||||
|
- **`ParameterInfo::stepCount` quantizes the PARAMETER** — permanently, for every writer
|
||||||
|
including the host's automation, and it is part of the frozen normalization (§6.3 names
|
||||||
|
`stepCount` explicitly).
|
||||||
|
|
||||||
|
Exposing the snap grid as `stepCount` would make continuous cents unreachable from a host
|
||||||
|
lane, quantize every automation curve to the snap grid, and freeze that quantization into a
|
||||||
|
contract that cannot be revised. **The two axes are independent, and the codebase already
|
||||||
|
proves it:** filter cutoff *displays* in Hz, *snaps* in whole percent of its normalized
|
||||||
|
position, and would be *automated* continuously — three different grids on one control, none
|
||||||
|
of which is the others.
|
||||||
|
|
||||||
|
> **Every continuous-valued parameter ships `stepCount = 0`.**
|
||||||
|
|
||||||
|
**The sweep, run over all 44: none carries a non-zero `stepCount`.** Every one of the 44 is
|
||||||
|
continuous — the 14 times, the 2 semitone throws, the 12 percent controls, the 12 curve
|
||||||
|
exponents, cutoff, Q, drive, master gain. **And that is structural rather than lucky:**
|
||||||
|
`stepCount > 0` is only meaningful for a discrete control, and §7's predicate excludes every
|
||||||
|
discrete control by construction — discrete controls are reload or rebuild tier (§7.2), which
|
||||||
|
is why the classification and the continuity coincide.
|
||||||
|
|
||||||
|
**The one case that would change this**, named so it is not a surprise: if a discrete control
|
||||||
|
ever gains a live path and therefore qualifies (voice count is the obvious candidate, 1–32),
|
||||||
|
`stepCount` becomes a live decision *at that moment* and freezes with the new id. §7.2
|
||||||
|
already names that door from the other side.
|
||||||
|
|
||||||
|
#### 6.7.7 The exact-preimage requirement, re-checked against this ruling
|
||||||
|
|
||||||
|
**Unchanged in force. Tightened in two ways, with one explicit NON-requirement added.**
|
||||||
|
|
||||||
|
1. **Tightening — `defaultNormalizedValue` is COMPUTED, never a literal.** It is
|
||||||
|
`toNormalized(default)`, evaluated against the one taper at table construction, where
|
||||||
|
`default` is read off a default-constructed `PlaySeconds` (the standing rule that there is
|
||||||
|
no second table of defaults). A hand-written normalized literal is a second source of truth
|
||||||
|
for the default and drifts from the taper silently — precisely the failure the whole
|
||||||
|
one-formatter/one-taper discipline exists to prevent.
|
||||||
|
2. **Tightening — the assertion moves onto the pair the host actually calls.** Γ-W1-T1 still
|
||||||
|
owns the taper *shape* that makes exactness achievable, and still fails there if it is not.
|
||||||
|
Γ-W4-T1 additionally asserts `toPlain(info.defaultNormalizedValue)` compares **exactly**
|
||||||
|
equal to the default, per parameter, on the host-facing functions. The prior wording made
|
||||||
|
this an inference from the taper's own tests; it is now a direct test of the contract.
|
||||||
|
3. **NON-requirement, stated so nobody over-constrains the taper: `toNormalized(toPlain(n))
|
||||||
|
== n` is NOT required for arbitrary `n`.** No log map satisfies that exactly in double,
|
||||||
|
and demanding it would rule out the taper the phase has already chosen. Exactness is
|
||||||
|
required **at the defaults**; monotonicity is required **everywhere**; round-trip exactness
|
||||||
|
at arbitrary points is required **nowhere**.
|
||||||
|
4. **The filter's four are exact by identity and need no work.** Their stored value *is* the
|
||||||
|
normalized one, so `defaultNormalizedValue` is the stored default verbatim and `toPlain`
|
||||||
|
never participates in the reset path.
|
||||||
|
5. **Master gain's unity remains the sharpest case** (≈ 0.714 norm), for the reason §8 row 7
|
||||||
|
gives: a host reset that lands a hair off unity is an audible gain error, not a cosmetic
|
||||||
|
one.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. The classification — which controls become parameters
|
## 7. The classification — which controls become parameters
|
||||||
@@ -604,8 +921,8 @@ are new; three are already frozen for an unrelated reason and are recorded so th
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **The ms and semitone tapers** (`ui/deck_values`) | Γ-W1-T1. Known. The taper *is* the host normalization. |
|
| 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. |
|
| 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.** |
|
| 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.** **Tightened by Ruling 3 — see §6.7.7**: `defaultNormalizedValue` is computed rather than written, the assertion moves onto `toPlain`, and arbitrary-point round-trip exactness is explicitly NOT required. |
|
||||||
| 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. |
|
| 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. **Ruling 3 does NOT conflict with this** — reporting Hz/Q/drive-depth to the host means *calling* the frozen laws, not replacing them; §6.7.5 works it out and names the one additive gap (drive has no published inverse). |
|
||||||
|
|
||||||
**Frozen by this work — record only, no change wanted:**
|
**Frozen by this work — record only, no change wanted:**
|
||||||
|
|
||||||
@@ -623,7 +940,10 @@ are new; three are already frozen for an unrelated reason and are recorded so th
|
|||||||
- **The params-payload ladder.** Under §6.1 the blob stays authoritative, so payload growth
|
- **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
|
continues on its own independent axis exactly as before. Automation adds no constraint to
|
||||||
it.
|
it.
|
||||||
- **`stepCount`.** No discrete parameter is exposed (§7.2), so no step count is frozen.
|
- **`stepCount`.** No discrete parameter is exposed (§7.2), so no step count is frozen — swept
|
||||||
|
explicitly over all 44 under Ruling 3 and confirmed: **every one ships `stepCount = 0`**, and
|
||||||
|
the coincidence is structural rather than lucky. §6.7.6, which also forbids the specific
|
||||||
|
mistake of exposing the editor's shift-snap grid as `stepCount`.
|
||||||
- **Hold fractions, sustain level, Trigger length.** Natural `[0,1]`, identity taper, nothing
|
- **Hold fractions, sustain level, Trigger length.** Natural `[0,1]`, identity taper, nothing
|
||||||
to freeze beyond the identity.
|
to freeze beyond the identity.
|
||||||
- **The limiter's −0.3 dBTP ceiling, `kMaxCurvePoints` = 128, the editor floor/budget
|
- **The limiter's −0.3 dBTP ceiling, `kMaxCurvePoints` = 128, the editor floor/budget
|
||||||
|
|||||||
Reference in New Issue
Block a user