Files
reasampler/docs/product/parameter-automation.md
T
daniel 256216d670 docs: close Γ-F6 and resequence Phase Γ into three waves
Dynamic latency ships as ruled; the restart cost is re-attributed to our own
setActive and filed in TODO. The reflow splits canvas (W1-T4) from arrangement
(W3-T1); preserve-time-stretch moves to W1-T5.
2026-08-01 16:51:04 -04:00

15 KiB

VST3 automation parameters — scoping, not scheduling

Daniel, 2026-08-01: "we need to scope the whole parameter system for automation, which will require redesigning the existing wiring." And, on Rate being latched at note-on: "we will need to analyze ALL the controls for LIVE use/automation use."

This is NOT Phase Γ work. It is framing and open questions, written now because Phase Γ makes two decisions whose cost changes permanently the day parameters ship, and because knowing that is what makes Γ's ordering correct rather than arbitrary. Nothing here is scheduled; nothing here is settled.

The one thing this doc does assert is a sequencing constraint — §4.


1. Current state, verified

The instrument has zero VST3 automatable parameters.

  • ReaSamplerProcessor::initialize never populates the parameter list, so getParameterCount() returns the SDK default 0. A host sees no parameters at all.
  • All state lives in the ComponentState blob — envelope v1…v11, params payload v1…v13 (core/instrument/map/component_state_io.h), a cross-artifact contract the extension's instrument_drop and the instrument's processor both read and write.
  • DeckParam ids are runtime-only and explicitly free to reorder — the editor's ParamControl is an alias of the same enum, and nothing persists an id.

Consequently the instrument today has: no host automation, no MIDI learn, no parameter linking, no host-side modulation, and no generic FX panel. Everything is reached through the plugin's own editor.


2. What is actually on the table (name the prize properly)

Framing this as "automation" undersells it. REAPER gives every exposed VST3 parameter a parameter-modulation block for free — an LFO, an envelope, an audio-follower/sidechain, MIDI link, and parameter linking, per parameter, with no plugin-side work beyond declaring the parameter.

So exposing parameters is not primarily about drawing automation lanes. It is the instrument gaining a modulation system it would otherwise have to design, build, persist and draw itself. A sampler with a host-provided LFO on filter cutoff, an envelope-follower on drive, and MIDI-CC on pitch offset is a materially different instrument, and none of that is our code.

That is the reason this is worth a phase. It is also the reason the parameter list's design matters more than the plumbing: the list is the modulation matrix's rows.


3. The hard problems, in the order they bite

3.1 Parameter IDs become FOREVER-STABLE

A VST3 ParamID is recorded in the project file. Once shipped, the id ↔ meaning mapping is frozen forever — the same discipline the extension's command-id strings and the VST3 class UIDs already carry, and the same discipline the params-payload version ladder carries.

DeckParam's "free to reorder" property dies the day parameters ship. Anything that wants to renumber, regroup or reorder that enum has to happen first. This is not a theoretical concern: Phase Γ adds two entries to it.

Open: is ParamID the DeckParam value directly, or an independent id space with an explicit mapping table? Direct is simpler and tempting; an independent space is what lets the enum keep being a UI-ordering convenience. Lean: independent id space with an explicit frozen table, on the same reasoning the command-id family already uses — the display order and the wire identity should not be the same number.

3.2 The taper becomes the host-facing contract

VST3 parameters are normalized [0,1] with the plugin owning the taper (toPlain/toNormalized). An automation envelope a user draws in REAPER is drawn against the normalized value.

Therefore: re-tapering a parameter after it ships silently re-interprets every recorded automation point in every saved project. A 40 %-of-travel node on an attack knob means 16 ms under a log taper and 800 ms under a linear one. There is no version ladder that can fix this, because the data is in the host's project file, not ours.

Today, re-tapering is free — normalization exists only in ui/deck_values.cpp as 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.

3.3 Two sources of truth

VST3 saves parameter values and calls setState/setComponentState. A value that lives in both can drift — a project reloaded with automation could restore the blob's value and then have it immediately overwritten by the host's parameter value, or the reverse, depending on call order.

