Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
vst3_adapter.hpp
Go to the documentation of this file.
1#pragma once
2
3// VST3 Adapter for Pulp
4// Implements Steinberg's VST3 interfaces by wrapping pulp::format::Processor
5// Uses SingleComponentEffect pattern (combined processor + controller)
6// Built from VST3 SDK headers (MIT license), not from any existing adapter
7
8// VST3 SDK requires one of these to be defined
9#ifndef NDEBUG
10 #ifndef DEVELOPMENT
11 #define DEVELOPMENT 1
12 #endif
13#else
14 #ifndef RELEASE
15 #define RELEASE 1
16 #endif
17#endif
18
19// Must come first per VST3 SDK requirements
20#include <public.sdk/source/vst/vstsinglecomponenteffect.h>
21
22#include <pluginterfaces/vst/ivstparameterchanges.h>
23#include <pluginterfaces/vst/ivstevents.h>
24#include <pluginterfaces/vst/ivstmidicontrollers.h>
25#include <pluginterfaces/vst/ivstnoteexpression.h>
26#include <pluginterfaces/vst/ivstunits.h>
27#include <pluginterfaces/base/ibstream.h>
28
35#include <pulp/format/detail/playhead_diff.hpp>
36#include <pulp/format/detail/vst3_restart_publisher.hpp>
45
46#include <array>
47#include <set>
48#include <atomic>
49#include <cstdint>
50#include <filesystem>
51#include <memory>
52#include <span>
53#include <vector>
54
55#include <pluginterfaces/gui/iplugview.h>
56
57namespace pulp::format {
58struct HostActionRequest;
59}
60
62
64
65// Constructs the editor IPlugView, defined in vst3_plug_view.cpp.
66//
67// This declaration is the adapter's only seam to its own GUI, and it is
68// deliberately view-free: PulpPlugView owns a ViewBridge and a
69// view::PluginViewHost, so naming the concrete type here would put
70// pulp/view on the include path of every translation unit that adapts VST3
71// audio. Returning the SDK interface instead keeps the audio adapter
72// compilable without the view layer, which is what lets it live in
73// pulp-format-core.
74//
75// vst3_plug_view.cpp is compiled per-plugin rather than into the shared
76// library, so this resolves the same way the direct constructor call did:
77// an undefined symbol satisfied by the plugin target.
78//
79// Both this call site and the definition are guarded by PULP_VST3_GUI, so
80// the two must agree: a GUI-capable adapter linked against a non-GUI
81// vst3_plug_view.cpp leaves this symbol undefined at link time rather than
82// silently losing the editor.
83Steinberg::IPlugView* make_plug_view(Processor& processor,
84 state::StateStore& store,
85 runtime::AliveToken::Handle owner_alive);
86
87// The process-context fields this adapter's decoder actually consumes,
88// as an IProcessContextRequirements flag mask.
89//
90// VST3 3.7+ makes this a *request*: the host supplies only the
91// ProcessContext fields the plug-in asks for here. The SDK is blunt about
92// the consequence of staying silent -- "If you do not implement this
93// interface, you may not get any information at all of the process
94// function!" -- and the trap is that SingleComponentEffect already
95// inherits IProcessContextRequirements and answers with
96// `processContextRequirements.flags`, which defaults to 0. So the
97// interface is advertised whether or not this mask is assigned: leaving it
98// unset is not a compile error and not a missing override, it is a
99// silently empty request that a strict host honours literally by sending
100// no tempo, no time signature and no transport state.
101//
102// Each flag below is here because build_process_context() reads the
103// matching kXxxValid bit; fields the decoder ignores (continuous time
104// samples, bar position -- derived from beats + time signature instead --
105// samples-to-next-clock, chord) are deliberately NOT requested, because
106// the point of the mask is to let the host skip work nobody consumes.
107// The host latches this once between initialize() and setActive().
108inline constexpr Steinberg::uint32 kProcessContextRequirements =
109 Steinberg::Vst::IProcessContextRequirements::kNeedTempo |
110 Steinberg::Vst::IProcessContextRequirements::kNeedTimeSignature |
111 Steinberg::Vst::IProcessContextRequirements::kNeedTransportState |
112 Steinberg::Vst::IProcessContextRequirements::kNeedProjectTimeMusic |
113 Steinberg::Vst::IProcessContextRequirements::kNeedCycleMusic |
114 Steinberg::Vst::IProcessContextRequirements::kNeedSystemTime |
115 Steinberg::Vst::IProcessContextRequirements::kNeedFrameRate;
116
117// The VST3 combined processor + controller
118// Wraps a pulp::format::Processor for the VST3 host
119//
120// Also implements IMidiMapping so MIDI controllers (CCs, mod wheel,
121// sustain, channel aftertouch, pitch bend) reach instruments. VST3 has no
122// raw MIDI controller events: the host queries getMidiControllerAssignment
123// for the ParamID each controller maps to, then delivers them as ordinary
124// parameter changes. The adapter reserves a private ParamID range for these
125// controllers (see detail/vst3_midi_mapping.hpp) and decodes them back into
126// MIDI messages in process().
127//
128// Implements INoteExpressionController so per-note expression (MPE) reaches
129// MPE-aware instruments. VST3 delivers per-note pitch / pressure / timbre as
130// Event::kNoteExpressionValueEvent keyed by the originating note-on's noteId.
131// The host first queries getNoteExpressionInfo to learn which expression
132// types the plug-in accepts; the adapter declares them only when the
133// descriptor opts into MPE. process() routes each value event through the
134// shared MpeVoiceTracker / MpeBuffer sidecar (the same contract the CLAP
135// adapter uses), mapping VST3 tuning -> per-note pitch bend, a volume-domain
136// expression -> per-note pressure, and brightness -> per-note timbre (CC74).
137class PulpVst3Processor : public Steinberg::Vst::SingleComponentEffect,
138 public Steinberg::Vst::IMidiMapping,
139 public Steinberg::Vst::INoteExpressionController,
140 public Steinberg::Vst::IKeyswitchController {
141public:
144
145 // FUnknown — expose IMidiMapping alongside the SingleComponentEffect
146 // interfaces. Refcounting is inherited from EditControllerEx1 via the
147 // base; only queryInterface needs the extra interface.
148 Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
149 void** obj) override;
150 Steinberg::uint32 PLUGIN_API addRef() override {
151 return Steinberg::Vst::SingleComponentEffect::addRef();
152 }
153 Steinberg::uint32 PLUGIN_API release() override {
154 return Steinberg::Vst::SingleComponentEffect::release();
155 }
156
157 // IPluginBase
158 Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) override;
159 Steinberg::tresult PLUGIN_API terminate() override;
160
161 // IMidiMapping — report which ParamID each (bus, channel, controller)
162 // maps to. Only populated when the descriptor accepts MIDI input.
163 Steinberg::tresult PLUGIN_API getMidiControllerAssignment(
164 Steinberg::int32 busIndex, Steinberg::int16 channel,
165 Steinberg::Vst::CtrlNumber midiControllerNumber,
166 Steinberg::Vst::ParamID& id) override;
167
168 // INoteExpressionController — declare the per-note expression types the
169 // plug-in accepts and convert between normalized values and strings.
170 // getNoteExpressionCount returns 0 (and the info/string methods decline)
171 // unless the descriptor opted into MPE, so a non-MPE plug-in advertises
172 // no note-expression surface and pays zero per-note cost in process().
173 Steinberg::int32 PLUGIN_API getNoteExpressionCount(
174 Steinberg::int32 busIndex, Steinberg::int16 channel) override;
175 Steinberg::tresult PLUGIN_API getNoteExpressionInfo(
176 Steinberg::int32 busIndex, Steinberg::int16 channel,
177 Steinberg::int32 noteExpressionIndex,
178 Steinberg::Vst::NoteExpressionTypeInfo& info /*out*/) override;
179 Steinberg::tresult PLUGIN_API getNoteExpressionStringByValue(
180 Steinberg::int32 busIndex, Steinberg::int16 channel,
181 Steinberg::Vst::NoteExpressionTypeID id,
182 Steinberg::Vst::NoteExpressionValue valueNormalized /*in*/,
183 Steinberg::Vst::String128 string /*out*/) override;
184 Steinberg::tresult PLUGIN_API getNoteExpressionValueByString(
185 Steinberg::int32 busIndex, Steinberg::int16 channel,
186 Steinberg::Vst::NoteExpressionTypeID id,
187 const Steinberg::Vst::TChar* string /*in*/,
188 Steinberg::Vst::NoteExpressionValue& valueNormalized /*out*/) override;
189
190 // IKeyswitchController — publishes Processor::note_names() as VST3 key
191 // switches, which is the surface a VST3 host uses to label individual keys
192 // (a drum instrument's "Kick", a sampler's articulation switches) instead
193 // of showing bare pitches. Each name becomes a single-key switch. A plug-in
194 // that names no notes reports a count of 0 and the host falls back to
195 // pitches, so nothing changes for existing plug-ins.
196 Steinberg::int32 PLUGIN_API getKeyswitchCount(
197 Steinberg::int32 busIndex, Steinberg::int16 channel) override;
198 Steinberg::tresult PLUGIN_API getKeyswitchInfo(
199 Steinberg::int32 busIndex, Steinberg::int16 channel,
200 Steinberg::int32 keySwitchIndex,
201 Steinberg::Vst::KeyswitchInfo& info /*out*/) override;
202
203 Steinberg::int32 PLUGIN_API getUnitCount() override;
204 Steinberg::tresult PLUGIN_API getUnitInfo(
205 Steinberg::int32 unitIndex, Steinberg::Vst::UnitInfo& info) override;
206 Steinberg::int32 PLUGIN_API getProgramListCount() override;
207 Steinberg::tresult PLUGIN_API getProgramListInfo(
208 Steinberg::int32, Steinberg::Vst::ProgramListInfo&) override;
209 Steinberg::tresult PLUGIN_API getProgramName(
210 Steinberg::Vst::ProgramListID, Steinberg::int32,
211 Steinberg::Vst::String128) override;
212 Steinberg::tresult PLUGIN_API getProgramInfo(
213 Steinberg::Vst::ProgramListID, Steinberg::int32,
214 Steinberg::Vst::CString, Steinberg::Vst::String128) override;
215 Steinberg::tresult PLUGIN_API hasProgramPitchNames(
216 Steinberg::Vst::ProgramListID, Steinberg::int32) override;
217 Steinberg::tresult PLUGIN_API getProgramPitchName(
218 Steinberg::Vst::ProgramListID, Steinberg::int32, Steinberg::int16,
219 Steinberg::Vst::String128) override;
220 Steinberg::Vst::UnitID PLUGIN_API getSelectedUnit() override;
221 Steinberg::tresult PLUGIN_API selectUnit(Steinberg::Vst::UnitID) override;
222 Steinberg::tresult PLUGIN_API getUnitByBus(
223 Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection,
224 Steinberg::int32, Steinberg::int32,
225 Steinberg::Vst::UnitID&) override;
226 Steinberg::tresult PLUGIN_API setUnitProgramData(
227 Steinberg::int32, Steinberg::int32, Steinberg::IBStream*) override;
228
229 // IEditController — parameter value/string conversion. Route through the
230 // author-supplied ParamInfo::to_string / from_string so a host's generic
231 // editor and text-entry field show and accept the plugin's own formatting
232 // (e.g. "-6.0 dB", "1.2 kHz"). Both decline to SingleComponentEffect's
233 // stock numeric formatting when the parameter declares no converter, so
234 // existing plugins are unchanged.
235 Steinberg::tresult PLUGIN_API getParamStringByValue(
236 Steinberg::Vst::ParamID tag,
237 Steinberg::Vst::ParamValue valueNormalized,
238 Steinberg::Vst::String128 string) override;
239 Steinberg::tresult PLUGIN_API getParamValueByString(
240 Steinberg::Vst::ParamID tag, Steinberg::Vst::TChar* string,
241 Steinberg::Vst::ParamValue& valueNormalized) override;
242
243 // Keep Pulp-authored parameters in their declared plain StateStore domain
244 // when a host asks the VST3 controller to convert values. Unknown IDs,
245 // including hidden controller IDs, retain the SDK's normalized fallback.
246 Steinberg::Vst::ParamValue PLUGIN_API normalizedParamToPlain(
247 Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue valueNormalized) override;
248 Steinberg::Vst::ParamValue PLUGIN_API plainParamToNormalized(
249 Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue plainValue) override;
250
251 // IEditController — editor view creation
252 Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
253
254 // IAudioProcessor
255 Steinberg::tresult PLUGIN_API setBusArrangements(
256 Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
257 Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) override;
258
259 Steinberg::tresult PLUGIN_API canProcessSampleSize(
260 Steinberg::int32 symbolicSampleSize) override;
261
262 Steinberg::tresult PLUGIN_API setupProcessing(
263 Steinberg::Vst::ProcessSetup& setup) override;
264
265 Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override;
266
267 Steinberg::uint32 PLUGIN_API getLatencySamples() override;
268 Steinberg::uint32 PLUGIN_API getTailSamples() override;
269
270 Steinberg::tresult PLUGIN_API process(
271 Steinberg::Vst::ProcessData& data) override;
272
274 return param_events_;
275 }
276
278 return param_events_monitor_;
279 }
280
285 state::ParamID bypass_parameter_id() const { return bypass_param_id_; }
286
294 std::uint32_t note_expression_drop_count() const {
295 return note_expression_drops_.load(std::memory_order_relaxed);
296 }
297
298 // IComponent (state)
299 // A host parameter write must land in the StateStore synchronously.
300 // Without this it only arrives via the process() parameter queue, so a host
301 // that writes a parameter and saves before the next audio block serializes
302 // a stale value.
303 Steinberg::tresult PLUGIN_API setParamNormalized(Steinberg::Vst::ParamID id,
304 Steinberg::Vst::ParamValue value) override;
305 Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) override;
306 Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) override;
307
313 void set_factory_presets_directory(std::filesystem::path dir) {
314 factory_presets_dir_ = std::move(dir);
315 }
316
317 static Steinberg::FUnknown* createInstance(void*);
318
322
323 private:
324 // Tell every live contract view that the processor is going away, so each
325 // invalidates and destroys its editor while the processor that owns the
326 // editor factory, and the store its port reads, are still alive.
327 void terminate_contract_views();
328 bool contract_host_action(const HostActionRequest& request);
329
330 // Declared FIRST so it is destroyed LAST: the final detach cancels and
331 // joins the tracing auto-flush timer and writes the .pftrace, and that must
332 // happen after every span this instance can still emit. RAII rather than a
333 // hand-balanced attach/detach pair — a leaked attachment silently produces
334 // an empty capture. No-op unless PULP_TRACING=ON.
336 ProcessorFactory factory_;
337 // The store is declared before the Processor so it is destroyed after it.
338 // `Processor::state()` dereferences a pointer to this store, and a Processor
339 // may read it from its destructor or from a worker thread that destructor is
340 // about to join. Reversing these two lines hands that thread a freed store.
341 state::StateStore store_;
342 std::unique_ptr<Processor> processor_;
343 // Panic requests (Processor::request_panic), serviced before each block.
344 std::shared_ptr<PanicState> panic_state_;
345 std::unique_ptr<ParamGroupProjection> param_groups_;
346 Steinberg::Vst::UnitID selected_unit_ = Steinberg::Vst::kRootUnitId;
347
348 // Factory presets, published as the VST3 program list. Built once in
349 // initialize(); `program_list_active_` is false when the plug-in ships none
350 // (or a real parameter already owns the program-change ParamID), in which
351 // case the adapter publishes no list and registers no program parameter.
352 std::filesystem::path factory_presets_dir_;
353 FactoryProgramTable programs_;
354 bool program_list_active_ = false;
355 // The current program, mirrored from the StateStore parameter that holds
356 // the selection. `program_applied_` is false until a program has been
357 // applied or a saved selection restored; until then any selection counts.
358 // Written by whichever thread delivers the program change, read by both.
359 std::atomic<std::int64_t> program_index_{0};
360 std::atomic<bool> program_applied_{false};
361 bool program_change_needed(std::size_t index) const;
362 // Set when the audio thread applied a program: the VST3 parameter objects
363 // still hold the old values until the main thread syncs them.
364 std::atomic<bool> parameter_objects_stale_{false};
365 // Applies program @p index. @p audio_thread selects the write path that
366 // never touches the file system, allocates, or calls the host.
367 void apply_program(std::size_t index, bool audio_thread, std::int32_t offset);
368 // Copies every StateStore value (and the program selection) into the VST3
369 // parameter objects the host reads back. Main thread only.
370 void sync_parameter_objects_from_store();
371 // Keeps a host setState() off the processor while process() is inside it.
372 // Declared after processor_ so it outlives every render that consults it.
373 StateRestoreGate state_restore_gate_;
374 // Declared after processor_ so reverse member destruction retires retained
375 // editor handles before either referenced object is released.
376 runtime::AliveToken owner_alive_;
377 // Contract views the host still holds (not owned: the host releases them).
378 std::vector<ContractPlugView*> contract_views_;
379
380 // Working buffers
381 std::vector<float*> input_ptrs_;
382 std::vector<float*> output_ptrs_;
383 std::vector<const double*> input64_ptrs_;
384 std::vector<double*> output64_ptrs_;
385 std::vector<std::vector<float>> f64_input_scratch_;
386 std::vector<std::vector<float>> f64_output_scratch_;
387 // Second input bus routed to Processor::set_sidechain(). Only input
388 // bus 1 is consumed; additional buses are ignored because the
389 // Processor API exposes a single sidechain slot. These arrays are sized in
390 // setupProcessing() to max(declared, natively accepted) channels so native
391 // layout negotiation cannot allocate or truncate on the audio thread. The
392 // unsupported-layout silence path retains the declared safe width.
393 std::vector<float*> sidechain_ptrs_;
394 std::vector<const double*> sidechain64_ptrs_;
395 std::vector<std::vector<float>> f64_sidechain_scratch_;
396 // Per-bus channel-pointer storage for secondary (aux) output buses,
397 // indexed [aux_bus][channel]. aux_output_ptrs_[i] backs the BufferView for
398 // the host output bus at index i+1, routed to the Processor's richer
399 // process(ProcessBuffers&) surface for multi-out instruments. Each
400 // sub-vector is sized in setupProcessing() from the ACCEPTED VST3 bus
401 // arrangement (which setBusArrangements() may have shifted mono↔stereo away
402 // from the descriptor default), so the routing path reuses this storage,
403 // allocates nothing on the audio thread, and never silently drops a channel
404 // the host negotiated.
405 std::vector<std::vector<float*>> aux_output_ptrs_;
406 std::vector<std::vector<double*>> aux_output64_ptrs_;
407 // Accepted native channel count per secondary output bus (index i = host
408 // output bus i+1), captured in setupProcessing(). The unsupported-layout
409 // silence path retains the descriptor-safe width. Reported as the aux
410 // ProcessBusBufferView's declared_channels so matches_declared_layout()
411 // compares host buffers against the active processing contract.
412 std::vector<int> declared_aux_channels_;
413 std::vector<std::vector<std::vector<float>>> f64_aux_output_scratch_;
414
415 // Per-block MIDI buffers, reused across process() calls. Reserved +
416 // realtime-capacity-limited in setupProcessing() so add()/add_sysex_copy()
417 // never heap-allocate on the audio thread (they drop past the reserved
418 // worst-case instead). Previously these were block-local, so the first
419 // add() on any block carrying MIDI allocated.
420 midi::MidiBuffer midi_in_;
421 midi::MidiBuffer midi_out_;
422
423 // Editor -> host parameter reporting. Owns the provenance decision (which
424 // writes are the EDITOR's), the single normalized snapshot per edit, and
425 // the begin/value/end ordering. Replaces a local "is a gesture open?"
426 // inference that host automation could satisfy — see
427 // host_parameter_edit.hpp. Held by pointer because it installs store
428 // callbacks in its constructor and removes them in its destructor, which
429 // must happen at defined points in this object's lifetime.
430 std::unique_ptr<HostParameterEditBridge> edit_bridge_;
431
432 // Parameter output: snapshot values before process to detect plugin-side changes
433 std::vector<float> param_snapshot_;
434 state::ParameterEventQueue param_events_;
435 // Processor::wants_sample_accurate_parameters(), asked in initialize().
436 bool sample_accurate_params_ = false;
437 // Records an opted-out processor reading param_events(); see processor.hpp.
438 ParamEventsOptOutMonitor param_events_monitor_;
439 // A block's automated values, written to the store in one batch after the
440 // input queues are read; one slot per parameter, reserved in
441 // setupProcessing().
442 std::vector<state::StateStore::ParamWrite> param_writes_;
443 void queue_param_write(state::ParamID id, float value);
444 // Lent to param_events_.sort(); sized in setupProcessing().
445 std::vector<state::ParameterEvent> param_sort_scratch_;
446 std::vector<std::uint32_t> param_offset_counts_;
447 // Sample-accurate parameter OUTPUT: the processor pushes offset-tagged events
448 // via push_output_param_event(); we drain them into per-parameter VST3 output
449 // queues after process(). All three vectors are sized off the audio thread
450 // (at prepare / block start alongside param_snapshot_) so the per-block drain
451 // stays allocation-free. output_param_has_event_ is the skip-set that keeps
452 // the offset-0 snapshot fallback from double-reporting a param that already
453 // emitted explicit events; output_param_queue_cache_ caches one VST3 queue
454 // per parameter index so a param with multiple offsets reuses one queue in
455 // ascending-offset order.
456 state::ParameterEventQueue output_param_events_;
457 std::vector<std::uint8_t> output_param_has_event_;
458 std::vector<Steinberg::Vst::IParamValueQueue*> output_param_queue_cache_;
459 // StateStore generation param_snapshot_ was taken at. Every store write
460 // advances the generation, so while it is unchanged the snapshot still
461 // equals the live values and the per-block refresh is skipped; likewise the
462 // post-process diff only runs when the generation moved across process().
463 // The snapshot is untrusted until the first refresh and after a refresh
464 // that overlapped a writer (boundary::refresh_param_snapshot).
465 boundary::ParamSnapshotState snapshot_state_;
466 std::uint64_t pre_process_generation_ = 0;
467
468 // Bus views handed to the Processor, kept as members so a block only
469 // writes the entries it routes instead of value-initialising every slot.
470 std::array<ProcessBusBufferView<float>, BusBufferSet::kMaxBuses> rt_output_buses_{};
471 std::array<ProcessBusBufferView<double>, BusBufferSet::kMaxBuses> rt_output64_buses_{};
472 std::array<ProcessBusBufferView<const double>, 2> rt_input64_buses_{};
473 // Channel count each declared bus expects, captured when the arrangement
474 // is (re)negotiated so process() validates layout without SDK RTTI string
475 // compares per block. Index past the cached count falls back to the bus.
476 static constexpr std::size_t kCachedBusCount = 32;
477 std::array<int, kCachedBusCount> expected_in_channels_{};
478 std::array<int, kCachedBusCount> expected_out_channels_{};
479 std::size_t cached_in_bus_count_ = 0;
480 std::size_t cached_out_bus_count_ = 0;
481 std::atomic<bool> bus_channel_cache_valid_{false};
482 void refresh_bus_channel_cache();
483
484 // Runs the adapter's own per-block path once on silence at activation so
485 // the host's first real block does not pay to fault in cold code and
486 // buffers. It never runs Processor::process() / process_f64() and publishes
487 // nothing to the host; it does hand the Processor the per-block pointers
488 // (parameter events, output events, MPE input) that every block sets and
489 // the first real block sets again. While set, process() also skips the
490 // trigger reset and every store- and host-visible side effect.
491 bool warming_render_path_ = false;
492 void warm_render_path();
493
494 // ── Per-note expression (MPE) sidecar ───────────────────────────────────
495 //
496 // Mirrors the CLAP adapter's MPE wiring: inbound note events plus the MIDI
497 // messages synthesized from note-expression value events run through the
498 // sidecar's tracker, whose callbacks append per-note expression deltas to
499 // its buffer. The buffer is handed to the Processor via set_mpe_input() for
500 // the duration of each process() call. Reserved + capacity-limited in
501 // setupProcessing() so the process path never allocates. Only active when
502 // the descriptor opts into MPE (mpe_.enabled). See boundary::MpeSidecar.
503 boundary::MpeSidecar mpe_;
504
505 // noteId -> (channel, note) linkage. VST3 note-expression value events
506 // reference the noteId carried by the originating note-on, so the adapter
507 // must remember which (channel, note) each live noteId maps to in order to
508 // route an expression to the right MPE voice. Fixed-capacity and
509 // allocation-free: a note-on with noteId >= 0 claims a slot, the matching
510 // note-off releases it. A slot's `active` flag identifies a live mapping.
511 struct NoteIdSlot {
512 bool active = false;
513 Steinberg::int32 note_id = -1;
514 uint8_t channel = 0;
515 uint8_t note = 0;
516 };
517 static constexpr std::size_t kMaxLiveNoteIds = 128;
518 std::array<NoteIdSlot, kMaxLiveNoteIds> note_id_map_{};
519
520 // Saturating drop counter for per-note expression routing failures (full
521 // noteId map on insert, or a value event for an unmapped noteId). Atomic so
522 // a host thread can read it via note_expression_drop_count() while the audio
523 // thread increments it; never the source of an audio-thread log.
524 std::atomic<std::uint32_t> note_expression_drops_{0};
525
526 // Record / release / look up a noteId -> (channel, note) mapping. All
527 // O(kMaxLiveNoteIds), allocation-free, audio-thread safe.
528 // note_id_map_insert returns false when the table is full (the mapping was
529 // dropped) so the caller can bump the drop counter.
530 bool note_id_map_insert(Steinberg::int32 note_id, uint8_t channel,
531 uint8_t note);
532 void note_id_map_erase(Steinberg::int32 note_id);
533 const NoteIdSlot* note_id_map_find(Steinberg::int32 note_id) const;
534 void note_id_map_clear();
535
536 // Cached parameter ID of the VST3 bypass parameter. 0 when none is
537 // available, so process() never short-circuits.
538 state::ParamID bypass_param_id_ = 0;
539
540 // True when the descriptor accepts MIDI input, so the adapter has
541 // registered the reserved hidden controller parameters and
542 // getMidiControllerAssignment / the process()-side controller decode
543 // are live. Mirrors the accepts_midi gating on the event input bus.
544 bool midi_controller_mapping_active_ = false;
545
546 // Membership bitmap over the reserved controller ParamID range, indexed
547 // by (id - kVst3MidiCcParamBase). A slot is true only if the adapter
548 // ACTUALLY registered that ID as a hidden controller parameter — i.e.
549 // the ID did not collide with a real plug-in parameter. Registration,
550 // getMidiControllerAssignment, and the process()-side diversion all use
551 // this one predicate, so a real plug-in parameter that happens to fall
552 // in the reserved range is never reported as a controller assignment and
553 // never diverted to MIDI (its host param-changes still reach store_).
554 // Sized once at init; the process()-thread lookup is O(1) and never
555 // allocates. Empty when MIDI mapping is inactive.
556 std::vector<bool> registered_controller_ids_;
557
558 // True iff `id` was registered as a hidden controller parameter. O(1),
559 // allocation-free — safe to call on the audio thread.
560 bool is_registered_controller(state::ParamID id) const;
561
562 // ── process() phase helpers ──────────────────────────────────────────────
563 // process() runs these phases in a fixed order. Each reads adapter member
564 // state (midi_in_, param_events_, output_ptrs_, store_, processor_, …)
565 // directly; only per-block locals are threaded explicitly. This is pure
566 // code motion — the audio output is bit-identical to the previously
567 // monolithic process(). See vst3_adapter.cpp.
568
569 // Phase (a): clear the reused param/MIDI buffers, decode host input
570 // parameter changes into the StateStore boundary, and divert
571 // IMidiMapping-registered controller changes into midi_in_. Sorts
572 // param_events_ on exit.
573 void process_decode_input_parameters(Steinberg::Vst::ProcessData& data);
574
575 // Validate the host block against the currently negotiated component-bus
576 // topology. Malformed layouts are zeroed and rejected before any host
577 // storage is published to the Processor.
578 bool process_validate_layout(Steinberg::Vst::ProcessData& data,
579 bool host_f64,
580 int original_num_samples);
581
582 // Phase (d, f32 half): wire the host's main input, sidechain, and main
583 // output channel pointers (with f64 boundary marshalling) into the reused
584 // ptr vectors, pre-zero routed aux output buses, and report the resolved
585 // channel counts.
586 void process_wire_buffers(Steinberg::Vst::ProcessData& data, bool host_f64,
587 bool native_f64, bool boundary_f64,
588 int num_samples, int original_num_samples,
589 int& in_channels, int& out_channels,
590 int& sc_channels);
591
592 // Phase (c): VST3 processBlockBypassed pass-through. Returns true when the
593 // bypass parameter is engaged and the block was handled here (the caller
594 // returns kResultOk); false when the plugin should process normally.
595 bool process_maybe_bypass(Steinberg::Vst::ProcessData& data, bool host_f64,
596 int in_channels, int out_channels,
597 int original_num_samples);
598
599 // Phase (b): decode host input events (notes, per-note expression, SysEx)
600 // onto midi_in_, then sort the combined controller+note stream.
601 void process_decode_input_events(Steinberg::Vst::ProcessData& data);
602
603 // Phase (f): drain the processor's sample-accurate output parameter events
604 // and the snapshot-diff fallback into the host output parameter queues.
605 void process_publish_output_params(
606 Steinberg::Vst::ProcessData& data,
607 std::span<const state::ParamInfo> all_params);
608
609 // Phase (h): OR-accumulate the processor's pending latency/tail restart
610 // flags into restart_publisher_ (RT-safe; the host callback fires later on
611 // the main thread).
612 void process_publish_restart_flags();
613
614 // Per-output-channel dry delay used during the bypass pass-through.
615 // A plugin that reports latency gets host plugin-delay-compensation on
616 // its path, so the bypassed dry signal must be delayed by exactly that
617 // many samples to stay sample-aligned with the compensated timeline.
618 // Sized once in setupProcessing() (off the audio thread); leaves the
619 // zero-copy fast path in place when the reported latency is 0. Shared
620 // adapter-boundary component so every format's bypass latency behaves
621 // identically (CLAP uses the same class).
623
624 // Host accommodations, resolved once in initialize() via the runtime
625 // policy (env / API / compile default). Adapters consult these flags
626 // instead of hardcoding DAW workarounds.
627 HostQuirks quirks_{};
628
629 // The processor is always prepare()'d with the descriptor-default
630 // channel counts (see setupProcessing), so these cache what the
631 // processor's buffers are sized for. When setBusArrangements accepts
632 // an arrangement the processor does not natively support, process()
633 // hands the processor only its prepared channel counts and zero-fills
634 // the host's extra channels.
635 int native_in_ = 0;
636 int native_out_ = 0;
637 // Descriptor contract cached during setupProcessing(); process() must not
638 // materialize the by-value descriptor on the audio thread.
639 bool descriptor_main_mono_ = false;
640 bool silence_unsupported_active_ = false;
641 bool native_f64_enabled_ = false;
642 Steinberg::int32 selected_sample_size_ = Steinberg::Vst::kSample32;
643 // setupProcessing() may be followed by setActive(false)/setActive(true)
644 // during an offline host render. Keep the prepared contract so activation
645 // can restore Processor-owned scratch and DSP state after release().
646 PrepareContext prepared_context_{};
647 Processor::BusesLayout prepared_layout_{};
648 bool has_prepared_context_ = false;
649
650 // Block size the processor's scratch buffers were prepared for in
651 // setupProcessing() (setup.maxSamplesPerBlock). process() clamps an
652 // oversized render to this so a host that exceeds the advertised max
653 // can't overrun the prepared buffers. 0 means "no limit set yet".
654 int max_block_size_ = 0;
655
656 // Previous-block transport snapshot used to derive the
657 // `tempo_changed` / `time_sig_changed` / `transport_changed` flags
658 // on `ProcessContext`. Default-constructed so the first process()
659 // call after construction reports no changes.
660 detail::PlayheadSnapshot playhead_prev_{};
661
662 // MainThreadDispatcher backend token. Acquired in initialize(),
663 // released in terminate(). On macOS this lets adapter-side and
664 // view-side code marshal work onto the DAW's main thread via
665 // `pulp::events::MainThreadDispatcher::call_async`.
666 pulp::events::MainThreadDispatcher::Token main_thread_token_ = 0;
667
668 // Marshals IComponentHandler::restartComponent off the audio thread.
669 // process() OR-accumulates latency/tail restart flags into this publisher
670 // (RT-safe); the actual host callback fires on the main thread via
671 // deliver_pending_restart() at a main-thread safe point outside any host
672 // call. See vst3_restart_publisher.hpp for the contract.
673 detail::Vst3RestartPublisher restart_publisher_;
674
675 // Liveness/lifetime token shared with every main-thread lambda this adapter
676 // posts to the MainThreadDispatcher. A posted lambda captures this shared_ptr
677 // (keeping the flag alive) plus a raw `this`, and checks the flag before
678 // touching `this`. terminate() flips it to false, so a callback still queued
679 // on the host's main thread when the component is destroyed becomes a no-op
680 // instead of dereferencing freed memory. Created in initialize().
681 std::shared_ptr<std::atomic<bool>> drain_alive_;
682
683 // True between setActive(true) and setActive(false)/terminate(): drives the
684 // self-rescheduling paced main-thread poll so a mid-stream latency/tail
685 // change is delivered without depending on an incidental host query.
686 std::shared_ptr<std::atomic<bool>> poll_active_;
687
688 // Deferred delivery of the two host callbacks this adapter owns:
689 // restart_publisher_ (componentHandler->restartComponent(flags)) and the
690 // Processor's state-dirty flag (IComponentHandler2::setDirty). Neither may
691 // be called from process(), and neither may be called from inside a
692 // host-initiated call (setActive / getLatencySamples / getTailSamples /
693 // getState): hosts invoke those while holding their own processing lock,
694 // and a host that services restartComponent synchronously (deactivating
695 // and reactivating the component on kReloadComponent, retaking that same
696 // non-recursive lock) deadlocks if the plug-in calls back into it
697 // re-entrantly.
698 //
699 // Delivery therefore only happens at a main-thread safe point outside any
700 // host call: (a) the paced self-rescheduling poll tick while active, and
701 // (b) a coalesced one-shot main-thread post that host entrypoints request
702 // via schedule_host_notifications(). The post runs after the host call has
703 // returned and released its locks.
704 enum class InlineFallback : std::uint8_t {
705 // The entrypoint may deliver inline when no main-thread dispatcher can
706 // accept a post (Windows / Linux hosts without a registered backend).
707 // Only state-free queries allow this; it keeps notifications flowing
708 // on those platforms, and a re-entrancy guard still refuses a nested
709 // delivery.
710 allowed,
711 // Never deliver inline, even without a dispatcher: the entrypoint is a
712 // host activation transition, where hosts hold their processing lock.
713 // The flags stay latched for the next safe delivery point.
714 forbidden,
715 };
716 void schedule_host_notifications(InlineFallback fallback);
717 void deliver_pending_restart();
718 // Delivers the Processor's state-dirty edge as IComponentHandler2::setDirty.
719 // Main-thread only; a host that predates IComponentHandler2, or that does not
720 // implement it, simply drops the edge (the flag is still consumed).
721 void deliver_pending_dirty();
722 // Both of the above, in order. Every main-thread delivery path calls this.
723 // Guarded against re-entry: a host that calls back into the plug-in from
724 // inside restartComponent/setDirty cannot trigger a nested delivery.
725 void deliver_pending_host_notifications();
726
727 // True while a one-shot main-thread delivery post is queued, so a burst of
728 // host entrypoint calls coalesces into one post. Cleared by the post
729 // itself before it delivers. Main-thread only after initialize().
730 std::shared_ptr<std::atomic<bool>> deferred_delivery_posted_;
731 // True while deliver_pending_host_notifications() is calling into the
732 // host. Main-thread only.
733 bool delivering_host_notifications_ = false;
734
735 // Start / stop the paced main-thread poll loop. start is idempotent; stop
736 // relies on poll_active_ going false so the next scheduled tick does not
737 // re-post. Both are main-thread only.
738 void start_restart_poll();
739 void schedule_restart_poll_tick();
740
741public:
742 // Test-only hook: drive the main-thread restart drain directly. Mirrors
743 // what a host-driven main-thread entrypoint does in production. Returns the
744 // coalesced flags delivered (0 when nothing was pending).
746 std::int32_t delivered = 0;
747 if (parameter_objects_stale_.exchange(false, std::memory_order_acq_rel))
748 sync_parameter_objects_from_store();
749 auto* handler = getComponentHandler();
750 restart_publisher_.poll_main_thread(
751 [&](std::int32_t flags) {
752 delivered = flags;
753 if (handler) handler->restartComponent(flags);
754 });
755 return delivered;
756 }
757
759 return restart_publisher_.dispatch_armed();
760 }
761};
762
763} // namespace pulp::format::vst3
The shared adapter-boundary core.
std::uint64_t Token
Definition main_thread_dispatcher.hpp:24
static constexpr std::size_t kMaxBuses
Definition process_block.hpp:148
Definition factory_program_table.hpp:40
Records, for one adapter, whether an opted-out processor read param_events(), and logs it once.
Definition processor.hpp:1362
Definition processor.hpp:174
Definition vst3_contract_view.hpp:57
Steinberg::tresult PLUGIN_API getNoteExpressionValueByString(Steinberg::int32 busIndex, Steinberg::int16 channel, Steinberg::Vst::NoteExpressionTypeID id, const Steinberg::Vst::TChar *string, Steinberg::Vst::NoteExpressionValue &valueNormalized) override
Steinberg::tresult PLUGIN_API getUnitInfo(Steinberg::int32 unitIndex, Steinberg::Vst::UnitInfo &info) override
std::int32_t poll_pending_restart_for_test()
Definition vst3_adapter.hpp:745
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid, void **obj) override
Steinberg::IPlugView *PLUGIN_API createView(Steinberg::FIDString name) override
Steinberg::tresult PLUGIN_API getProgramPitchName(Steinberg::Vst::ProgramListID, Steinberg::int32, Steinberg::int16, Steinberg::Vst::String128) override
Steinberg::tresult PLUGIN_API setBusArrangements(Steinberg::Vst::SpeakerArrangement *inputs, Steinberg::int32 numIns, Steinberg::Vst::SpeakerArrangement *outputs, Steinberg::int32 numOuts) override
Steinberg::tresult PLUGIN_API hasProgramPitchNames(Steinberg::Vst::ProgramListID, Steinberg::int32) override
Steinberg::tresult PLUGIN_API setParamNormalized(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue value) override
Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream *state) override
Steinberg::tresult PLUGIN_API getProgramInfo(Steinberg::Vst::ProgramListID, Steinberg::int32, Steinberg::Vst::CString, Steinberg::Vst::String128) override
const state::ParameterEventQueue & last_input_param_events() const
Definition vst3_adapter.hpp:273
Steinberg::Vst::ParamValue PLUGIN_API normalizedParamToPlain(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue valueNormalized) override
Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown *context) override
Steinberg::int32 PLUGIN_API getKeyswitchCount(Steinberg::int32 busIndex, Steinberg::int16 channel) override
Steinberg::tresult PLUGIN_API terminate() override
Steinberg::int32 PLUGIN_API getUnitCount() override
Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override
Steinberg::uint32 PLUGIN_API addRef() override
Definition vst3_adapter.hpp:150
Steinberg::tresult PLUGIN_API getNoteExpressionStringByValue(Steinberg::int32 busIndex, Steinberg::int16 channel, Steinberg::Vst::NoteExpressionTypeID id, Steinberg::Vst::NoteExpressionValue valueNormalized, Steinberg::Vst::String128 string) override
Steinberg::tresult PLUGIN_API getMidiControllerAssignment(Steinberg::int32 busIndex, Steinberg::int16 channel, Steinberg::Vst::CtrlNumber midiControllerNumber, Steinberg::Vst::ParamID &id) override
Steinberg::int32 PLUGIN_API getProgramListCount() override
void forget_contract_view(ContractPlugView *view)
Called by a ContractPlugView (vst3_contract_view.hpp) as it is destroyed, so the adapter stops tracki...
void set_factory_presets_directory(std::filesystem::path dir)
Point factory-preset discovery at dir instead of the loaded bundle's Contents/Resources/Presets.
Definition vst3_adapter.hpp:313
Steinberg::tresult PLUGIN_API getKeyswitchInfo(Steinberg::int32 busIndex, Steinberg::int16 channel, Steinberg::int32 keySwitchIndex, Steinberg::Vst::KeyswitchInfo &info) override
Steinberg::Vst::UnitID PLUGIN_API getSelectedUnit() override
Steinberg::tresult PLUGIN_API getProgramListInfo(Steinberg::int32, Steinberg::Vst::ProgramListInfo &) override
Steinberg::tresult PLUGIN_API getParamStringByValue(Steinberg::Vst::ParamID tag, Steinberg::Vst::ParamValue valueNormalized, Steinberg::Vst::String128 string) override
Steinberg::Vst::ParamValue PLUGIN_API plainParamToNormalized(Steinberg::Vst::ParamID id, Steinberg::Vst::ParamValue plainValue) override
static Steinberg::FUnknown * createInstance(void *)
Steinberg::tresult PLUGIN_API getProgramName(Steinberg::Vst::ProgramListID, Steinberg::int32, Steinberg::Vst::String128) override
Steinberg::tresult PLUGIN_API getUnitByBus(Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection, Steinberg::int32, Steinberg::int32, Steinberg::Vst::UnitID &) override
Steinberg::int32 PLUGIN_API getNoteExpressionCount(Steinberg::int32 busIndex, Steinberg::int16 channel) override
Steinberg::tresult PLUGIN_API getNoteExpressionInfo(Steinberg::int32 busIndex, Steinberg::int16 channel, Steinberg::int32 noteExpressionIndex, Steinberg::Vst::NoteExpressionTypeInfo &info) override
PulpVst3Processor(ProcessorFactory factory)
bool restart_dispatch_armed_for_test() const
Definition vst3_adapter.hpp:758
ParamEventsOptOutMonitor & param_events_monitor()
Definition vst3_adapter.hpp:277
Steinberg::uint32 PLUGIN_API getLatencySamples() override
Steinberg::tresult PLUGIN_API process(Steinberg::Vst::ProcessData &data) override
Steinberg::uint32 PLUGIN_API release() override
Definition vst3_adapter.hpp:153
Steinberg::uint32 PLUGIN_API getTailSamples() override
state::ParamID bypass_parameter_id() const
Returns the StateStore ParamID the adapter routes VST3's kIsBypass parameter to.
Definition vst3_adapter.hpp:285
Steinberg::tresult PLUGIN_API setupProcessing(Steinberg::Vst::ProcessSetup &setup) override
std::uint32_t note_expression_drop_count() const
Saturating count of per-note expression routings the adapter could not deliver: a note-on whose noteI...
Definition vst3_adapter.hpp:294
Steinberg::tresult PLUGIN_API setUnitProgramData(Steinberg::int32, Steinberg::int32, Steinberg::IBStream *) override
Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream *state) override
Steinberg::tresult PLUGIN_API selectUnit(Steinberg::Vst::UnitID) override
Steinberg::tresult PLUGIN_API getParamValueByString(Steinberg::Vst::ParamID tag, Steinberg::Vst::TChar *string, Steinberg::Vst::ParamValue &valueNormalized) override
Steinberg::tresult PLUGIN_API canProcessSampleSize(Steinberg::int32 symbolicSampleSize) override
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Shared, fail-closed lifetime signal for callbacks that may outlive an owner.
Definition alive_token.hpp:19
std::shared_ptr< State > Handle
Definition alive_token.hpp:53
RAII attachment.
Definition trace_session.hpp:276
Definition parameter_event_queue.hpp:82
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
A plug-in's factory presets as an index-addressable, preloaded program table.
Per-host accommodation flags consumed by format adapters.
Sample-accurate MPE expression event stream.
MPE (MIDI Polyphonic Expression) voice tracker.
LatencyCompensatedBypassT< kBoundaryMaxChannels > LatencyCompensatedBypass
Definition adapter_boundary.hpp:294
Definition vst3_adapter.hpp:61
constexpr Steinberg::uint32 kProcessContextRequirements
Definition vst3_adapter.hpp:108
Steinberg::IPlugView * make_plug_view(Processor &processor, state::StateStore &store, runtime::AliveToken::Handle owner_alive)
What the product grants a device: the quota table indexed by capability tier and live thermal state.
Definition aax_editor.hpp:31
std::unique_ptr< Processor >(*)() ProcessorFactory
Factory function type — plugins provide this to create processor instances.
Definition processor.hpp:1416
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
Proving the audio thread is outside process() while plugin state is restored under it.
One host action and its arguments.
Definition editor_host.hpp:520