Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
clap_entry.hpp
Go to the documentation of this file.
1#pragma once
2
3// Generic CLAP entry point generator
4// Plugin developers include this and call PULP_CLAP_PLUGIN() with their factory function.
5// All CLAP boilerplate (factory, extensions, entry point) is generated automatically.
6//
7// Usage (in one .cpp file per plugin):
8// #include "my_processor.hpp"
9// #include <pulp/format/clap_entry.hpp>
10// PULP_CLAP_PLUGIN(my_namespace::create_my_processor)
11
13#include <pulp/format/detail/editor_environment.hpp>
14#include <pulp/format/detail/locale_independent_float.hpp>
20// A WebCLAP module has no native window to embed an editor in, and its
21// curated source list (PulpWclap.cmake) carries no editor-host contract.
22#if !(defined(PULP_WCLAP) && PULP_WCLAP)
23#define PULP_CLAP_CONTRACT_GUI 1
25#endif
26#if defined(PULP_CLAP_GUI) && PULP_CLAP_GUI
30#endif
31#include <pulp/runtime/log.hpp>
33#include <clap/clap.h>
34#include <clap/ext/audio-ports-config.h>
35#include <cctype>
36#include <charconv>
37#include <cmath>
38#include <cstring>
39#include <cstdio>
40#include <chrono>
41#include <string>
42#include <string_view>
43
44// Internal implementation — do not call directly
46
47// Per-plugin record. One CLAP binary can host many plugins (a bundle); the
48// single-plugin macro registers exactly one. Only the factory is stored at
49// static init — the descriptor (which requires CALLING the factory to build a
50// Processor) is filled lazily in init_record(), invoked from entry_init(), the
51// host's module-load callback. Constructing a Processor during C++ static
52// initialization is unsafe (std::string/global order), so this deferral is
53// load-bearing, not cosmetic.
57 clap_plugin_descriptor_t clap_desc{};
58 const char* features[4] = {};
59 bool initialized = false;
60 // Bundle records publish themselves to the shared keyed registry (for
61 // cross-format enumeration + per-plugin editor-asset lookup). The
62 // legacy single-plugin PULP_CLAP_PLUGIN path leaves the keyed table
63 // empty — matching AU/VST3, where only the bundle macros register keyed
64 // entries — so single-plugin binaries keep their documented contract.
65 bool publish_keyed = false;
66};
67
68// Fixed-capacity storage — no heap, static-init safe. A combined bundle
69// registers one record per plugin; 64 is far above any real CLAP suite.
70inline constexpr uint32_t kMaxClapPlugins = 64;
72inline uint32_t g_clap_record_count = 0;
73
74// Deduplicated view over the initialized records — the addressable plugin set
75// the CLAP factory advertises. Built by init_all_records(); a record whose id
76// collides with an earlier one (developer error in a bundle) is excluded so the
77// host never sees an advertised-but-unreachable plugin (create_plugin resolves
78// an id to the FIRST matching record, so only that one is addressable).
80inline uint32_t g_clap_index_count = 0;
81
82// Static-init-safe: stores the factory only, never calls it. `publish_keyed`
83// records also register into the shared keyed registry at init time.
84inline void register_clap_record(ProcessorFactory factory, bool publish_keyed) {
87 rec.factory = factory;
88 rec.publish_keyed = publish_keyed;
89}
90
91// Deferred descriptor build (NOT at static init). Idempotent.
92inline void init_record(ClapPluginRecord& rec) {
93 if (rec.initialized || !rec.factory) return;
94 auto proc = rec.factory();
95 if (!proc) return;
96 rec.desc = proc->descriptor();
97
98 switch (rec.desc.category) {
100 rec.features[0] = CLAP_PLUGIN_FEATURE_AUDIO_EFFECT;
101 break;
103 rec.features[0] = CLAP_PLUGIN_FEATURE_INSTRUMENT;
104 break;
106 rec.features[0] = CLAP_PLUGIN_FEATURE_NOTE_EFFECT;
107 break;
108 }
109 rec.features[1] = nullptr;
110
111 // c_str() views into rec.desc, which lives in the stable g_clap_records
112 // array for the process lifetime — so these pointers stay valid.
113 rec.clap_desc = {
114 .clap_version = CLAP_VERSION,
115 .id = rec.desc.bundle_id.c_str(),
116 .name = rec.desc.name.c_str(),
117 .vendor = rec.desc.manufacturer.c_str(),
118 .url = "",
119 .manual_url = "",
120 .support_url = "",
121 .version = rec.desc.version.c_str(),
122 .description = "",
123 .features = rec.features,
124 };
125
126 rec.initialized = true;
127}
128
129inline void init_all_records() {
131 for (uint32_t i = 0; i < g_clap_record_count; ++i) {
132 auto& rec = g_clap_records[i];
133 init_record(rec);
134 if (!rec.initialized) continue; // factory returned null
135
136 // Exclude a duplicate id: the host must not be handed an advertised
137 // descriptor it cannot instantiate (create_plugin resolves to the
138 // first record with that id). First writer wins; later dupes drop.
139 bool dupe = false;
140 for (uint32_t j = 0; j < g_clap_index_count; ++j) {
141 if (std::strcmp(g_clap_index[j]->clap_desc.id, rec.clap_desc.id) == 0) {
142 dupe = true;
143 break;
144 }
145 }
146 if (dupe) continue;
148
149 // Publish to the shared keyed registry only AFTER dedup, so a duplicate
150 // id never lands an unreachable second entry in registered_plugins().
151 // Bundle records only — the legacy single-plugin path leaves it empty
152 // (see publish_keyed). Guard on find_plugin so publication is idempotent:
153 // a matched re-init won't double-publish, and in a combined AU+VST3+CLAP
154 // bundle the one logical plugin (shared bundle_id across formats) keeps a
155 // single keyed entry for editor-asset lookup. AU codes are 0 for CLAP.
156 if (rec.publish_keyed && find_plugin(rec.clap_desc.id) == nullptr) {
158 .id = rec.clap_desc.id,
159 .factory = rec.factory,
160 });
161 }
162 }
163}
164
165// Test-only: clear all records back to empty. Registration is meant to happen
166// once at static init and never be torn down; this exists so a test can swap the
167// binary's plugin set deterministically. NOT for production use.
169 for (uint32_t i = 0; i < g_clap_record_count; ++i) {
171 }
173 for (uint32_t i = 0; i < g_clap_index_count; ++i) g_clap_index[i] = nullptr;
175}
176
177// Find a record by CLAP id (bundle_id). Null if unknown or not yet initialized.
178inline ClapPluginRecord* find_clap_record(const char* id) {
179 if (!id) return nullptr;
180 for (uint32_t i = 0; i < g_clap_record_count; ++i) {
181 if (g_clap_records[i].initialized &&
182 strcmp(g_clap_records[i].clap_desc.id, id) == 0) {
183 return &g_clap_records[i];
184 }
185 }
186 return nullptr;
187}
188
189// ── Audio ports extension (multi-bus) ──────────────────────────────────
190inline uint32_t audio_ports_count(const clap_plugin_t* plugin, bool is_input) {
191 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
192 auto desc = self->processor ? self->processor->descriptor()
193 : self->descriptor_snapshot;
194 return static_cast<uint32_t>(is_input ? desc.input_buses.size() : desc.output_buses.size());
195}
196
197inline bool audio_ports_get(const clap_plugin_t* plugin, uint32_t index, bool is_input,
198 clap_audio_port_info_t* info) {
199 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
200 auto desc = self->processor ? self->processor->descriptor()
201 : self->descriptor_snapshot;
202 auto& buses = is_input ? desc.input_buses : desc.output_buses;
203
204 if (index >= buses.size()) return false;
205 auto& bus = buses[index];
206
207 int channel_count = bus.default_channels;
208 if (!desc.supported_bus_layouts.empty()) {
209 const auto selected = std::min<std::size_t>(
210 self->selected_bus_layout, desc.supported_bus_layouts.size() - 1);
211 const auto& layout = desc.supported_bus_layouts[selected];
212 const auto& widths = is_input ? layout.inputs : layout.outputs;
213 if (index < widths.size()) channel_count = widths[index];
214 }
215
216 info->id = static_cast<clap_id>((is_input ? 0 : 100) + index);
217 runtime::copy_c_string(info->name, bus.name);
218 info->channel_count = static_cast<uint32_t>(std::max(0, channel_count));
219 // Every Pulp port accepts 64-bit buffers: clap_process() converts f64 at
220 // the adapter boundary for f32-internal processors and dispatches
221 // process_f64() natively when the descriptor opts in — the same posture as
222 // the VST3 adapter's unconditional canProcessSampleSize(kSample64). A host
223 // only offers data64 to ports carrying SUPPORTS_64BITS, so without this
224 // flag the whole f64 path is unreachable from spec-compliant hosts.
225 // PREFERS_64BITS is advertised only when the processor is natively
226 // double-precision (supports_f64_audio) — for boundary-converted plugins a
227 // 64-bit stream buys nothing, so the host shouldn't be nudged toward it.
228 // REQUIRES_COMMON_SAMPLE_SIZE is deliberately NOT set: clap_process()
229 // handles mixed 32/64 buses by demoting the block to the f32 path.
230 info->flags = (index == 0) ? CLAP_AUDIO_PORT_IS_MAIN : 0;
231 info->flags |= CLAP_AUDIO_PORT_SUPPORTS_64BITS;
232 if (desc.effective_capabilities().supports_f64_audio) {
233 info->flags |= CLAP_AUDIO_PORT_PREFERS_64BITS;
234 }
235 info->port_type = channel_count == 1 ? CLAP_PORT_MONO
236 : channel_count == 2 ? CLAP_PORT_STEREO : nullptr;
237 info->in_place_pair = CLAP_INVALID_ID;
238 return true;
239}
240
241inline const clap_plugin_audio_ports_t audio_ports_ext = {
242 .count = audio_ports_count, .get = audio_ports_get,
243};
244
245inline uint32_t audio_ports_config_count(const clap_plugin_t* plugin) {
246 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
247 if (!self || !self->processor) return 0;
248 return static_cast<uint32_t>(
249 self->processor->descriptor().supported_bus_layouts.size());
250}
251
252inline bool audio_ports_config_get(const clap_plugin_t* plugin, uint32_t index,
253 clap_audio_ports_config_t* config) {
254 if (!config) return false;
255 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
256 if (!self || !self->processor) return false;
257 const auto desc = self->processor->descriptor();
258 if (index >= desc.supported_bus_layouts.size()) return false;
259 const auto& layout = desc.supported_bus_layouts[index];
260 std::memset(config, 0, sizeof(*config));
261 config->id = index;
262 runtime::copy_c_string(config->name,
263 layout.name.empty() ? std::string("Layout ") + std::to_string(index + 1)
264 : layout.name);
265 config->input_port_count = static_cast<uint32_t>(layout.inputs.size());
266 config->output_port_count = static_cast<uint32_t>(layout.outputs.size());
267 config->has_main_input = !layout.inputs.empty() && layout.inputs.front() > 0;
268 config->main_input_channel_count = config->has_main_input
269 ? static_cast<uint32_t>(layout.inputs.front()) : 0;
270 config->main_input_port_type = layout.inputs.empty() ? nullptr
271 : layout.inputs.front() == 1 ? CLAP_PORT_MONO
272 : layout.inputs.front() == 2 ? CLAP_PORT_STEREO : nullptr;
273 config->has_main_output = !layout.outputs.empty() && layout.outputs.front() > 0;
274 config->main_output_channel_count = config->has_main_output
275 ? static_cast<uint32_t>(layout.outputs.front()) : 0;
276 config->main_output_port_type = layout.outputs.empty() ? nullptr
277 : layout.outputs.front() == 1 ? CLAP_PORT_MONO
278 : layout.outputs.front() == 2 ? CLAP_PORT_STEREO : nullptr;
279 return true;
280}
281
282inline bool audio_ports_config_select(const clap_plugin_t* plugin, clap_id config_id) {
283 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
284 if (!self || !self->processor) return false;
285 const auto desc = self->processor->descriptor();
286 if (config_id >= desc.supported_bus_layouts.size()) return false;
287 const auto& layout = desc.supported_bus_layouts[config_id];
288 if (!self->processor->is_bus_layout_supported(
289 {layout.inputs, layout.outputs})) return false;
290 self->selected_bus_layout = config_id;
291 return true;
292}
293
294inline const clap_plugin_audio_ports_config_t audio_ports_config_ext = {
298};
299
300// ── State extension ────────────────────────────────────────────────────
301inline bool state_save(const clap_plugin_t* plugin, const clap_ostream_t* stream) {
302 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
303 if (!self || !self->processor) return false;
304 const auto data = plugin_state_io::serialize(self->store, *self->processor);
305 // CLAP's stream->write() is spec'd to return a short-write count on
306 // success — callers MUST loop. Hosts (and clap-validator's
307 // `state-reproducibility-flush` with a 23-byte write cap) exercise
308 // this path. A single write() returning less than data.size() is
309 // NOT an error; only negative or zero returns are.
310 std::size_t written = 0;
311 while (written < data.size()) {
312 const auto n = stream->write(stream,
313 data.data() + written,
314 data.size() - written);
315 if (n <= 0) return false;
316 written += static_cast<std::size_t>(n);
317 }
318 return true;
319}
320
321inline bool state_load(const clap_plugin_t* plugin, const clap_istream_t* stream) {
322 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
323 if (!self || !self->processor) return false;
324 std::vector<uint8_t> data;
325 uint8_t buf[4096];
326 while (true) {
327 auto read = stream->read(stream, buf, sizeof(buf));
328 if (read <= 0) break;
329 data.insert(data.end(), buf, buf + read);
330 }
331 // Hosts may call state.load while the plug-in is active and rendering.
332 // Acquiring the gate proves no audio callback is inside clap_process(),
333 // which is the condition Processor::deserialize_plugin_state() is
334 // documented to run under. Released before on_non_realtime_tick() so a
335 // processor that reconciles derived state there cannot stall the audio
336 // thread for the length of that work.
337 bool ok = false;
338 {
339 auto restore_lock = self->state_restore_gate.lock_for_restore();
340 ok = plugin_state_io::deserialize(data, self->store, *self->processor);
341 }
342 // state.load is a main-thread call. A restored state can name a different
343 // derived source than the live one (SuperConvolver: a different impulse
344 // response), and a worker-less processor — every wasm build — has no thread
345 // to notice. Reconcile here or the audio thread renders the OLD state for
346 // the rest of the session. Default no-op for processors that don't opt in.
347 self->processor->on_non_realtime_tick();
348 // CLAP hosts cache parameter values. A successful state restore changes
349 // those values without emitting automation, so invalidate that cache.
350 if (ok && self->host && self->host_params && self->host_params->rescan) {
351 self->host_params->rescan(self->host, CLAP_PARAM_RESCAN_VALUES);
352 }
353 return ok;
354}
355
356inline const clap_plugin_state_t state_ext = { .save = state_save, .load = state_load };
357
358// ── Params extension ───────────────────────────────────────────────────
359inline uint32_t params_count(const clap_plugin_t* plugin) {
360 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
361 return static_cast<uint32_t>(self->store.param_count());
362}
363
364inline uint32_t params_flags(const state::ParamInfo& parameter) {
365 uint32_t flags = 0;
366 // Automation is opt-out: an ordinary parameter keeps the AUTOMATABLE flag
367 // it has always carried, and only a parameter declared non-automatable (or
368 // read-only, which cannot be host-written) withholds it.
369 if (state::is_automatable_param(parameter))
370 flags |= CLAP_PARAM_IS_AUTOMATABLE;
371 if (state::is_hidden_param(parameter))
372 flags |= CLAP_PARAM_IS_HIDDEN;
373 if (state::is_read_only_param(parameter))
374 flags |= CLAP_PARAM_IS_READONLY;
375 const bool is_bypass = state::is_bypass_param(parameter);
376 if (state::is_discrete_param(parameter) || is_bypass)
377 flags |= CLAP_PARAM_IS_STEPPED;
378 if (is_bypass) flags |= CLAP_PARAM_IS_BYPASS;
379 // A host only sends CLAP_EVENT_PARAM_MOD to a parameter it was told is
380 // modulatable, so the adapter's PARAM_MOD decode and
381 // StateStore::get_modulated() are unreachable without this flag.
382 if (state::is_modulatable_param(parameter))
383 flags |= CLAP_PARAM_IS_MODULATABLE;
384 return flags;
385}
386
387inline bool params_get_info(const clap_plugin_t* plugin, uint32_t index, clap_param_info_t* info) {
388 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
389 auto params = self->store.all_params();
390 if (index >= params.size()) return false;
391 auto& p = params[index];
392 memset(info, 0, sizeof(*info));
393 info->id = p.id;
394 runtime::copy_c_string(info->name, p.name);
395 const ParamGroupProjection groups(self->store.all_groups());
396 if (const auto* group = groups.find(p.group_id))
397 runtime::copy_c_string(info->module, group->path);
398 info->min_value = p.range.min;
399 info->max_value = p.range.max;
400 info->default_value = p.range.default_value;
401 info->flags = params_flags(p);
402 return true;
403}
404
405inline bool params_get_value(const clap_plugin_t* plugin, clap_id param_id, double* value) {
406 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
407 *value = self->store.get_value(static_cast<state::ParamID>(param_id));
408 return true;
409}
410
411inline bool params_value_to_text(const clap_plugin_t* plugin, clap_id param_id,
412 double value, char* display, uint32_t size) {
413 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
414 auto* info = self->store.info(static_cast<state::ParamID>(param_id));
415 if (!info) return false;
416 const auto out = format_parameter_text(*info, static_cast<float>(value));
417 if (out.empty()) return false;
418 snprintf(display, size, "%s", out.c_str());
419 return true;
420}
421
422inline bool params_text_to_value(const clap_plugin_t* plugin, clap_id param_id,
423 const char* text, double* value) {
424 if (!text) return false;
425
426 if (!plugin) return false;
427 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
428 const auto* info = self->store.info(static_cast<state::ParamID>(param_id));
429 if (!info) return false;
430 const auto parsed = parse_parameter_text(*info, text);
431 if (!parsed) return false;
432 *value = *parsed;
433 return true;
434}
435
436inline void params_flush(const clap_plugin_t* plugin, const clap_input_events_t* in,
437 const clap_output_events_t* out) {
439}
440
441inline const clap_plugin_params_t params_ext = {
442 .count = params_count, .get_info = params_get_info,
443 .get_value = params_get_value, .value_to_text = params_value_to_text,
444 .text_to_value = params_text_to_value, .flush = params_flush,
445};
446
447// ── Note ports extension (for instruments) ─────────────────────────────
448inline uint32_t note_ports_count(const clap_plugin_t* plugin, bool is_input) {
449 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
450 const auto& desc = self->processor ? self->processor->descriptor()
451 : self->descriptor_snapshot;
452 if (is_input && desc.accepts_midi) return 1;
453 if (!is_input && desc.produces_midi) return 1;
454 return 0;
455}
456
457inline bool note_ports_get(const clap_plugin_t* plugin, uint32_t index, bool is_input,
458 clap_note_port_info_t* info) {
459 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
460 const auto& desc = self->processor ? self->processor->descriptor()
461 : self->descriptor_snapshot;
462 if (index != 0) return false;
463 if (is_input && !desc.accepts_midi) return false;
464 if (!is_input && !desc.produces_midi) return false;
465
466 info->id = is_input ? 0 : 1;
467 runtime::copy_c_string(info->name, is_input ? "Note In" : "Note Out");
468 info->supported_dialects = CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI;
469 if (is_input) {
470 // Input-only: these advertise what the adapter can *decode*. The
471 // outbound path emits CLAP_EVENT_MIDI exclusively, so claiming them
472 // on the output port would promise packets Pulp never sends.
473 const auto caps = desc.effective_capabilities();
474 if (caps.supports_mpe)
475 info->supported_dialects |= CLAP_NOTE_DIALECT_MIDI_MPE;
476 if (caps.supports_ump)
477 info->supported_dialects |= CLAP_NOTE_DIALECT_MIDI2;
478 }
479 info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP;
480 return true;
481}
482
483inline const clap_plugin_note_ports_t note_ports_ext = {
484 .count = note_ports_count, .get = note_ports_get,
485};
486
487// ── Voice info extension (for voice-based instruments) ─────────────────
488inline uint32_t declared_voice_count(const clap_plugin_t* plugin) {
489 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
490 const auto& desc = self->processor ? self->processor->descriptor() : self->descriptor_snapshot;
491 return desc.voice_count;
492}
493
494inline bool voice_info_get(const clap_plugin_t* plugin, clap_voice_info_t* info) {
495 const uint32_t count = declared_voice_count(plugin);
496 if (count == 0)
497 return false;
498 info->voice_count = count;
499 info->voice_capacity = count;
500 // Not CLAP_VOICE_INFO_SUPPORTS_OVERLAPPING_NOTES: that bit promises the
501 // plugin separates overlapping notes by note_id, and the adapter lowers
502 // CLAP note events to MIDI 1.0 keyed on channel + key, discarding note_id.
503 info->flags = 0;
504 return true;
505}
506
507inline const clap_plugin_voice_info_t voice_info_ext = {.get = voice_info_get};
508
509// ── Latency extension ───────────────────────────────────────────────────
510inline uint32_t latency_get(const clap_plugin_t* plugin) {
511 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
512 if (!self->processor) return 0;
513 // CLAP reports latency as unsigned; clamp a negative
514 // latency_samples() to 0 unless the quirk is filtered out
515 // (PULP_HOST_QUIRKS=off).
516 return static_cast<uint32_t>(
517 reported_latency_samples(self->processor->latency_samples(),
518 self->host_quirks));
519}
520
521inline const clap_plugin_latency_t latency_ext = { .get = latency_get };
522
523// ── Tail extension ──────────────────────────────────────────────────────
524inline uint32_t tail_get(const clap_plugin_t* plugin) {
525 auto* self = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
526 if (!self->processor) return 0;
527 auto tail = self->processor->descriptor().tail_samples;
528 if (tail < 0) return UINT32_MAX; // infinite tail
529 return static_cast<uint32_t>(tail);
530}
531
532inline const clap_plugin_tail_t tail_ext = { .get = tail_get };
533
534// ── GUI extension (only in plugin targets that define PULP_CLAP_GUI) ──
535
536#if defined(PULP_CLAP_GUI) && PULP_CLAP_GUI
537
538inline bool gui_is_api_supported(const clap_plugin_t*, const char* api, bool is_floating) {
539 if (pulp::format::detail::editor_launch_blocked_by_environment()) return false;
540 if (is_floating) return false;
541#ifdef __APPLE__
542 return strcmp(api, CLAP_WINDOW_API_COCOA) == 0;
543#elif defined(_WIN32)
544 return strcmp(api, CLAP_WINDOW_API_WIN32) == 0;
545#elif defined(__linux__)
546 return strcmp(api, CLAP_WINDOW_API_X11) == 0;
547#else
548 (void)api;
549 return false;
550#endif
551}
552
553inline bool gui_get_preferred_api(const clap_plugin_t*, const char** api, bool* is_floating) {
554 if (pulp::format::detail::editor_launch_blocked_by_environment()) return false;
555 *is_floating = false;
556#ifdef __APPLE__
557 *api = CLAP_WINDOW_API_COCOA;
558#elif defined(_WIN32)
559 *api = CLAP_WINDOW_API_WIN32;
560#elif defined(__linux__)
561 *api = CLAP_WINDOW_API_X11;
562#else
563 return false;
564#endif
565 return true;
566}
567
568inline bool request_host_resize(
569 clap_adapter::PulpClapPlugin& plugin,
570 uint32_t width,
571 uint32_t height) {
572 const clap_host_gui_t* gui = nullptr;
573 if (plugin.host && plugin.host->get_extension) {
574 gui = static_cast<const clap_host_gui_t*>(
575 plugin.host->get_extension(plugin.host, CLAP_EXT_GUI));
576 }
577 return gui && gui->request_resize &&
578 gui->request_resize(plugin.host, width, height);
579}
580
581inline void install_editor_resize_handler(
582 clap_adapter::PulpClapPlugin& plugin) {
583 if (!plugin.processor) return;
584 plugin.processor->set_editor_resize_handler(
585 &plugin,
586 [&plugin](uint32_t width, uint32_t height) -> bool {
587 if (!plugin.bridge) return false;
589 *plugin.bridge, width, height,
590 [&plugin](uint32_t requested_width,
591 uint32_t requested_height) {
592 return request_host_resize(
593 plugin, requested_width, requested_height);
594 });
595 if (!accepted) return false;
596
597 if (plugin.editor_host) {
599 *plugin.editor_host, plugin.bridge->size_hints(),
600 width, height);
601 }
602 return true;
603 });
604}
605
606// Native embedded CLAP editors are driven by the host's timer-support
607// extension. The callback is delivered on the host UI thread; use that thread
608// as the scheduling boundary and pass a monotonic timestamp into the Linux
609// PluginViewHost's HostFramePump. Hosts that do not expose timer-support keep
610// the honest static/input-triggered behavior and emit an unavailable receipt.
611inline void stop_native_gui_timer(clap_adapter::PulpClapPlugin& plugin) {
612 if (plugin.editor_timer_id != CLAP_INVALID_ID && plugin.editor_host_timer && plugin.host &&
613 plugin.editor_host_timer->unregister_timer) {
614 plugin.editor_host_timer->unregister_timer(plugin.host, plugin.editor_timer_id);
615 }
616 plugin.editor_timer_id = CLAP_INVALID_ID;
617 plugin.editor_host_timer = nullptr;
618}
619
620inline bool start_native_gui_timer(clap_adapter::PulpClapPlugin& plugin) {
621 if (plugin.editor_timer_id != CLAP_INVALID_ID)
622 return true;
623 if (!plugin.host || !plugin.host->get_extension)
624 return false;
625 auto* timer = static_cast<const clap_host_timer_support_t*>(
626 plugin.host->get_extension(plugin.host, CLAP_EXT_TIMER_SUPPORT));
627 if (!timer || !timer->register_timer || !timer->unregister_timer) {
628 runtime::log_warn("CLAP editor: host timer-support unavailable; "
629 "continuous animation disabled");
630 return false;
631 }
632 clap_id id = CLAP_INVALID_ID;
633 if (!timer->register_timer(plugin.host, 16, &id) || id == CLAP_INVALID_ID) {
634 runtime::log_warn("CLAP editor: host refused the 16 ms frame timer; "
635 "continuous animation disabled");
636 return false;
637 }
638 plugin.editor_host_timer = timer;
639 plugin.editor_timer_id = id;
640 return true;
641}
642
643inline void native_gui_on_timer(const clap_plugin_t* plugin, clap_id timer_id) {
644 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
645 if (!p || !p->editor_host || !p->editor_parent_attached || !p->editor_visible ||
646 p->editor_timer_id != timer_id)
647 return;
648 const double now =
649 std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
650 p->editor_host->advance_frame(now);
651}
652
653inline const clap_plugin_timer_support_t native_timer_ext = {
654 .on_timer = native_gui_on_timer,
655};
656
657inline bool gui_create(const clap_plugin_t* plugin, const char*, bool) {
658 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
659 if (pulp::format::detail::editor_launch_blocked_by_environment()) {
660 runtime::log_info("CLAP editor: disabled in headless/CI/test environment");
661 return false;
662 }
663 if (!p->processor || !p->processor->has_editor()) return false;
664
665 std::string editor_error;
666 p->bridge = std::make_unique<ViewBridge>(
667 *p->processor, p->store, p->owner_alive.capture(),
669 if (!p->bridge->open(&editor_error)) {
670 runtime::log_error("CLAP editor: bridge->open failed ({})", editor_error);
671 p->bridge.reset();
672 return false;
673 }
674
675 const auto& hints = p->bridge->size_hints();
676 const auto gpu = decide_gpu_host(*p->bridge);
677 const auto opts = editor_host_options(
678 *p->bridge, gpu, {hints.preferred_width, hints.preferred_height});
679
680 p->editor_host = view::PluginViewHost::create(*p->bridge->view(), opts);
681 if (p->editor_host) {
682 // Run editor automation, restore/reload, and scripted work per vsync.
683 p->editor_host->set_idle_callback(make_editor_idle_pump(*p->bridge));
684 // Route navigator.gpu / canvas.getContext('webgpu') through the host's
685 // live GpuSurface instead of the JS mock path — and keep following it.
686 // The Windows host does not have a surface until gui_set_parent()
687 // reparents its HWND, so the one-shot read this replaces was null on
688 // every Windows CLAP editor. The subscription also carries the detach
689 // edge, and owns the CPU-fallback diagnostic.
690 p->gpu_surface_binding = bind_gpu_surface(
691 *p->editor_host, p->bridge->scripted_ui(), gpu, "CLAP");
692 // Resize contract (ViewSize), mirroring the VST3 adapter:
693 // - not resizable (min==0): pin the viewport at preferred so an
694 // off-size pane letterbox-scales the content (today's behavior).
695 // - resizable + aspect_ratio>0: pin viewport + lock aspect; the
696 // adjust_size / get_resize_hints path enforces the aspect on drag.
697 // - resizable + aspect_ratio==0: honor "free drag within [min,max]" —
698 // NO viewport, NO aspect lock; the root reflows via Yoga at the host
699 // size and adjust_size only clamps min/max.
700 // `resizable` follows gui_can_resize's convention (min>0 on both axes).
701 configure_native_viewport(*p->editor_host, hints);
702 // Editor-INITIATED resize: let the editor ask the DAW to resize the
703 // plugin window (e.g. a chrome-hiding mode wanting a smaller shape).
704 // Publish the new hints optimistically because a host may synchronously
705 // query them from inside request_resize. A refusal rolls them back and
706 // leaves the live viewport untouched, so the old host window cannot
707 // render against the rejected mode's aspect. Captures the plugin struct
708 // `p`; gui_destroy clears the handler before tearing the editor down.
709 install_editor_resize_handler(*p);
710 runtime::log_info("CLAP editor: created ({}x{}, mode={}, gpu={})",
711 hints.preferred_width, hints.preferred_height,
712 gpu.mode, p->editor_host->is_gpu_backed());
713 } else {
714 runtime::log_error("CLAP editor: failed to create host");
715 p->bridge->close();
716 p->bridge.reset();
717 }
718 return p->editor_host != nullptr;
719}
720
721inline void gui_destroy(const clap_plugin_t* plugin) {
722 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
723 // Drop the editor→host resize handler BEFORE the bridge / editor host it
724 // captures, so a late call can never dereference freed editor state.
725 if (p->processor) p->processor->set_editor_resize_handler(p, nullptr);
726 // Unsubscribe BEFORE the host publishes its teardown and before the bridge
727 // that owns the scripted session goes away: no callback after teardown.
728 stop_native_gui_timer(*p);
729 p->editor_parent_attached = false;
730 p->gpu_surface_binding.reset();
731 p->editor_host.reset();
732 if (p->bridge) {
733 p->bridge->close();
734 p->bridge.reset();
735 }
736 p->editor_visible = false;
737}
738
739inline bool gui_set_scale(const clap_plugin_t*, double) {
740 return false; // Cocoa uses logical pixels — no explicit scaling needed
741}
742
743inline bool gui_get_size(const clap_plugin_t* plugin, uint32_t* width, uint32_t* height) {
744 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
745 if (!p->processor) return false;
746 if (p->bridge) {
747 const auto& h = p->bridge->size_hints();
748 *width = h.preferred_width;
749 *height = h.preferred_height;
750 } else {
751 auto [w, ht] = p->processor->editor_size();
752 *width = w;
753 *height = ht;
754 }
755 return true;
756}
757
758// Editor resize negotiation: the plugin advertises proportional resize
759// locked to the editor's design aspect. The host's design viewport
760// (set in gui_create) scales content to fit the host window without
761// re-layout — so any (w, h) the DAW lands on still looks correct.
762// Resize capability is declared by non-zero min_width/min_height in
763// view_size() — the same bounds gui_get_resize_hints exposes. Plugins
764// that use the base-class default (min_width=0, min_height=0) are not
765// resizable; plugins that declare bounds (e.g. GpuEditorSmoke {320,240})
766// are. This check works before gui_create (no bridge needed).
767inline bool gui_can_resize(const clap_plugin_t* plugin) {
768 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
769 if (!p->processor) return false;
770 const auto vs = p->processor->view_size();
771 return vs.min_width > 0 && vs.min_height > 0;
772}
773
774inline bool gui_get_resize_hints(const clap_plugin_t* plugin,
775 clap_gui_resize_hints_t* hints) {
776 if (!hints) return false;
777 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
778 if (!p->bridge) return false;
779 const auto& size_hints = p->bridge->size_hints();
780 if (size_hints.preferred_width == 0 || size_hints.preferred_height == 0) {
781 return false;
782 }
783 hints->can_resize_horizontally = true;
784 hints->can_resize_vertically = true;
785 // preserve_aspect_ratio tracks the resize contract: a resizable editor with
786 // aspect_ratio==0 drags freely (no lock); otherwise the aspect is held.
787 const bool resizable =
788 size_hints.min_width > 0 && size_hints.min_height > 0;
789 const bool free_resize = resizable && size_hints.aspect_ratio <= 0.0;
790 hints->preserve_aspect_ratio = !free_resize;
791 hints->aspect_ratio_width = size_hints.preferred_width;
792 hints->aspect_ratio_height = size_hints.preferred_height;
793 return true;
794}
795
796inline bool gui_adjust_size(const clap_plugin_t* plugin,
797 uint32_t* width, uint32_t* height) {
798 if (!width || !height || *width == 0 || *height == 0) return false;
799 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
800 if (!p->bridge) return false;
801 const auto& size_hints = p->bridge->size_hints();
802 if (size_hints.preferred_width == 0 || size_hints.preferred_height == 0) {
803 return false;
804 }
805 // Free-resize contract: resizable (min>0) with aspect_ratio==0 means "any
806 // ratio; drag freely within [min,max]". Clamp each axis independently — NO
807 // aspect snap — matching gui_get_resize_hints.preserve_aspect_ratio=false.
808 const bool resizable =
809 size_hints.min_width > 0 && size_hints.min_height > 0;
810 const bool free_resize = resizable && size_hints.aspect_ratio <= 0.0;
811 if (free_resize) {
812 if (size_hints.min_width > 0 && *width < size_hints.min_width)
813 *width = size_hints.min_width;
814 if (size_hints.min_height > 0 && *height < size_hints.min_height)
815 *height = size_hints.min_height;
816 if (size_hints.max_width > 0 && *width > size_hints.max_width)
817 *width = size_hints.max_width;
818 if (size_hints.max_height > 0 && *height > size_hints.max_height)
819 *height = size_hints.max_height;
820 return true;
821 }
822 // Snap to the design aspect ratio — pick the largest box with the
823 // design aspect that fits within the requested rectangle. Same shape
824 // the standalone host's drag-to-resize lands on.
825 const double design_aspect =
826 static_cast<double>(size_hints.preferred_width) /
827 static_cast<double>(size_hints.preferred_height);
828 const double req_aspect =
829 static_cast<double>(*width) /
830 static_cast<double>(*height);
831 if (req_aspect > design_aspect) {
832 // Requested rect is too wide → shrink width to height * aspect.
833 *width = static_cast<uint32_t>(
834 static_cast<double>(*height) * design_aspect + 0.5);
835 } else if (req_aspect < design_aspect) {
836 // Requested rect is too tall → shrink height to width / aspect.
837 *height = static_cast<uint32_t>(
838 static_cast<double>(*width) / design_aspect + 0.5);
839 }
840 // Respect plugin min/max constraints when defined. A naive clamp
841 // after the aspect snap would re-introduce off-aspect rects (e.g.
842 // design 2:1, request (500,1000) snaps to (500,250); if min_height
843 // is 400, a naive clamp gives (500,400) — no longer 2:1). After
844 // any clamp, re-snap by EXPANDING the other dimension to restore
845 // the design aspect. The advertised preserve_aspect_ratio=true
846 // contract MUST hold.
847 auto resnap = [&]() {
848 // Use whichever dimension expanded by the clamp; expand the
849 // other to match the design aspect.
850 const double aspect_now =
851 static_cast<double>(*width) / static_cast<double>(*height);
852 if (aspect_now > design_aspect) {
853 *height = static_cast<uint32_t>(
854 static_cast<double>(*width) / design_aspect + 0.5);
855 } else if (aspect_now < design_aspect) {
856 *width = static_cast<uint32_t>(
857 static_cast<double>(*height) * design_aspect + 0.5);
858 }
859 };
860 if (size_hints.min_width > 0 && *width < size_hints.min_width) {
861 *width = size_hints.min_width;
862 resnap();
863 }
864 if (size_hints.min_height > 0 && *height < size_hints.min_height) {
865 *height = size_hints.min_height;
866 resnap();
867 }
868 if (size_hints.max_width > 0 && *width > size_hints.max_width) {
869 *width = size_hints.max_width;
870 resnap();
871 }
872 if (size_hints.max_height > 0 && *height > size_hints.max_height) {
873 *height = size_hints.max_height;
874 resnap();
875 }
876 return true;
877}
878
879inline bool gui_set_size(const clap_plugin_t* plugin, uint32_t width, uint32_t height) {
880 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
881 if (p->bridge) p->bridge->resize(width, height);
882 if (p->editor_host) {
883 p->editor_host->set_size(width, height);
884 return true;
885 }
886 return false;
887}
888
889inline bool gui_set_parent(const clap_plugin_t* plugin, const clap_window_t* window) {
890 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
891 if (!p->editor_host) return false;
892
893 // A reparent is a lifecycle boundary. Stop any queued timer before
894 // attaching the new native parent; the callback checks attachment again,
895 // but unregistering here avoids keeping a hidden editor alive between
896 // parent transactions.
897 stop_native_gui_timer(*p);
898 p->editor_parent_attached = false;
899
900 bool attached = false;
901#ifdef __APPLE__
902 if (strcmp(window->api, CLAP_WINDOW_API_COCOA) == 0) {
903 attached = p->editor_host->try_attach_to_parent(window->cocoa);
904 }
905#elif defined(_WIN32)
906 if (strcmp(window->api, CLAP_WINDOW_API_WIN32) == 0) {
907 attached = p->editor_host->try_attach_to_parent(window->win32);
908 }
909#elif defined(__linux__)
910 if (strcmp(window->api, CLAP_WINDOW_API_X11) == 0) {
911 attached = p->editor_host->try_attach_to_parent(reinterpret_cast<void*>(window->x11));
912 }
913#endif
914 if (attached && p->bridge) {
915 p->editor_parent_attached = true;
916 p->bridge->notify_attached();
917 // Content-first: the first frame the host composites is the mounted
918 // document (ViewBridge::prepare_first_frame()).
919 p->bridge->prepare_first_frame(*p->editor_host);
920#if defined(__linux__)
921 // CLAP normally calls show() after set_parent(), but tolerate hosts
922 // that make the calls in the opposite order without leaving a visible
923 // editor permanently unscheduled.
924 if (p->editor_visible)
925 start_native_gui_timer(*p);
926#endif
927 }
928 return attached;
929}
930
931inline bool gui_set_transient(const clap_plugin_t*, const clap_window_t*) {
932 return false; // No floating window support
933}
934
935inline void gui_suggest_title(const clap_plugin_t*, const char*) {
936 // No-op for embedded windows
937}
938
939inline bool gui_show(const clap_plugin_t* plugin) {
940 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
941 if (p->editor_host) {
942 p->editor_visible = true;
943#if defined(__linux__)
944 start_native_gui_timer(*p);
945#endif
946 p->editor_host->repaint();
947 return true;
948 }
949 return false;
950}
951
952inline bool gui_hide(const clap_plugin_t* plugin) {
953 auto* p = static_cast<clap_adapter::PulpClapPlugin*>(plugin->plugin_data);
954 p->editor_visible = false;
955#if defined(__linux__)
956 stop_native_gui_timer(*p);
957#endif
958 // A hidden editor keeps its host and surfaces for gui_show, but nothing it
959 // is not drawing should keep holding GPU memory meanwhile.
960 if (p->editor_host)
961 p->editor_host->release_gpu_caches();
962 return true;
963}
964
965inline const clap_plugin_gui_t gui_ext = {
966 .is_api_supported = gui_is_api_supported,
967 .get_preferred_api = gui_get_preferred_api,
968 .create = gui_create,
969 .destroy = gui_destroy,
970 .set_scale = gui_set_scale,
971 .get_size = gui_get_size,
972 .can_resize = gui_can_resize,
973 .get_resize_hints = gui_get_resize_hints,
974 .adjust_size = gui_adjust_size,
975 .set_size = gui_set_size,
976 .set_parent = gui_set_parent,
977 .set_transient = gui_set_transient,
978 .suggest_title = gui_suggest_title,
979 .show = gui_show,
980 .hide = gui_hide,
981};
982
983#endif // PULP_CLAP_GUI
984
985// ── Extension dispatch ─────────────────────────────────────────────────
986inline const void* get_static_extension(const clap_plugin_t* plugin, const char* id) {
987 if (!id) return nullptr;
988 if (strcmp(id, CLAP_EXT_GUI) == 0) {
989 // A processor with an editor factory opens its editor through the
990 // editor-host contract; create_view() stays the path without one.
991#if defined(PULP_CLAP_CONTRACT_GUI)
992 if (const auto* contract = clap_adapter::contract_gui_extension(plugin))
993 return contract;
994#endif
995#if defined(PULP_CLAP_GUI) && PULP_CLAP_GUI
996 if (pulp::format::detail::editor_launch_blocked_by_environment()) return nullptr;
997 return &gui_ext;
998#else
999 return nullptr;
1000#endif
1001 }
1002#if defined(PULP_CLAP_CONTRACT_GUI)
1003 if (strcmp(id, CLAP_EXT_TIMER_SUPPORT) == 0)
1004 if (const auto* contract = clap_adapter::contract_timer_extension(plugin))
1005 return contract;
1006#endif
1007#if defined(PULP_CLAP_GUI) && PULP_CLAP_GUI && defined(__linux__)
1008 if (strcmp(id, CLAP_EXT_TIMER_SUPPORT) == 0)
1009 return &native_timer_ext;
1010#endif
1011 if (strcmp(id, CLAP_EXT_AUDIO_PORTS) == 0) return &audio_ports_ext;
1012 if (strcmp(id, CLAP_EXT_AUDIO_PORTS_CONFIG) == 0)
1014 if (strcmp(id, CLAP_EXT_NOTE_PORTS) == 0) return &note_ports_ext;
1015 if (strcmp(id, CLAP_EXT_VOICE_INFO) == 0)
1016 return declared_voice_count(plugin) > 0 ? &voice_info_ext : nullptr;
1017 if (strcmp(id, CLAP_EXT_PARAMS) == 0) return &params_ext;
1018 if (strcmp(id, CLAP_EXT_STATE) == 0) return &state_ext;
1019 if (strcmp(id, CLAP_EXT_LATENCY) == 0) return &latency_ext;
1020 if (strcmp(id, CLAP_EXT_TAIL) == 0) return &tail_ext;
1021 return nullptr;
1022}
1023
1024inline const void* get_extension(const clap_plugin_t* plugin, const char* id) {
1025 if (const void* ext = get_static_extension(plugin, id)) {
1026 return ext;
1027 }
1029}
1030
1031// ── Plugin creation ────────────────────────────────────────────────────
1032inline const clap_plugin_t* create_plugin(const clap_plugin_factory_t*,
1033 const clap_host_t* host,
1034 const char* plugin_id) {
1035 // Resolve which of the binary's plugins the host asked for. In a
1036 // single-plugin binary there is exactly one record; in a bundle, match by id.
1037 ClapPluginRecord* rec = find_clap_record(plugin_id);
1038 if (!rec) return nullptr;
1039
1040 auto* instance = new clap_adapter::PulpClapPlugin();
1041 instance->factory = rec->factory;
1042 // Cache this plugin's descriptor so pre-init extension queries (audio/note
1043 // ports) resolve per-plugin metadata without a shared global.
1044 instance->descriptor_snapshot = rec->desc;
1045 // Keep the host pointer so clap_on_main_thread() can republish
1046 // latency / tail changes the processor flagged.
1047 instance->host = host;
1048 instance->plugin = {
1049 .desc = &rec->clap_desc,
1050 .plugin_data = instance,
1052 .destroy = clap_adapter::clap_destroy,
1053 .activate = clap_adapter::clap_activate,
1054 .deactivate = clap_adapter::clap_deactivate,
1055 .start_processing = clap_adapter::clap_start_processing,
1056 .stop_processing = clap_adapter::clap_stop_processing,
1057 .reset = clap_adapter::clap_reset,
1058 .process = clap_adapter::clap_process,
1059 .get_extension = get_extension,
1060 .on_main_thread = clap_adapter::clap_on_main_thread,
1061 };
1062 return &instance->plugin;
1063}
1064
1065// Enumerate over the deduplicated index (built in init_all_records) so the host
1066// only ever sees addressable, uniquely-identified plugins — never a failed-init
1067// hole or an advertised-but-unreachable duplicate id.
1068inline uint32_t get_plugin_count(const clap_plugin_factory_t*) {
1069 return g_clap_index_count;
1070}
1071inline const clap_plugin_descriptor_t* get_plugin_descriptor(const clap_plugin_factory_t*, uint32_t i) {
1072 if (i >= g_clap_index_count) return nullptr;
1073 return &g_clap_index[i]->clap_desc;
1074}
1075
1076inline const clap_plugin_factory_t plugin_factory = {
1077 .get_plugin_count = get_plugin_count,
1078 .get_plugin_descriptor = get_plugin_descriptor,
1079 .create_plugin = create_plugin,
1080};
1081
1082inline bool entry_init(const char*) {
1083 // Build every registered plugin's descriptor here — the host's module-load
1084 // callback, safely after C++ static initialization — never at static init.
1086 return true;
1087}
1088// CLAP's init/deinit are nestable and matched, and { return true; } / {} pairs
1089// are explicitly sanctioned by the entry contract. Our records are an immutable,
1090// bounded (kMaxClapPlugins) module-lifetime cache: init_record() is idempotent,
1091// so a re-init after a matched deinit reuses identical descriptors, and the
1092// storage does not grow per cycle. Final DSO unload reclaims it. This mirrors the
1093// AU/VST3 registrars, which run at static init and never unregister. Nothing to
1094// release here — and doing so would be actively wrong, since a record's clap_desc
1095// backs any clap_plugin_t the host has not yet destroyed.
1096inline void entry_deinit() {}
1097inline const void* entry_get_factory(const char* factory_id) {
1098 if (strcmp(factory_id, CLAP_PLUGIN_FACTORY_ID) == 0) return &plugin_factory;
1099 return nullptr;
1100}
1101
1102} // namespace pulp::format::clap_generic
1103
1104// ── Public macros ──────────────────────────────────────────────────────
1105
1106// The one CLAP module entry symbol. Exactly ONE per binary — the single-plugin
1107// macro emits it for you; a bundle emits it once via PULP_CLAP_BUNDLE_ENTRY().
1108#define PULP_CLAP_BUNDLE_ENTRY() \
1109 extern "C" CLAP_EXPORT const clap_plugin_entry_t clap_entry = { \
1110 .clap_version = CLAP_VERSION, \
1111 .init = pulp::format::clap_generic::entry_init, \
1112 .deinit = pulp::format::clap_generic::entry_deinit, \
1113 .get_factory = pulp::format::clap_generic::entry_get_factory, \
1114 };
1115
1116// Register one plugin in a multi-plugin CLAP bundle. Repeat once per plugin,
1117// each with a distinct `Ident` token (used only to name the registrar). The
1118// factory is stored at static init; its descriptor is built later in
1119// entry_init() (never at static init). Pair N of these with ONE
1120// PULP_CLAP_BUNDLE_ENTRY(). Bundles do NOT set the legacy global factory slot —
1121// each plugin is resolved by id at create_plugin().
1122#define PULP_CLAP_BUNDLE_PLUGIN(Ident, factory_fn) \
1123 namespace { \
1124 struct Ident##_ClapRegistrar { \
1125 Ident##_ClapRegistrar() { \
1126 pulp::format::clap_generic::register_clap_record(factory_fn, \
1127 /*publish_keyed=*/true); \
1128 } \
1129 } Ident##_clap_registrar_; \
1130 }
1131
1132// Place in ONE .cpp file per single-plugin CLAP bundle. Registers the one
1133// plugin, keeps the legacy global factory slot (single-plugin back-compat), and
1134// emits the module entry. Unchanged usage; descriptor build now happens in
1135// entry_init() rather than at static init, so no Processor is constructed during
1136// C++ static initialization.
1137#define PULP_CLAP_PLUGIN(factory_fn) \
1138 namespace { \
1139 struct PulpClapInit { \
1140 PulpClapInit() { \
1141 pulp::format::clap_generic::register_clap_record(factory_fn, \
1142 /*publish_keyed=*/false); \
1143 pulp::format::register_plugin(factory_fn); \
1144 } \
1145 } _pulp_clap_init; \
1146 } \
1147 PULP_CLAP_BUNDLE_ENTRY()
Validated, registration-ordered projection of StateStore parameter groups.
Definition param_group_projection.hpp:16
const Entry * find(int id) const noexcept
Definition param_group_projection.hpp:98
std::span< const ParamInfo > all_params() const
View of all registered parameters (in registration order).
Definition store.hpp:439
static std::unique_ptr< PluginViewHost > create(View &root, Size size)
@ accepted
Definition realtime_time_stretch.hpp:31
void clap_on_main_thread(const clap_plugin_t *plugin)
bool clap_init(const clap_plugin_t *plugin)
void clap_destroy(const clap_plugin_t *plugin)
void clap_stop_processing(const clap_plugin_t *plugin)
clap_process_status clap_process(const clap_plugin_t *plugin, const clap_process_t *process)
bool clap_start_processing(const clap_plugin_t *plugin)
void clap_deactivate(const clap_plugin_t *plugin)
void clap_params_flush(const clap_plugin_t *plugin, const clap_input_events_t *in, const clap_output_events_t *out)
const clap_plugin_timer_support_t * contract_timer_extension(const clap_plugin_t *plugin)
The timer-support extension that drives a contract editor's idle tick, or null when the contract GUI ...
const void * clap_get_extension(const clap_plugin_t *plugin, const char *id)
void clap_reset(const clap_plugin_t *plugin)
bool clap_activate(const clap_plugin_t *plugin, double sr, uint32_t min_frames, uint32_t max_frames)
const clap_plugin_gui_t * contract_gui_extension(const clap_plugin_t *plugin)
The contract GUI extension for plugin, or null when its processor is not initialized,...
Definition clap_entry.hpp:45
bool params_get_info(const clap_plugin_t *plugin, uint32_t index, clap_param_info_t *info)
Definition clap_entry.hpp:387
const clap_plugin_factory_t plugin_factory
Definition clap_entry.hpp:1076
const void * get_static_extension(const clap_plugin_t *plugin, const char *id)
Definition clap_entry.hpp:986
uint32_t note_ports_count(const clap_plugin_t *plugin, bool is_input)
Definition clap_entry.hpp:448
const void * get_extension(const clap_plugin_t *plugin, const char *id)
Definition clap_entry.hpp:1024
bool params_value_to_text(const clap_plugin_t *plugin, clap_id param_id, double value, char *display, uint32_t size)
Definition clap_entry.hpp:411
uint32_t tail_get(const clap_plugin_t *plugin)
Definition clap_entry.hpp:524
bool params_text_to_value(const clap_plugin_t *plugin, clap_id param_id, const char *text, double *value)
Definition clap_entry.hpp:422
bool audio_ports_config_get(const clap_plugin_t *plugin, uint32_t index, clap_audio_ports_config_t *config)
Definition clap_entry.hpp:252
bool audio_ports_get(const clap_plugin_t *plugin, uint32_t index, bool is_input, clap_audio_port_info_t *info)
Definition clap_entry.hpp:197
const clap_plugin_voice_info_t voice_info_ext
Definition clap_entry.hpp:507
bool note_ports_get(const clap_plugin_t *plugin, uint32_t index, bool is_input, clap_note_port_info_t *info)
Definition clap_entry.hpp:457
bool params_get_value(const clap_plugin_t *plugin, clap_id param_id, double *value)
Definition clap_entry.hpp:405
uint32_t audio_ports_config_count(const clap_plugin_t *plugin)
Definition clap_entry.hpp:245
uint32_t declared_voice_count(const clap_plugin_t *plugin)
Definition clap_entry.hpp:488
void params_flush(const clap_plugin_t *plugin, const clap_input_events_t *in, const clap_output_events_t *out)
Definition clap_entry.hpp:436
bool state_load(const clap_plugin_t *plugin, const clap_istream_t *stream)
Definition clap_entry.hpp:321
uint32_t g_clap_record_count
Definition clap_entry.hpp:72
void reset_clap_records_for_testing()
Definition clap_entry.hpp:168
bool state_save(const clap_plugin_t *plugin, const clap_ostream_t *stream)
Definition clap_entry.hpp:301
constexpr uint32_t kMaxClapPlugins
Definition clap_entry.hpp:70
uint32_t audio_ports_count(const clap_plugin_t *plugin, bool is_input)
Definition clap_entry.hpp:190
bool audio_ports_config_select(const clap_plugin_t *plugin, clap_id config_id)
Definition clap_entry.hpp:282
bool entry_init(const char *)
Definition clap_entry.hpp:1082
const clap_plugin_audio_ports_config_t audio_ports_config_ext
Definition clap_entry.hpp:294
uint32_t g_clap_index_count
Definition clap_entry.hpp:80
const clap_plugin_audio_ports_t audio_ports_ext
Definition clap_entry.hpp:241
const void * entry_get_factory(const char *factory_id)
Definition clap_entry.hpp:1097
const clap_plugin_params_t params_ext
Definition clap_entry.hpp:441
const clap_plugin_tail_t tail_ext
Definition clap_entry.hpp:532
ClapPluginRecord * find_clap_record(const char *id)
Definition clap_entry.hpp:178
const clap_plugin_note_ports_t note_ports_ext
Definition clap_entry.hpp:483
void entry_deinit()
Definition clap_entry.hpp:1096
void register_clap_record(ProcessorFactory factory, bool publish_keyed)
Definition clap_entry.hpp:84
const clap_plugin_t * create_plugin(const clap_plugin_factory_t *, const clap_host_t *host, const char *plugin_id)
Definition clap_entry.hpp:1032
ClapPluginRecord g_clap_records[kMaxClapPlugins]
Definition clap_entry.hpp:71
ClapPluginRecord * g_clap_index[kMaxClapPlugins]
Definition clap_entry.hpp:79
void init_all_records()
Definition clap_entry.hpp:129
uint32_t get_plugin_count(const clap_plugin_factory_t *)
Definition clap_entry.hpp:1068
uint32_t params_flags(const state::ParamInfo &parameter)
Definition clap_entry.hpp:364
uint32_t latency_get(const clap_plugin_t *plugin)
Definition clap_entry.hpp:510
const clap_plugin_state_t state_ext
Definition clap_entry.hpp:356
void init_record(ClapPluginRecord &rec)
Definition clap_entry.hpp:92
const clap_plugin_latency_t latency_ext
Definition clap_entry.hpp:521
const clap_plugin_descriptor_t * get_plugin_descriptor(const clap_plugin_factory_t *, uint32_t i)
Definition clap_entry.hpp:1071
bool voice_info_get(const clap_plugin_t *plugin, clap_voice_info_t *info)
Definition clap_entry.hpp:494
uint32_t params_count(const clap_plugin_t *plugin)
Definition clap_entry.hpp:359
bool negotiate_preferred_size(ViewBridge &bridge, uint32_t width, uint32_t height, HostRequest &&request)
Internal transaction shared by the CLAP and VST3 adapters.
Definition view_bridge.hpp:456
bool deserialize(std::span< const uint8_t > bytes, state::StateStore &store, Processor &processor)
Restore a host-facing plugin state blob.
std::vector< uint8_t > serialize(const state::StateStore &store, const Processor &processor)
Serialize the host-facing plugin state blob for a Processor.
constexpr int reported_latency_samples(int raw, const HostQuirks &q) noexcept
Apply clamp_latency_to_nonneg to a raw Processor::latency_samples() value before reporting it to the ...
Definition host_quirks.hpp:480
std::function< void()> make_editor_idle_pump(ViewBridge &bridge)
Build the per-vsync idle pump for a bridge.
Definition editor_idle_pump.hpp:18
void commit_editor_requested_viewport(NativeHost &host, const ViewSize &hints, uint32_t width, uint32_t height)
Commit viewport state after an editor-initiated host resize succeeds.
Definition plugin_descriptor.hpp:161
std::unique_ptr< Processor >(*)() ProcessorFactory
Factory function type — plugins provide this to create processor instances.
Definition processor.hpp:1416
GpuHostDecision decide_gpu_host(const ViewBridge &bridge)
Decide whether the GPU host should be requested for bridge, and emit the [plugin-gpu-host] debug-pari...
Definition gpu_host_select.hpp:46
view::PluginViewHost::Options editor_host_options(const ViewBridge &bridge, const GpuHostDecision &gpu, view::PluginViewHost::Size size)
The editor-host options every format adapter builds its PluginViewHost from: the size the adapter neg...
Definition gpu_host_select.hpp:104
std::string format_parameter_text(const state::ParamInfo &info, float plain_value)
Canonical plain-value formatting shared by format adapters and framework UI.
Definition parameter_text.hpp:16
void configure_native_viewport(NativeHost &host, const ViewSize &hints)
Apply the declared viewport policy to a newly-created native host.
Definition plugin_descriptor.hpp:142
@ Instrument
Synth/sampler (receives MIDI, produces audio).
Definition plugin_descriptor.hpp:187
@ MidiEffect
MIDI processor (receives MIDI, produces MIDI).
Definition plugin_descriptor.hpp:188
@ Effect
Audio effect (takes input, produces output).
Definition plugin_descriptor.hpp:186
@ ok
Definition offline_sample_bounce.hpp:20
void register_plugin(ProcessorFactory factory)
const PluginRegistration * find_plugin(const char *id)
std::optional< float > parse_parameter_text(const state::ParamInfo &info, std::string_view text)
Parse host text into a constrained plain value.
Definition parameter_text.hpp:52
view::PluginViewHost::GpuSurfaceSubscription bind_gpu_surface(view::PluginViewHost &host, view::ScriptedEditorSession *session, const GpuHostDecision &decision, const char *format_tag)
Wire a host's GPU-surface lifecycle to a scripted UI session, for every format.
Definition gpu_host_select.hpp:162
@ plugin
Definition editor_host_kind.hpp:15
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
@ timer
Conscious ~60 Hz timer fallback (Linux/other).
Definition render_loop.hpp:56
void log_warn(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:190
void log_error(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:195
void copy_c_string(char *dest, std::size_t dest_size, std::string_view src)
Definition system.hpp:55
void log_info(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:185
@ width
Definition config.hpp:225
bool is_bypass_param(const ParamInfo &info)
Decide whether info is a bypass control.
Definition parameter.hpp:514
bool is_modulatable_param(const ParamInfo &info)
Decide whether a host may apply a modulation offset to info.
Definition parameter.hpp:537
bool is_hidden_param(const ParamInfo &info)
Whether the host should omit this parameter from its generic parameter list and automation lanes.
Definition parameter.hpp:560
bool is_discrete_param(const ParamInfo &info)
Definition parameter.hpp:417
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
bool is_read_only_param(const ParamInfo &info)
Whether the host may read this parameter but must not write it.
Definition parameter.hpp:574
bool is_automatable_param(const ParamInfo &info)
Whether the host may record and play back automation for this parameter.
Definition parameter.hpp:590
@ gpu
Offscreen GPU (Dawn + Skia via pulp::render::HeadlessSurface).
Definition screenshot.hpp:16
@ h
Definition input_events.hpp:39
@ p
Definition input_events.hpp:40
@ w
Definition input_events.hpp:41
@ height
Definition css_animation.hpp:128
Plugin metadata — declared once, immutable.
Definition plugin_descriptor.hpp:245
std::string bundle_id
Definition plugin_descriptor.hpp:248
std::string manufacturer
Definition plugin_descriptor.hpp:247
std::string name
Definition plugin_descriptor.hpp:246
PluginCategory category
Definition plugin_descriptor.hpp:250
std::string version
Semantic version string, e.g. "1.0.0".
Definition plugin_descriptor.hpp:249
One registered plugin.
Definition registry.hpp:35
static Options hosted_editor()
Options for a primary editor embedded in a host: hot reload follows the developer's PULP_DEV_HOT_RELO...
Definition view_bridge.hpp:112
Definition clap_adapter.hpp:81
state::StateStore store
Definition clap_adapter.hpp:94
std::unique_ptr< Processor > processor
Definition clap_adapter.hpp:95
bool publish_keyed
Definition clap_entry.hpp:65
ProcessorFactory factory
Definition clap_entry.hpp:55
PluginDescriptor desc
Definition clap_entry.hpp:56
const char * features[4]
Definition clap_entry.hpp:58
bool initialized
Definition clap_entry.hpp:59
clap_plugin_descriptor_t clap_desc
Definition clap_entry.hpp:57
Immutable parameter metadata, registered once at plugin initialization.
Definition parameter.hpp:316