Two coherent answers, both viable, neither obviously right:

  • (a) Parameters are the source of truth for everything automatable; the blob carries only non-automatable structure — sample refs, velocity curves, spline contours, mode selections, the loaded capture. Cleanest host semantics; largest rewrite; and it splits the parameter set across two storage mechanisms, which the "one parameter set" invariant went out of its way to unify.
  • (b) The blob stays authoritative; parameters are a projection with one defined sync direction and a defined precedence at load. Smallest change; preserves the cross-artifact blob contract intact; risks exactly the drift class above if the precedence is not airtight.

Open. This is the load-bearing architectural decision of that phase and it should be made first, the way Ξ-W2-T1's crossing decision was.

3.4 Which controls can be parameters at all — three classes

The good news: this analysis is already done once, in one place. isLiveDeckParam / liveCommitFor (ui/deck_groups) is exactly "which controls can change without a rebuild," which is the same question automation asks. Phase Γ widens it from two states to three (§3.5). The parameter work should widen the same decision point again rather than start a second table — that is the standing rule (core/instrument/CLAUDE.md: "which controls are live is ONE decision, recorded in ONE place").

Class Examples Automatable?
Continuous, live-safe filter cutoff/Q/morph/drive/mod amt/key-track, every stage time and level, pitch env depth, master gain, pitch offset Yes — the live tier already delivers them at block boundaries
Discrete / rebuild-tier voice count, Poly/Mono, Retrig/Legato, Gate/Trigger, Staged/Spline, pitch engine, filter law Only if each gains a live path, or is exposed as a stepped parameter that is explicitly not safe to automate at rate. Today they route through rebuildVoiceEngine or a full reload — neither is RT-safe at automation rates
Structural the loaded capture, SampleRefs, the three velocity curves, the three spline contours, loop points, root note No. These are not scalars; they stay in the blob
Latency-changing the limiter enable No — and for a reason unrelated to the live tier. Flipping it changes reported latency, which the SDK defines as a host deactivate/reactivate. §3.8

The awkward middle class is the second row, and it is where the design work is.

3.5 Live vs. latched is a per-parameter decision, and Γ opens the seam

Phase Γ settles that Rate is latched at note-on, and — importantly — settles it as a third commit class (Live / NoteOnLatched / Reload) recorded in the same deck_groups predicate, rather than as a special case at a call site. See docs/product/instrument-control-surface.md §2.3.

That is exactly the vocabulary the parameter system needs. A VST3 parameter has to declare what it means to move it mid-note, and the answer is per parameter:

  • Live — the sounding voice follows (the φ-holding applyLive rule).
  • Note-on latched — published, but read only by snapLive. Automation still works; it just takes effect on the next note. Rate is here, and the reason it is here is recorded: loop resolution and contour mapping are note-on folds, so live rate means re-folding a resolved loop mid-note without a discontinuity.
  • Not automatable — rebuild or structural.

Lifting Rate from latched to live is a real feature with a named cost, not a flag flip. When someone proposes it, that is the paragraph to read first.

3.6 Parameter count, grouping, and the generic panel

~60 DeckParams plus the non-deck controls (voice count, master gain, key-track, preview velocity, limiter — though the limiter enable is excluded from the automatable set, see §3.8). REAPER's generic FX panel and every automation-lane picker will list all of them flat unless they are grouped.

