Report the instrument's automatable parameters to the host under a frozen id table, in signal-flow order, with real units

42 of 44 ids issued: pitch key-track and Trigger length stay reserved
pending a live path. Master gain reclassified Live — it never reloaded.
This commit is contained in:
2026-08-02 15:14:16 -04:00
parent c7afa3a80f
commit bfaa0f2614
38 changed files with 1742 additions and 218 deletions
+10 -5
View File
@@ -17,6 +17,11 @@ subdirectories:
- **`bake/`** — the resample bake's pure half: the programmed note resolved to a frame
window, the offline render over a voice engine built for that render alone, and the
ratified post-bake reset. See `bake/CLAUDE.md`.
- **`param/`** — what the instrument tells a VST3 host about its automatable parameters,
with no VST3 type in it: the FOREVER-FROZEN id table, the exposed set derived from
`deckParamCommit`, the plain-value layer, and the one formatter per unit category. Sits
ABOVE `ui/` — the list is a function of the commit predicate, never the reverse. See
`param/CLAUDE.md`.
- **`ui/`** — pure editor geometry/hit-test modules (the band-stack allocator and its band
interiors, waveform, keyboard strip, capture browser, param controls, envelope
overlay/edit). These are geometry-and-math only; the LICE draw + REAPER/VST3 plumbing is
@@ -309,7 +314,7 @@ anything for a trigger shape.
never buy itself a higher lowest-findable fundamental.
- `time_stretch` — the TIME half beside `pitch_shift`'s PITCH half, header-only: `StretchCursor`, the per-output-frame source-feed schedule (a fractional cursor carrying its rate debt, loop-wrapped), plus the rate bounds and their clamp. Rate 1.0 is exactly one source frame per output frame with no residue, which is what makes the unity Preserve read bit-identical to the pre-stretch engine. The bounds are **measured**, not arbitrary — see the header.
- `velocity_curve` — THE monotone spline, shared by every consumer: the three velocity transfer curves and the three spline EGs. `VelocityCurve` is evaluated as ONE OR MORE FritschCarlson monotone cubic Hermite splines joined at its HARD points — a hard knot is a sub-curve boundary for tangent purposes (exactly what the point array's own ends already are), so the two adjacent segments meet at their natural angle instead of a shared derivative and the no-overshoot guarantee holds PER SEGMENT rather than globally. Points are smooth by default; the ceiling is `kMaxCurvePoints` = 128, a MUSICAL bound (long rhythmic phrases, ~two points per articulation event) and not a performance one — **do not lower it**. `eval(velocity)` is the COLD reader, called once per note-on or once per drawn pixel column; `SplineCursor` is the RT one, an indexed segment search plus one Hermite evaluation with the segment and its tangents cached across samples. Both share the same `segmentTangents`/`hermiteAt` free functions, so there is one spline and not two. It carries its own y `CurveDomain`: UNIPOLAR [0,1] is the amp's GAIN, defaulting to `flat()` (y=1, every velocity→unity — a deliberate non-back-compat replacement of the old fixed `velocity/127` path, Daniel-approved); BIPOLAR [1,1] is the signed modulation shape for pitch and filter, defaulting to `zero()` so velocity modulates neither until a curve is drawn. A bipolar curve does not imply the absence of a depth beside it: the filter keeps its `velAmount` knob and the two compose multiplicatively (`velAmount × curve.eval(v)`, `play_params.h`), while the pitch curve's throw is the fixed `kVelocityPitchRangeSemitones`.
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob and the processor multiply so the needle, persisted value, and audio multiply cannot drift.
- `master_gain` — pure dB↔linear taper math (FB1): normalized [0,1] ↔ dB ↔ linear for the post-mixer master gain control (−∞…+24 dB, norm 0 = true silence, unity ≈ 0.714). Shared by the editor knob, the processor multiply and the host's `toPlain` so the needle, persisted value, audio multiply and reported dB cannot drift. Math only — the dB label is `param/param_format`'s, so the editor and the host cannot print it two ways.
- `limiter` — the master bus's lookahead brickwall limiter, the stage after `master_gain`'s multiply: a 4x-oversampled TRUE-PEAK detector in the SIDECHAIN ONLY (the signal path is never oversampled), one stereo-linked gain, a baked 0.3 dBTP ceiling and **no makeup gain of any kind**. The gain law is a sliding MINIMUM of the per-sample target over the lookahead window followed by a MOVING AVERAGE of the same width: every term of that average is a minimum whose own window contains the sample being gained, so the ceiling is held **structurally** rather than by a tuned attack, and the one-pole release only ever slows the RISE so that bound survives it. Bypassed and settled, `process()` returns without reading or writing a sample — the byte-identical at-rest path, on the same discipline as `live == nullptr` and the filter's exact skip at `modAmount == 0`. `prepare()` owns every allocation and every transcendental. **Switching is a MUTE, never a blend:** unlimited signal is emitted at weight 1 (the untouched bypass buffer) or at weight 0 and never in between, because a fraction of an unlimited signal is a peak over the ceiling — so the fade always rides the limited path and the hard edge always lands on the bypassed side, against silence. Do not reintroduce an equal-gain dry/wet crossfade over the toggle.
- `meter_ballistics` — the output meter's UI-side ballistics and dB scale: instantaneous rise, 20 dB/s fall, the 1.5 s peak hold and its release at the same rate, the clip latch, and the dB → normalized map over 60…+6 dBFS. The audio thread publishes raw block peaks and converts nothing; this module is what turns them into what the bar draws. Per-channel and stage-agnostic — the MASTER column's own state (both channels plus the gain-reduction lamp) composes it in `ui/master_meter`.
@@ -349,10 +354,10 @@ anything for a trigger shape.
value is COPIED rather than round-tripped: that taper bypass is mandatory and must never be
"simplified" back into a norm round trip), `deckParamUnit`/`snapDeckParamNorm` (THE snap-unit
table, and where each control's full scale enters — a whole DISPLAYED percent is a different
norm step at 0..100 %, 0..200 % and ±100 %), and `formatEnvTimeMs`, the
ONE time-constant formatter: every displayed time constant reads in **ms**, never seconds, so
two stage times are comparable at a glance. A display-unit decision only — nothing about the
stored representation changes. Links the header-only `play_seconds`, deliberately not
norm step at 0..100 %, 0..200 % and ±100 %). Display FORMATTING is not here — `param/`'s
`param_format` owns the one formatter per unit category, because the host and the editor must
be its two callers and neither may hold a second implementation. Links the header-only
`play_seconds`, deliberately not
`sample_map`: `PlaySeconds` is the whole of what a deck edits, and linking the mapping would
drag the bank model and the WAV codec in behind it. The shell keeps only the controls the
parameter set does not carry (key-track, voice count, master gain, preview velocity) and the