Skip to content

Processing and parameters

PluginProcessor::process runs on the audio thread once per engine sub-block. Everything is preallocated (process.rs: up to 256 parameter queues of 64 points, 1024 events); on overflow data is dropped rather than allocating. Each block:

  1. Clears the parameter and event lists and fills the input parameter changes: edits from the plug-in’s own editor (drained from a ring, at offset 0), then host values queued with queue_param (automation; two points per block so the plug-in ramps), then MIDI.
  2. Converts MIDI: note-on/off become note events; CCs, channel pressure and pitch bend become parameter changes through the IMidiMapping table. Unmapped controllers, poly pressure and program changes are dropped.
  3. Copies the host’s stereo buffers into input bus 0 (a mono bus gets L+R/2), runs process with a ProcessContext (tempo, time signature, musical position, bar position, playing, recording, cycle), and copies output bus 0 back (mono duplicated).
  4. Sends the last value of each output parameter queue to the controller side, which applies it with setParamNormalized in idle(). Plug-in event output is ignored.

setProcessing(true) is called lazily on the first block. Offline renders also run in kRealtime mode.

plugin/multi_out.rs. All buses’ buffers are allocated at instantiation, so switching one needs no allocation, but VST3 only allows it while the component is inactive, and setActive belongs to the main thread. PluginController::set_active_outputs therefore keeps the audio thread out without it ever waiting: each process and suspend takes OutGate with a compare-and-swap. The controller asks the processor to park; it fades its outputs over 5 ms, calls setProcessing(0) and leaves the gate parked, and process then outputs silence. If no block comes within 250 ms (nothing is calling process), or the plug-in isn’t processing, the controller takes the gate itself, waiting only for a block under way. Then setActive(0), activateBus, setActive(1), and the next block starts the plug-in again and fades in. The rest of the graph plays on. PluginProcessor::output_bus reads a bus’s last block; the engine’s dsp/multi_out.rs uses it.

  • Editor → processor: performEdit pushes (id, value) into the edits ring, which the next block drains.
  • Automation → processor: the engine calls queue_param (see the engine graph).
  • Automation → editor display: Session::idle → follow_editors → PluginController::set_param_value while playing in Read, Touch or Latch.
  • Gestures: beginEdit, performEdit and endEdit also push Gesture::{Begin, Perform, End} into a ring. Session::idle (every 30 ms) turns them into automation touches and pass points (session/auto_write.rs), so moves in a plug-in’s own editor are written as automation. They are timestamped with the playhead when idle runs, so their resolution is about 30 ms.
  • PluginController::parameters() lists the automatable, visible, writable parameters for the automation parameter pop-up.

session/vst_undo.rs turns the same gestures into undo steps, for plug-ins with an open editor (Session::vst_watches). Nothing else makes these steps, so a plug-in with its editor closed costs nothing.

  • The state from before an edit can’t be read once the edit has begun, so each Watch keeps the last known state: read when the editor opens, after each edit, and when the host has changed the plug-in (PluginController::host_changes: automation following, a controller assignment, load_state) and that has been quiet for 300 ms, at most once a second and 1 % of the time. An open drag calls getState not at all, however long it lasts. In vst_undo_check: 24 µs on TONE3000 without a model (4 KB), 365 µs on the 98 KB of LSP’s Multi-Sampler.
  • When a step is made: when the gesture ends, 60 ms later, once the edits have left the ring for the processor, whose state getState reads; a bypassed plug-in gets them through PluginProcessor::pass_edits. A gesture starting in those 60 ms joins the step, since a plug-in loading a preset by hand sends a run of them. checkpoint, undo and redo make pending steps first, keeping the order. The state read then is compared with the known one: no difference, no step. So a click that moves nothing doesn’t clear the redo stack.
  • Edits without a begin and end are a burst until they pause for 250 ms, and bursts on one parameter merge through the undo key vst-edit:<uid>:<param>. Since a plug-in may be moving a parameter itself, Tracker gives them less rope: read-only parameters are ignored, and a parameter is ignored from then on if its burst runs 10 s without a pause, three of its bursts leave the state as it was, or the plug-in has made eight such steps in 30 s (a gesture on it still counts). restartComponent(kParamValuesChanged) is treated as a burst, labelled “Parameters”, and gets the same cap.
  • Not steps: what the host sets (set_param_value, edit_param), and the same value coming back from the plug-in within 200 ms (host_echo). While the track writes automation during playback the move belongs to the pass’s step, and the known state just catches up.
  • Restoring is load_state with no parameter messages: the plug-in applies it as it does when loading, on the main thread while the audio thread runs (VST3 allows this). Gestures from the plug-in for the next 300 ms are its answer to that, and are dropped, also for automation write. Edits still in the ring would land on top, so flush_vst_edits first waits up to 50 ms for them.

PluginController::latency() calls getLatencySamples on the main thread. restartComponent stores its flags; idle() reports kLatencyChanged, and Session::idle then reruns update_pdc. kParamValuesChanged goes to undo. Other restart flags are ignored, and the component is not reactivated.

save_state reads component.getState and controller.getState; load_state calls component.setState, controller.setComponentState and controller.setState. The session stores the pair as base64 in PluginSlot::state and restores it straight after instantiation.