VST3's answer is IUnitInfo — a unit tree that maps naturally onto the deck's own group structure (PITCH/RATE, FILTER, VELOCITY, VOICE, the three envelopes, MASTER). Also needed per parameter: a display name that survives truncation, ParameterInfo::units, stepCount for discretes, and the right flags (kCanAutomate, kIsBypass — the limiter toggle is emphatically not the plugin's bypass parameter).

Open: does the unit tree mirror the deck's visual grouping (which Phase Γ has just reflowed into two rows) or the engine's signal flow? They currently agree. Keeping them agreeing is a constraint worth stating before they diverge.

3.7 Sample-accurate automation vs. block-boundary observation

The live-parameter seqlock is observed once per render() and once per note-on — block boundaries, by design, and that design is what keeps the per-sample path free of indirection. VST3's IParameterChanges can carry multiple points inside one block.

Block-boundary application is standard, acceptable, and what most instruments do. Reading the points sample-accurately would put a per-sample "has a parameter changed" question on the hot path, which the phase-wide guardrail forbids in its current form. Lean: block boundary, explicitly, and say so — then revisit only if a user reports audible stepping on a fast automation curve.

3.8 Latency reporting — SETTLED, and it removes one control from the parameter list

Fork Γ-F2 is ruled (Daniel, 2026-08-01): the limiter has lookahead and the plugin reports latency DYNAMICALLY — zero when the limiter is off, the lookahead when it is on. Do not plan against a zero-latency instrument.

The consequence for this doc is concrete and it is a subtraction from the parameter list:

The limiter enable is NOT automatable. It goes in §3.4's third class, and the reason is not that it lacks a live path — it is that changing it changes the plugin's reported latency, and the vendored SDK defines restartComponent(kLatencyChanged) as "the host has to deactivate and reactivate the plug-in" (pluginterfaces/vst/ivsteditcontroller.h:105-108). In this plugin a deactivate frees every sounding voice and a reactivate re-decodes the WAV. An automation lane toggling that parameter would deactivate the plugin on every flip.

Two corollaries the parameter work must carry rather than rediscover:

  • It is also not the plugin's kIsBypass parameter. A safety limiter is not a bypass; binding it to kIsBypass would hand the host a control that restarts the component.
  • Latency reporting must be derived from persisted state, not from a transient. The SDK states the new latency is what getLatencySamples returns after setActive(true) — and this plugin's setActive(false) frees essentially everything. Whatever holds the limiter flag must survive that cycle.

Full reasoning, the SDK quotes, and the required verification steps are in docs/product/instrument-control-surface.md §3.1.1. Fork Γ-F6 is now RULED (Daniel, 2026-08-01): dynamic latency ships as specced and the deactivate/reactivate is accepted. There is no constant-reported-latency fallback — that option is closed, not shelved — so this section does not shrink to a footnote and the limiter enable does not become automatable. Plan against the not-automatable classification; it is settled.

One future condition could reopen it, and it is worth knowing about. The restart is only expensive because this plugin's setActive(true) re-decodes the WAV — not because the SDK requires it. docs/TODO.md ("Decouple the instrument reload from VST3 activation") files that reduction, and "the limiter enable is wanted automatable" is one of its named trigger conditions. If the parameter work genuinely needs that lane, the answer is to do the decoupling first, not to re-litigate the classification.


4. The sequencing assertion — the one thing this doc claims

The knob-taper work (Phase Γ item D) must land before the parameter system. Not alongside; before.

  • Re-tapering is free today — normalization is a UI layer, the payload stores raw engine doubles, and a re-taper moves the needle angle and nothing else (saved projects reload bit-identical).
  • Re-tapering is permanently expensive after parameters ship — the taper becomes the host-facing normalization, and changing it re-interprets recorded automation in project files we do not own and cannot migrate (§3.2).

The same argument, weaker but real, applies to DeckParam additions and reordering (§3.1): Phase Γ adds Rate and Pitch to that enum, and doing it now costs one exhaustive-switch update; doing it after parameters ship costs a frozen-id decision.

So the tension the brief flags is real, and it resolves in Γ's favour. Both item D and the parameter system touch deck_values.cpp and editor_controls.cpp, and the ordering is not a coin flip: item D first, by a wide margin.


5. Open questions, collected

Not one of these is a Phase Γ blocker. They are what a future phase's first wave answers.

  1. Blob vs. parameters as the source of truth (§3.3) — the architectural decision, made first, before any wiring.
  2. ParamID space — direct DeckParam values, or an independent frozen table (§3.1). Lean: independent.
  3. The discrete/rebuild-tier controls (§3.4) — for each of voice count, Poly/Mono, Retrig/Legato, Gate/Trigger, Staged/Spline, pitch engine, filter law: gain a live path, expose as non-automatable, or omit from the parameter list entirely.
  4. Per-parameter live/latched classification (§3.5) — the three-state predicate Phase Γ opens must be filled in for every exposed parameter, including whether Rate is lifted to live and at what cost.
  5. Unit tree shape (§3.6) — mirror the deck's visual grouping, or the signal flow.
  6. Sample-accurate vs. block-boundary (§3.7). Lean: block boundary, stated explicitly.
  7. Does the extension's instrument_drop need to know? It writes the same ComponentState bytes. If parameters become the source of truth for automatable values (option (a)), a blob the extension writes would no longer fully determine the instrument's sound. That is a cross-artifact contract change and it is the sharpest hidden cost of option (a).
  8. Migration. Every saved project predates parameters. On first load a parameter's value comes from the blob; after that the host owns it. Confirm that path is lossless, and confirm what happens to a project saved by the new build and opened by an older one (the blob still carries everything, so it should be fine — verify, do not assume).