Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
plugin_slot.hpp
Go to the documentation of this file.
1#pragma once
2
3// Plugin Slot for Pulp Host
4// Wraps a loaded plugin instance with a uniform interface for audio processing.
5// Handles plugin lifecycle: load → prepare → process → release.
6//
7// Usage:
8// auto slot = PluginSlot::load(info);
9// slot->prepare(48000, 512);
10// slot->process(output, input, midi_in, midi_out);
11// slot->release();
12
13#include <pulp/host/scanner.hpp>
19#include <pulp/audio/buffer.hpp>
20#include <pulp/midi/buffer.hpp>
21#include <memory>
22#include <string>
23#include <vector>
24#include <functional>
25#include <cstdint>
26
27namespace pulp::host {
28
29// ── Plugin parameter info ───────────────────────────────────────────────
30
31// Parameter behavior flags. Loaders fill these from their format-native
32// parameter descriptors; consumers (automation routing, UI, automation gates)
33// honor them. All defaults are conservative — a loader that doesn't know the
34// flag value should leave it at its default.
35struct ParamFlags {
36 bool automatable = true; // Host may drive this via connect_automation.
37 bool read_only = false; // Plugin reports it; host must not write.
38 bool hidden = false; // Don't show in default UIs (developer/internal).
39 bool stepped = false; // Discrete int-valued (max_value - min_value + 1 steps).
40 bool is_bypass = false; // This parameter IS the plugin's bypass control.
41 // Only a format whose bypass lives in the
42 // parameter list can set it — see BypassSurface
43 // for the formats that carry bypass elsewhere.
44 bool rampable = true; // Plugin handles per-block linear interpolation.
45 bool modulatable = true; // Plugin accepts per-voice modulation events
46 // (CLAP MOD, etc.) — distinct from automation.
47};
48
49// Where a loaded plugin keeps its own bypass control.
50//
51// `ParamFlags::is_bypass` can only describe a bypass that IS a parameter.
52// VST3 (`kIsBypass`) and CLAP (`CLAP_PARAM_IS_BYPASS`) both are, so for those
53// formats the flagged `parameters()` entry is the whole story. AU keeps bypass
54// off the parameter list entirely, as the unit property
55// `kAudioUnitProperty_BypassEffect`; `AudioUnitParameterOptions` carries no
56// bypass bit at all, so every AU parameter reports `is_bypass == false` and the
57// flag alone cannot answer "does this plugin have a bypass". This enum answers
58// that for every format, so a caller never has to guess from parameter names.
59//
60// Guessing is not a theoretical hazard: Apple's stock AUNBandEQ publishes eight
61// parameters named exactly "Bypass", boolean over [0, 1] — per-band controls,
62// none of them the unit bypass — so a name/range heuristic reports eight bypass
63// parameters for a plugin that has none.
64enum class BypassSurface {
65 none, // No bypass control is exposed. PluginSlot::set_bypass() is
66 // emulated entirely host-side.
67 parameter, // A parameter carries bypass: the `parameters()` entry whose
68 // `ParamFlags::is_bypass` is set.
69 unit_property, // A plugin-wide control outside the parameter list carries
70 // bypass (AU `kAudioUnitProperty_BypassEffect`). No parameter
71 // reports `is_bypass`, and none stands in for it.
72};
73
75 uint32_t id = 0;
76 std::string name;
77 std::string unit;
78 // Plain (not normalized) parameter range. PluginSlot::get_parameter() and
79 // set_parameter() operate in this domain; loaders that natively normalize
80 // (VST3) do the conversion internally.
81 float min_value = 0.0f;
82 float max_value = 1.0f;
83 float default_value = 0.0f;
86};
87
89
90struct PluginLoadResult;
91
92// ── Plugin Slot ─────────────────────────────────────────────────────────
93
95 public:
96 // Result of the most recent audio block. A slot reports this through an
97 // audio-thread-safe latch so the graph executor can contain a plugin that
98 // rejects a block without changing the existing void process ABI. The
99 // status is diagnostic/executor control only; each format retains its
100 // explicit output fallback policy.
101 enum class ProcessStatus : std::uint8_t { ok, failed };
102
103 virtual ~PluginSlot() = default;
104
105 // Load a plugin from a PluginInfo descriptor.
106 // Returns nullptr if the plugin can't be loaded.
107 static std::unique_ptr<PluginSlot> load(const PluginInfo& info);
108
111
112 // Plugin metadata
113 virtual const PluginInfo& info() const = 0;
114 virtual bool is_loaded() const = 0;
115
116 // Lifecycle
117 virtual bool prepare(double sample_rate, int max_block_size) = 0;
118 virtual void release() = 0;
119
120 // Audio processing.
121 //
122 // param_events carries sample-accurate parameter changes to deliver to the
123 // plugin during this block (sorted by sample_offset). Loaders translate
124 // into the plugin's native event stream. A loader that doesn't yet support
125 // per-block parameter events may ignore the queue; callers will then see
126 // the most recent value set via set_parameter().
127 virtual void process(audio::BufferView<float>& output,
129 const midi::MidiBuffer& midi_in,
130 midi::MidiBuffer& midi_out,
131 const ParameterEventQueue& param_events,
132 int num_samples) = 0;
133
134 // Parameters
135 virtual std::vector<HostParamInfo> parameters() const = 0;
136 virtual float get_parameter(uint32_t id) const = 0;
137 virtual void set_parameter(uint32_t id, float normalized_value) = 0;
138
139 // Bypass.
140 //
141 // `set_bypass()` is a HOST-side control and its output guarantee is the
142 // same for every format: while bypassed the slot passes input through
143 // unchanged, whether or not the plugin has a bypass of its own. Loaders
144 // whose plugin exposes a native bypass also mirror the state onto it, so a
145 // hosted plugin's own bypass state (and its editor) tracks the host's.
146 // Control thread, not the audio thread: mirroring reaches the plugin
147 // through its non-realtime property/parameter API.
148 virtual void set_bypass(bool bypassed) = 0;
149 virtual bool is_bypassed() const = 0;
150
151 // Where this plugin keeps its own bypass control is reported by
152 // bypass_surface(), declared at the end of the class.
153
154 // State (preset save/load)
155 virtual std::vector<uint8_t> save_state() const = 0;
156 virtual bool restore_state(const std::vector<uint8_t>& data) = 0;
157
158 // Editor handle API.
159 //
160 // `create_hosted_editor()` is the typed surface for embedding native
161 // plugin editors. `pulp::view::WindowHost::attach_native_child_view` is
162 // the host-side bridge and `pulp::view::EditorAttachment` wires the two
163 // together. Slots may still override these void* entry points when they
164 // have not moved to the typed surface; the default `create_hosted_editor()`
165 // fallback keeps those subclasses working.
166 virtual bool has_editor() const = 0;
167 [[deprecated("Override create_hosted_editor() instead — see pulp::view::EditorAttachment "
168 "for typed hosted editor attachment.")]]
169 virtual void* create_editor_view() = 0; // Returns platform-native view handle
170 [[deprecated("Override destroy_hosted_editor() instead — see pulp::view::EditorAttachment "
171 "for typed hosted editor attachment.")]]
172 virtual void destroy_editor_view() = 0;
173
174 // Hosted editor.
175 //
176 // Typed replacement for the void* editor API. Slot implementations
177 // fill in a platform-native parent-window handle, initial size, and
178 // resizable-ness so the host can lay the view out without guessing.
179 //
180 // Lifecycle:
181 // auto ed = slot->create_hosted_editor(parent_window);
182 // if (ed) { /* host attaches ed->native_handle into its window */ }
183 // // ... on close ...
184 // slot->destroy_hosted_editor(std::move(ed));
185 //
186 // Format-specific editor surfaces:
187 // CLAP — clap_plugin_gui: create / set_parent / show / get_size
188 // VST3 — IEditController::createView("editor") + IPlugView::attached
189 // AU — kAudioUnitProperty_CocoaUI on the v2 AudioUnit handle. Both
190 // AudioUnit and AudioUnitV3 load through the v2 C API
191 // (AudioComponentInstanceNew), so there is no AUAudioUnit object
192 // here and no requestViewControllerWithCompletionHandler.
193 // LV2 — ui: extension + shared-object UI
194 //
195 // CLAP and VST3 CONSUME a parent view rather than returning one, so those
196 // slots hand back a host-owned container as `native_handle`. See
197 // hosted_editor_container.hpp.
199 void* native_handle = nullptr;
200 uint32_t width = 0;
201 uint32_t height = 0;
202 bool resizable = false;
203 };
204
208 virtual std::unique_ptr<HostedEditor>
209 create_hosted_editor(void* /*parent_window*/) {
210 if (!has_editor()) return nullptr;
211#if defined(__GNUC__) || defined(__clang__)
212# pragma GCC diagnostic push
213# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
214#endif
215 void* h = create_editor_view();
216#if defined(__GNUC__) || defined(__clang__)
217# pragma GCC diagnostic pop
218#endif
219 if (!h) return nullptr;
220 auto ed = std::make_unique<HostedEditor>();
221 ed->native_handle = h;
222 return ed;
223 }
224
226 virtual void destroy_hosted_editor(std::unique_ptr<HostedEditor> ed) {
227#if defined(__GNUC__) || defined(__clang__)
228# pragma GCC diagnostic push
229# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
230#endif
231 if (ed) destroy_editor_view();
232#if defined(__GNUC__) || defined(__clang__)
233# pragma GCC diagnostic pop
234#endif
235 }
236
237 // Latency
238 virtual int latency_samples() const = 0;
239 virtual int tail_samples() const = 0;
240
249
250 // Native-handle visitor.
251 //
252 // Typed plugin introspection: subclass NativeHandleVisitor, override
253 // the visit_* methods you care about, then call
254 // slot.accept(visitor)
255 // The slot dispatches to the matching visit_* overload (visit_clap,
256 // visit_vst3, ...) with a populated handle struct. The default
257 // implementation here calls visit_unknown so placeholder/unresolved
258 // slots and slots whose format adapter was not compiled in degrade
259 // gracefully without leaking a stale handle.
260 virtual void accept(NativeHandleVisitor& visitor) const {
262 }
263
264 // Additive host-side multi-bus processing entry point. Keep appended to
265 // preserve the existing PluginSlot virtual ordering.
266 //
267 // The default projection preserves the original PluginSlot contract by
268 // selecting the active main output and optional main input from
269 // ProcessBuffers, then calling the original main-in/main-out process()
270 // callback. Slot implementations that need direct access to sidechain,
271 // aux, surround, or multi-output host buses can override this method.
273 const midi::MidiBuffer& midi_in,
274 midi::MidiBuffer& midi_out,
275 const ParameterEventQueue& param_events,
276 int num_samples) {
277 auto* output = audio.main_output();
278 audio::BufferView<float> empty_output;
280 auto* input = audio.main_input();
281 process(output ? *output : empty_output, input ? *input : empty_input, midi_in, midi_out,
282 param_events, num_samples);
283 }
284
285 // ── Opt-in host transport (additive) ────────────────────────────────────
286 //
287 // A slot whose output depends on the host timeline (playhead, tempo, loop,
288 // process-mode) opts in by overriding wants_transport() to return true and
289 // overriding the transport-carrying process() overload below to read the
290 // supplied ProcessContext. A slot that ignores transport leaves both at
291 // their defaults and is byte-for-byte unchanged from before this seam.
292 //
293 // INVARIANT (prepare-stable): wants_transport() is resolved ONCE when the
294 // owning graph compiles its snapshot (cached into the routed binding and the
295 // anticipation-eligibility analysis from the SAME read), never re-polled per
296 // block on the audio thread. A slot must therefore settle its transport
297 // capability before prepare(); a slot that changes the value later requires
298 // a re-prepare for the graph to observe the new value. The cached bit is
299 // also the precondition that lets a transport-sensitive slot coexist with
300 // anticipative rendering: such a node is excluded from the ahead-rendered
301 // interior so it always runs live and can safely observe transport.
302 virtual bool wants_transport() const { return false; }
303
304 // Transport-carrying multi-bus process entry point. Appended last to
305 // preserve the existing PluginSlot virtual ordering. The default forwards to
306 // the transport-less overload above, so a slot that does not override it is
307 // unaffected. Only invoked by the host when wants_transport() returned true
308 // and a transport is available for the block; otherwise the host calls the
309 // transport-less overload.
311 const midi::MidiBuffer& midi_in,
312 midi::MidiBuffer& midi_out,
313 const ParameterEventQueue& param_events,
314 int num_samples,
315 const format::ProcessContext& /*transport*/) {
316 process(audio, midi_in, midi_out, param_events, num_samples);
317 }
318
332
333 // Editor resize negotiation. Both appended last to preserve the existing
334 // PluginSlot virtual ordering; the defaults make every slot that does not
335 // override them behave exactly as before.
336 //
337 // Main thread only, and only meaningful between create_hosted_editor() and
338 // destroy_hosted_editor().
339
343 using EditorResizeRequestHandler = std::function<bool(uint32_t width, uint32_t height)>;
344
349
354 virtual bool set_hosted_editor_size(uint32_t& /*width*/, uint32_t& /*height*/) {
355 return false;
356 }
357
362
363 // Optional: the channel widths the caller will actually render, supplied
364 // before prepare(). Keep appended to preserve the existing PluginSlot
365 // virtual ordering.
366 //
367 // A format that negotiates its stream shape at initialize time (AU) cannot
368 // infer the width from buffers handed to it later. Without this the AU is
369 // initialized at its descriptor/default width and a render at any other
370 // width is rejected as a malformed shape: output zeroed,
371 // kAudioUnitRenderAction_OutputIsSilence set, status noErr -- a SILENT
372 // render reported as success. Slots that take their width from the
373 // per-block buffers (VST3, CLAP, LV2) need nothing and do not override it.
374 virtual void set_preferred_channel_layout(int /*inputs*/, int /*outputs*/) {}
375
381 virtual std::string retained_history_key() const {
382 return {};
383 }
384 virtual std::vector<uint8_t> serialize_dsp_state() const {
385 return {};
386 }
387 virtual bool restore_dsp_state(const std::vector<uint8_t>&) {
388 return false;
389 }
390 virtual bool reseed_dsp_state(std::uint64_t) {
391 return false;
392 }
393
394 // Appended after the original PluginSlot virtual surface to preserve ABI
395 // ordering for existing slot implementations. The graph executor calls
396 // begin_process() before a routed block; adapters also clear their latch at
397 // process() entry so direct slot callers observe the same per-block result.
398 virtual void begin_process() noexcept {}
399
400 // Status for the most recent process block. Implementations must provide an
401 // allocation-free, nonblocking read suitable for the audio thread. The
402 // status does not replace a format's output fallback policy.
403 virtual ProcessStatus process_status() const noexcept {
404 return ProcessStatus::ok;
405 }
406
408 virtual std::optional<PluginResolvedIdentity> resolved_identity() const {
409 return std::nullopt;
410 }
411};
412
415 std::variant<std::unique_ptr<PluginSlot>, PluginIdentityError> value;
416};
417
418} // namespace pulp::host
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Definition native_handle_visitor.hpp:116
virtual void visit_unknown(const PluginSlot &, NativeHandleFormat)
Called when the slot represents a format the visitor did not override, or when the slot itself does n...
Definition native_handle_visitor.hpp:123
Definition plugin_slot.hpp:94
virtual bool is_loaded() const =0
virtual BypassSurface bypass_surface() const
Where this plugin keeps its own bypass control.
Definition plugin_slot.hpp:361
virtual void release()=0
virtual void begin_process() noexcept
Definition plugin_slot.hpp:398
ProcessStatus
Definition plugin_slot.hpp:101
@ failed
Definition plugin_slot.hpp:101
@ ok
Definition plugin_slot.hpp:101
virtual bool set_hosted_editor_size(uint32_t &, uint32_t &)
Host-initiated resize: ask the plugin to adopt (width, height).
Definition plugin_slot.hpp:354
virtual void destroy_hosted_editor(std::unique_ptr< HostedEditor > ed)
Tear down a hosted editor. Default routes to destroy_editor_view().
Definition plugin_slot.hpp:226
virtual void process(format::ProcessBuffers &audio, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples)
Definition plugin_slot.hpp:272
virtual std::vector< uint8_t > save_state() const =0
virtual bool wants_transport() const
Definition plugin_slot.hpp:302
static std::unique_ptr< PluginSlot > load(const PluginInfo &info)
virtual void process(audio::BufferView< float > &output, const audio::BufferView< const float > &input, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples)=0
virtual int tail_samples() const =0
virtual float get_parameter(uint32_t id) const =0
std::function< bool(uint32_t width, uint32_t height)> EditorResizeRequestHandler
Called when the plugin asks the host to resize its editor.
Definition plugin_slot.hpp:343
virtual void * create_editor_view()=0
virtual void set_bypass(bool bypassed)=0
static PluginLoadResult load_exact(const PluginSelection &selection)
Explicit strict CLAP selection. Legacy load() keeps best-effort behavior.
virtual int latency_samples() const =0
virtual bool restore_state(const std::vector< uint8_t > &data)=0
virtual std::optional< PluginResolvedIdentity > resolved_identity() const
Detached strict-load receipt; unavailable for legacy or unsupported loaders.
Definition plugin_slot.hpp:408
virtual void set_preferred_channel_layout(int, int)
Definition plugin_slot.hpp:374
virtual void set_editor_resize_request_handler(EditorResizeRequestHandler)
Install the handler invoked on a plugin-initiated resize request.
Definition plugin_slot.hpp:348
virtual void destroy_editor_view()=0
virtual void accept(NativeHandleVisitor &visitor) const
Definition plugin_slot.hpp:260
virtual std::string retained_history_key() const
Optional realtime DSP-history carry for a prepared replacement.
Definition plugin_slot.hpp:381
virtual std::vector< uint8_t > serialize_dsp_state() const
Definition plugin_slot.hpp:384
virtual const PluginInfo & info() const =0
virtual std::unique_ptr< HostedEditor > create_hosted_editor(void *)
Create the hosted editor.
Definition plugin_slot.hpp:209
virtual bool prepare(double sample_rate, int max_block_size)=0
virtual void set_parameter(uint32_t id, float normalized_value)=0
virtual LatencyQuery latency_query() const
Whether latency_samples() is a fact from the plugin, or a placeholder.
Definition plugin_slot.hpp:331
LatencyQuery
Whether latency_samples() is a fact from the plugin, or a placeholder.
Definition plugin_slot.hpp:244
@ Available
The value is the plugin's own report. Zero means zero.
Definition plugin_slot.hpp:245
@ Unsupported
This backend cannot ask. The value is meaningless.
Definition plugin_slot.hpp:246
@ QueryFailed
The backend asked and the plugin errored.
Definition plugin_slot.hpp:247
virtual bool is_bypassed() const =0
virtual void process(format::ProcessBuffers &audio, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples, const format::ProcessContext &)
Definition plugin_slot.hpp:310
virtual bool has_editor() const =0
virtual ProcessStatus process_status() const noexcept
Definition plugin_slot.hpp:403
virtual bool restore_dsp_state(const std::vector< uint8_t > &)
Definition plugin_slot.hpp:387
virtual std::vector< HostParamInfo > parameters() const =0
virtual ~PluginSlot()=default
virtual bool reseed_dsp_state(std::uint64_t)
Definition plugin_slot.hpp:390
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Definition analyzer_provider.hpp:12
ProcessBuffersT< float > ProcessBuffers
Definition process_block.hpp:422
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
@ Unknown
Definition native_handle_visitor.hpp:47
@ none
Definition graph_serializer.hpp:49
::pulp::state::ParameterEventQueue ParameterEventQueue
Definition parameter_event_queue.hpp:8
BypassSurface
Definition plugin_slot.hpp:64
@ parameter
Definition plugin_slot.hpp:67
@ none
Definition plugin_slot.hpp:65
@ unit_property
Definition plugin_slot.hpp:69
state::ParamRate ParamRate
Definition plugin_slot.hpp:88
PluginIdentityError
Definition plugin_identity.hpp:21
ParamRate
Parameter update-rate classification.
Definition parameter.hpp:62
@ ControlRate
Definition parameter.hpp:63
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Definition plugin_slot.hpp:74
float default_value
Definition plugin_slot.hpp:83
std::string unit
Definition plugin_slot.hpp:77
std::string name
Definition plugin_slot.hpp:76
float max_value
Definition plugin_slot.hpp:82
ParamFlags flags
Definition plugin_slot.hpp:84
state::ParamRate rate
Definition plugin_slot.hpp:85
float min_value
Definition plugin_slot.hpp:81
Definition plugin_slot.hpp:35
bool modulatable
Definition plugin_slot.hpp:45
bool stepped
Definition plugin_slot.hpp:39
bool read_only
Definition plugin_slot.hpp:37
bool hidden
Definition plugin_slot.hpp:38
bool rampable
Definition plugin_slot.hpp:44
bool automatable
Definition plugin_slot.hpp:36
bool is_bypass
Definition plugin_slot.hpp:40
Definition scanner.hpp:35
Exactly one loaded slot or typed refusal. A successful slot owns its receipt.
Definition plugin_slot.hpp:414
std::variant< std::unique_ptr< PluginSlot >, PluginIdentityError > value
Definition plugin_slot.hpp:415
Definition plugin_identity.hpp:119
Definition plugin_slot.hpp:198
void * native_handle
NSView*, HWND, GdkWindow*, clap_plugin_gui* etc.
Definition plugin_slot.hpp:199
bool resizable
Definition plugin_slot.hpp:202
uint32_t height
Definition plugin_slot.hpp:201
uint32_t width
Definition plugin_slot.hpp:200