Service both VST3 parameter channels, and promote pitch key-track and Trigger length so all 44 ids issue

The SDK's own single-component sample drains inputParameterChanges in
process() and implements setParamNormalized; automation was reading the
GUI channel alone. The audio thread now patches a block it solely owns.
This commit is contained in:
2026-08-02 16:22:17 -04:00
parent bfaa0f2614
commit de5654fb6f
44 changed files with 1205 additions and 302 deletions
+33 -14
View File
@@ -98,22 +98,34 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
next touch re-imposing, a superseded value. Master gain has its own funnel
(`setMasterGainLinear`) because it is the one exposed control that does not ride the
parameter set.
- **`setState` ordering against the host's first parameter block is irrelevant by
construction.** There is one model and one funnel per control, so whichever writes last wins
and the host's display follows the model either way — the ordering is not assumed, it is
removed as a question.
- **`process()` reads no parameter queue and is unchanged by the parameter surface.** A host
write arrives on the UI/main thread and reaches the audio thread through the SAME live block
the editor's knobs publish into, observed once per `render()` — block boundaries, last write
wins. `[verify — DAW]` that REAPER delivers automation to a single-component plug-in through
`IEditController::setParamNormalized` and not through `ProcessData::inputParameterChanges`
alone; if it is the latter only, an RT-safe drain is required and `process()` is where it
would have to land.
- **BOTH delivery channels are serviced, and the audio-side one is the normative one.**
`IEditController::setParamNormalized` is the CONTROLLER channel — the SDK says a controller
"should update the according GUI element(s) only" there, so nothing about the audio may depend
on a host calling it. `ProcessData::inputParameterChanges` is the AUDIO channel, and the SDK's
own single-component sample (`public.sdk/samples/vst/again/source/againsimple.cpp`) drains it
in `process()` while also implementing `setParamNormalized`. We do both, for the same reason.
- **The audio thread is the sole writer of the block the ENGINE reads.** Two `LiveParams`
blocks: the model's publishers (editor commits, reload, `setState`) write `liveParams_` off
the audio thread and may allocate on the way; `process()` merges that block with the host's
automation points into `automationLive_`, which is what `SampleData::live` points at. Two
blocks rather than one because the seqlock's single-writer contract is load-bearing and the two
writers genuinely differ in thread. The merge republishes ONLY when either side moved, so a
block carrying neither costs one relaxed load and the engine's read shape is unchanged.
- **The automation values fold back into the model on the UI thread** (`drainAutomationToModel`,
called from `getState`, the editor's sync tick, and the bake's reload tail). The blob is
authoritative, so a value that never came back would be lost on save. The fold is suppressed
from notifying the host — the values came FROM it, and echoing them would let a lane in write
mode re-record its own playback.
- **`setState` does not need an ordering guarantee against the host's first parameter block.**
An automation point held by the audio thread is re-applied over every merge, so a written lane
outranks the restore whichever way round the two arrive — which is VST3's own rule, not a race
we lost.
- **`IMidiMapping` is deliberately NOT implemented** — no conventional CC names most of what
is exposed, an invented map would hijack CCs the user's controller already sends, and
REAPER's own per-parameter MIDI learn covers the case without freezing anything.
`IParameterFunctionName` and `IAutomationState` are assessed and not implemented; the reasons
are in the product spec and are not re-surveyed here.
`[verify — DAW]` REAPER's own per-parameter MIDI learn is expected to cover the case without
freezing anything. `IParameterFunctionName` and `IAutomationState` are assessed and not
implemented — `bake/CLAUDE.md` owns the `IAutomationState` reasoning, at its one consequence
site.
**Non-goals / guardrails.**
- The instrument never captures and never inserts into the arrange. Playback is a
@@ -151,6 +163,13 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
## Gotchas
- **`reasampler_processor.h` is a documented ~600-line-ceiling exception** (root `CLAUDE.md`,
structural heuristic 1), on the same footing as `voice.h`'s: it is ONE class declaration, so
the seam the heuristic asks for does not exist — a split would be an arbitrary bisection, and
the implementation is already split across three TUs on its real seams. Its bulk is the
drain-slot proof, the RT-discipline constraints and the two-block automation contract, all of
which the comment conventions name as keep-worthy. Not silent overshoot.
- **The bake click only ARMS; the editor's sync tick runs it.** Calling
`Main_OnCommandEx` inline from `WM_LBUTTONDOWN` would run the extension's whole landing
nested inside a mouse handler with `SetCapture` held, while the invoked action re-points