DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DSParkClap.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
53#define DSPARK_PLUGIN_CLAP_INCLUDED 1
54
55#include "../DSParkPlugin.h"
56
57#include "clap/clap.h"
58
59#include <algorithm>
60#include <atomic>
61#include <cstdint>
62#include <cstdlib>
63#include <cstring>
64#include <new>
65#include <vector>
66
68
69inline constexpr uint32_t kBypassParamId = 0x42595053u; // matches the VST3 backend
70inline constexpr int kBypassRampSamples = 256;
71
72template <typename P>
73struct Plugin
74{
75 static constexpr size_t kNumParams = P::parameters.size();
76 static constexpr bool kIsInstrument =
77 P::descriptor.category == Category::Instrument;
78 static constexpr int kNumPresets = factoryPresetCountOf<P>();
79 static_assert(!(kIsInstrument && HasSidechain<P>),
80 "an Instrument has no audio inputs; remove the sidechain "
81 "processBlock or use Category::Fx");
82 static_assert(!kIsInstrument || HasMidi<P>,
83 "an Instrument needs handleMidiEvent (see HasMidi): it has "
84 "no audio input to process");
85 static_assert(paramIdsUnique<P>(),
86 "two parameter ids share a hash32 (or collide with the "
87 "reserved PRGM/BYPS state ids): automation and state would "
88 "cross-wire. Rename one id.");
89
90 clap_plugin_t plugin {}; // the C-facing object (plugin_data = this)
91 const clap_host_t* host = nullptr;
92 const clap_host_latency_t* hostLatency = nullptr;
93 const clap_host_params_t* hostParams = nullptr;
94 const clap_host_preset_load_t* hostPresetLoad = nullptr;
95
96 P user {};
97 double sampleRate = 48000.0;
98 uint32_t maxFrames = 0;
99 bool prepared = false;
101 int currentChannels = defaultChannelCount<P>(); // selected ports config
102 bool offlineRender = false; // last render-extension setting
103 std::atomic<bool> latencyDirty { false }; // audio -> main thread notify
104
105 std::atomic<double> shadow[kNumParams == 0 ? 1 : kNumParams] {};
106 std::atomic<bool> bypass { false };
107 std::atomic<int> currentProgram { 0 }; // last factory preset loaded
108 float bypassMix = 0.0f;
109 std::vector<float> dryL, dryR;
110 std::vector<float> silence; // stand-in sidechain when not connected
111
112#if defined(DSPARK_PLUGIN_WEBVIEW)
113 // --- editor state + UI -> host event queue (single producer: main thread;
114 // single consumer: process() on audio or flush() on main - never both).
115 webview_ui::Editor<P> guiEditor;
116 bool guiActive = false;
117 double guiScale = 1.0;
118 int guiWidth = 0, guiHeight = 0;
119 bool guiEditActive[kNumParams == 0 ? 1 : kNumParams] {};
120#if defined(__linux__)
121 // GTK is driven from the host's run loop: a timer-support tick pumps it.
122 const clap_host_timer_support_t* hostTimer = nullptr;
123 clap_id guiTimerId = CLAP_INVALID_ID;
124#endif
125
126 enum : uint8_t { kUiGestureBegin = 0, kUiValue = 1, kUiGestureEnd = 2 };
127 struct UiEvent
128 {
129 uint32_t paramId;
130 uint8_t kind;
131 double value;
132 };
133 static constexpr uint32_t kUiQueueSize = 256; // power of two
134 UiEvent uiQueue[kUiQueueSize] {};
135 std::atomic<uint32_t> uiHead { 0 }, uiTail { 0 };
136#endif
137
138 explicit Plugin(const clap_host_t* h) noexcept : host(h)
139 {
140 for (size_t i = 0; i < kNumParams; ++i)
141 shadow[i].store(toNormalized(P::parameters[i], P::parameters[i].defValue),
142 std::memory_order_relaxed);
143 }
144
145 static Plugin* self(const clap_plugin_t* p) noexcept
146 {
147 return static_cast<Plugin*>(p->plugin_data);
148 }
149
150 void applyNormalized(int index, double normalized) noexcept
151 {
152 // Host values are untrusted doubles: a NaN would pass both range
153 // clamps, poison the shadow and reach the user's setter. Ignore it.
154 if (normalized != normalized) return;
155 const auto& spec = P::parameters[static_cast<size_t>(index)];
156 shadow[static_cast<size_t>(index)].store(normalized, std::memory_order_relaxed);
157 user.setParameter(index, static_cast<float>(toPlain(spec, normalized)));
158 }
159
160 static int indexOfParamId(uint32_t id) noexcept
161 {
162 for (size_t i = 0; i < kNumParams; ++i)
163 if (hash32(P::parameters[i].id) == id) return static_cast<int>(i);
164 return -1;
165 }
166
168 void applyFactoryPresetIdx(int idx) noexcept
169 {
170 if constexpr (kNumPresets > 0)
171 {
172 if (idx < 0 || idx >= kNumPresets) return;
173 for (size_t i = 0; i < kNumParams; ++i)
174 applyNormalized(static_cast<int>(i), presetNormalized<P>(idx, i));
175 currentProgram.store(idx, std::memory_order_relaxed);
176 }
177 else
178 (void) idx;
179 }
180
184 void refreshLatency() noexcept
185 {
186 if constexpr (HasLatency<P>)
187 {
188 const int now = user.getLatency();
189 if (prepared && now != cachedLatency)
190 {
191 cachedLatency = now;
192 latencyDirty.store(true, std::memory_order_release);
193 if (host != nullptr && host->request_callback != nullptr)
194 host->request_callback(host);
195 }
196 }
197 }
198
202 void collectEvent(const clap_event_header_t* ev, uint32_t numSamples,
203 BlockEvent* events, int& count) noexcept
204 {
205 if (ev == nullptr || ev->space_id != CLAP_CORE_EVENT_SPACE_ID) return;
206 BlockEvent out {};
207 out.offset = static_cast<int32_t>(ev->time);
208 if (numSamples > 0 && out.offset >= static_cast<int32_t>(numSamples))
209 out.offset = static_cast<int32_t>(numSamples) - 1;
210 if (out.offset < 0) out.offset = 0;
211
212 if (ev->type == CLAP_EVENT_PARAM_VALUE)
213 {
214 const auto* pv = reinterpret_cast<const clap_event_param_value_t*>(ev);
215 if (pv->param_id == kBypassParamId)
216 {
217 out.kind = BlockEvent::Kind::Bypass;
218 out.value = pv->value;
219 }
220 else
221 {
222 const int idx = indexOfParamId(pv->param_id);
223 if (idx < 0) return;
224 out.kind = BlockEvent::Kind::Param;
225 out.paramId = pv->param_id;
226 out.value = toNormalized(P::parameters[static_cast<size_t>(idx)],
227 pv->value);
228 }
229 }
230 else if constexpr (HasMidi<P>)
231 {
232 if (ev->type == CLAP_EVENT_NOTE_ON || ev->type == CLAP_EVENT_NOTE_OFF)
233 {
234 const auto* note = reinterpret_cast<const clap_event_note_t*>(ev);
235 out.kind = BlockEvent::Kind::Midi;
236 out.midi.type = ev->type == CLAP_EVENT_NOTE_ON
238 out.midi.channel = static_cast<uint8_t>(
239 note->channel >= 0 ? note->channel & 0x0F : 0);
240 out.midi.note = static_cast<uint8_t>(
241 note->key >= 0 ? note->key & 0x7F : 0);
242 out.midi.value = static_cast<float>(note->velocity);
243 }
244 else if (ev->type == CLAP_EVENT_MIDI)
245 {
246 const auto* midi = reinterpret_cast<const clap_event_midi_t*>(ev);
247 const uint8_t status = midi->data[0] & 0xF0u;
248 const uint8_t channel = midi->data[0] & 0x0Fu;
249 const uint8_t d1 = midi->data[1] & 0x7Fu;
250 const uint8_t d2 = midi->data[2] & 0x7Fu;
251 out.kind = BlockEvent::Kind::Midi;
252 out.midi.channel = channel;
253 switch (status)
254 {
255 case 0x90: // wire convention: velocity 0 means note off
256 out.midi.type = d2 > 0 ? MidiEvent::Type::NoteOn
258 out.midi.note = d1;
259 out.midi.value = static_cast<float>(d2) / 127.0f;
260 break;
261 case 0x80:
262 out.midi.type = MidiEvent::Type::NoteOff;
263 out.midi.note = d1;
264 out.midi.value = static_cast<float>(d2) / 127.0f;
265 break;
266 case 0xA0:
267 out.midi.type = MidiEvent::Type::PolyPressure;
268 out.midi.note = d1;
269 out.midi.value = static_cast<float>(d2) / 127.0f;
270 break;
271 case 0xB0:
272 out.midi.type = MidiEvent::Type::ControlChange;
273 out.midi.note = d1;
274 out.midi.value = static_cast<float>(d2) / 127.0f;
275 break;
276 case 0xD0:
277 out.midi.type = MidiEvent::Type::ChannelPressure;
278 out.midi.value = static_cast<float>(d1) / 127.0f;
279 break;
280 case 0xE0:
281 out.midi.type = MidiEvent::Type::PitchBend;
282 out.midi.value = (static_cast<float>((d2 << 7) | d1) - 8192.0f)
283 / 8192.0f;
284 break;
285 default:
286 return;
287 }
288 }
289 else
290 return;
291 }
292 else
293 return;
294
295 if (count < kMaxBlockEvents) events[count++] = out;
296 else events[kMaxBlockEvents - 1] = out;
297 }
298
299 void collectEvents(const clap_input_events_t* in, uint32_t numSamples,
300 BlockEvent* events, int& count) noexcept
301 {
302 if (in == nullptr) return;
303 const uint32_t n = in->size(in);
304 for (uint32_t i = 0; i < n; ++i)
305 collectEvent(in->get(in, i), numSamples, events, count);
306 }
307
310 bool applyBlockEvent(const BlockEvent& ev, int blockStart) noexcept
311 {
312 switch (ev.kind)
313 {
315 bypass.store(ev.value >= 0.5, std::memory_order_relaxed);
316 return false;
318 if constexpr (HasMidi<P>)
319 {
320 MidiEvent midi = ev.midi;
321 midi.sampleOffset = ev.offset - blockStart;
322 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
323 user.handleMidiEvent(midi);
324 }
325 return false;
327 default:
328 if (const int idx = indexOfParamId(ev.paramId); idx >= 0)
329 {
330 applyNormalized(idx, ev.value);
331 return true;
332 }
333 return false;
334 }
335 }
336
338 void forwardTransport(const clap_event_transport_t* t) noexcept
339 {
340 if constexpr (HasTransport<P>)
341 {
342 if (t == nullptr) return;
343 constexpr double kBeatFactor = static_cast<double>(CLAP_BEATTIME_FACTOR);
344 TransportInfo info {};
345 info.playing = (t->flags & CLAP_TRANSPORT_IS_PLAYING) != 0;
346 info.recording = (t->flags & CLAP_TRANSPORT_IS_RECORDING) != 0;
347 info.looping = (t->flags & CLAP_TRANSPORT_IS_LOOP_ACTIVE) != 0;
348 if ((t->flags & CLAP_TRANSPORT_HAS_TEMPO) != 0)
349 {
350 info.tempoBpm = t->tempo;
351 info.tempoValid = true;
352 }
353 if ((t->flags & CLAP_TRANSPORT_HAS_BEATS_TIMELINE) != 0)
354 {
355 info.ppqPosition = static_cast<double>(t->song_pos_beats) / kBeatFactor;
356 info.barStartPpq = static_cast<double>(t->bar_start) / kBeatFactor;
357 info.positionValid = true;
358 info.loopStartPpq =
359 static_cast<double>(t->loop_start_beats) / kBeatFactor;
360 info.loopEndPpq = static_cast<double>(t->loop_end_beats) / kBeatFactor;
361 info.loopValid = true;
362 }
363 if ((t->flags & CLAP_TRANSPORT_HAS_TIME_SIGNATURE) != 0)
364 {
365 info.timeSigNumerator = t->tsig_num;
366 info.timeSigDenominator = t->tsig_denom;
367 info.timeSigValid = true;
368 }
369 user.setTransport(info);
370 }
371 else
372 (void) t;
373 }
374
375#if defined(DSPARK_PLUGIN_WEBVIEW)
376 // --- UI -> host parameter events ---------------------------------------------
377
378 void pushUiEvent(uint32_t paramId, uint8_t kind, double value) noexcept
379 {
380 const uint32_t tail = uiTail.load(std::memory_order_relaxed);
381 const uint32_t next = (tail + 1) & (kUiQueueSize - 1);
382 if (next == uiHead.load(std::memory_order_acquire)) return; // full: drop
383 uiQueue[tail] = UiEvent { paramId, kind, value };
384 uiTail.store(next, std::memory_order_release);
385 }
386
387 void requestUiFlush() noexcept
388 {
389 // Host schedules process()/flush(), which drains the queue below.
390 if (hostParams != nullptr && host != nullptr)
391 hostParams->request_flush(host);
392 }
393
394 void drainUiEvents(const clap_output_events_t* out) noexcept
395 {
396 if (out == nullptr) return;
397 uint32_t head = uiHead.load(std::memory_order_relaxed);
398 const uint32_t tail = uiTail.load(std::memory_order_acquire);
399 while (head != tail)
400 {
401 const UiEvent& e = uiQueue[head];
402 if (e.kind == kUiValue)
403 {
404 clap_event_param_value_t ev {};
405 ev.header.size = sizeof(ev);
406 ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
407 ev.header.type = CLAP_EVENT_PARAM_VALUE;
408 ev.param_id = e.paramId;
409 ev.note_id = -1;
410 ev.port_index = -1;
411 ev.channel = -1;
412 ev.key = -1;
413 ev.value = e.value;
414 out->try_push(out, &ev.header);
415 }
416 else
417 {
418 clap_event_param_gesture_t ev {};
419 ev.header.size = sizeof(ev);
420 ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
421 ev.header.type = static_cast<uint16_t>(
422 e.kind == kUiGestureBegin ? CLAP_EVENT_PARAM_GESTURE_BEGIN
423 : CLAP_EVENT_PARAM_GESTURE_END);
424 ev.param_id = e.paramId;
425 out->try_push(out, &ev.header);
426 }
427 head = (head + 1) & (kUiQueueSize - 1);
428 }
429 uiHead.store(head, std::memory_order_release);
430 }
431#endif // DSPARK_PLUGIN_WEBVIEW
432
433 // --- clap_plugin_t callbacks ------------------------------------------------
434
435 static bool sInit(const clap_plugin_t* p) noexcept
436 {
437 auto* s = self(p);
438 if (s->host != nullptr)
439 {
440 s->hostParams = static_cast<const clap_host_params_t*>(
441 s->host->get_extension(s->host, CLAP_EXT_PARAMS));
442 s->hostLatency = static_cast<const clap_host_latency_t*>(
443 s->host->get_extension(s->host, CLAP_EXT_LATENCY));
444 if constexpr (kNumPresets > 0)
445 s->hostPresetLoad = static_cast<const clap_host_preset_load_t*>(
446 s->host->get_extension(s->host, CLAP_EXT_PRESET_LOAD));
447 }
448 return true;
449 }
450
451 static void sDestroy(const clap_plugin_t* p) noexcept { delete self(p); }
452
453 static bool sActivate(const clap_plugin_t* p, double sr,
454 uint32_t, uint32_t maxFrames) noexcept
455 {
456 auto* s = self(p);
457 s->sampleRate = sr;
458 s->maxFrames = maxFrames;
459 dspark::AudioSpec spec { sr, static_cast<int>(maxFrames), s->currentChannels };
460 s->user.prepare(spec);
461 if constexpr (HasOfflineMode<P>)
462 s->user.setOfflineRendering(s->offlineRender);
463 for (size_t i = 0; i < kNumParams; ++i)
464 s->user.setParameter(static_cast<int>(i),
465 static_cast<float>(toPlain(P::parameters[i],
466 s->shadow[i].load(std::memory_order_relaxed))));
467 s->dryL.assign(maxFrames, 0.0f);
468 s->dryR.assign(maxFrames, 0.0f);
469 if constexpr (HasSidechain<P>)
470 s->silence.assign(maxFrames, 0.0f);
471 s->bypassMix = s->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
472 if constexpr (HasLatency<P>)
473 s->cachedLatency = s->user.getLatency();
474 s->prepared = true;
475 return true;
476 }
477
478 static void sDeactivate(const clap_plugin_t*) noexcept {}
479 static bool sStartProcessing(const clap_plugin_t*) noexcept { return true; }
480 static void sStopProcessing(const clap_plugin_t*) noexcept {}
481
482 static void sReset(const clap_plugin_t* p) noexcept
483 {
484 auto* s = self(p);
485 if constexpr (HasReset<P>)
486 s->user.reset();
487 s->bypassMix = s->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
488 }
489
490 static clap_process_status sProcess(const clap_plugin_t* p,
491 const clap_process_t* process) noexcept
492 {
493 auto* s = self(p);
494
495 // FTZ/DAZ for the whole callback: DSPark processors guard their own
496 // hot loops, this covers user DSP in hosts that do not set it.
497 dspark::DenormalGuard denormalGuard;
498
499#if defined(DSPARK_PLUGIN_WEBVIEW)
500 s->drainUiEvents(process->out_events);
501#endif
502
503 const uint32_t n = process->frames_count;
504
506 int eventCount = 0;
507 s->collectEvents(process->in_events, n, events, eventCount);
508 sortBlockEvents(events, eventCount);
509
510 bool paramsChanged = false;
511
512 const bool canRender = n > 0 && s->prepared
513 && process->audio_outputs_count >= 1
514 && process->audio_outputs[0].data32 != nullptr
515 && process->audio_outputs[0].channel_count >= 1;
516 if (!canRender)
517 {
518 for (int i = 0; i < eventCount; ++i)
519 paramsChanged |= s->applyBlockEvent(events[i], events[i].offset);
520 if (paramsChanged) s->refreshLatency();
521 return CLAP_PROCESS_CONTINUE;
522 }
523
524 s->forwardTransport(process->transport);
525
526 auto& outPort = process->audio_outputs[0];
527 float** out = outPort.data32;
528 const uint32_t width = static_cast<uint32_t>(s->currentChannels);
529 uint32_t nCh = outPort.channel_count < width
530 ? outPort.channel_count : width;
531 while (nCh > 0 && out[nCh - 1] == nullptr) --nCh; // degenerate hosts
532 if (nCh < 1) return CLAP_PROCESS_CONTINUE;
533
534 const bool haveIn = !kIsInstrument && process->audio_inputs_count >= 1
535 && process->audio_inputs[0].data32 != nullptr;
536 float** in = haveIn ? process->audio_inputs[0].data32 : nullptr;
537
538 if (n > s->dryL.size()) return CLAP_PROCESS_CONTINUE; // oversize block
539
540 // Dry copy for the bypass blend; instruments start cleared (voices
541 // ADD) and bypass toward silence (their dry vectors stay zero).
542 float* dry[2] = { s->dryL.data(), s->dryR.data() };
543 const size_t bytes = sizeof(float) * n;
544 for (uint32_t ch = 0; ch < nCh; ++ch)
545 {
546 if (kIsInstrument)
547 {
548 std::memset(out[ch], 0, bytes);
549 continue;
550 }
551 const float* src = (haveIn && in[ch] != nullptr) ? in[ch] : out[ch];
552 std::memcpy(dry[ch], src, bytes);
553 if (out[ch] != src)
554 std::memcpy(out[ch], src, bytes);
555 }
556
557 // Sidechain: input port 1, pre-allocated silence when not routed.
558 float* scPtrs[2] = { nullptr, nullptr };
559 if constexpr (HasSidechain<P>)
560 {
561 if (n > s->silence.size()) return CLAP_PROCESS_CONTINUE;
562 scPtrs[0] = scPtrs[1] = s->silence.data();
563 if (process->audio_inputs_count >= 2
564 && process->audio_inputs[1].data32 != nullptr)
565 {
566 const auto& scPort = process->audio_inputs[1];
567 const uint32_t scCh = scPort.channel_count < 2
568 ? scPort.channel_count : 2;
569 for (uint32_t ch = 0; ch < scCh; ++ch)
570 if (scPort.data32[ch] != nullptr)
571 scPtrs[ch] = scPort.data32[ch];
572 if (scCh == 1 && scPort.data32[0] != nullptr)
573 scPtrs[1] = scPort.data32[0]; // mono key feeds both ears
574 }
575 }
576
577 // Sub-block processing at quantum-aligned event positions (the
578 // sample-accurate default); opted out, everything applies up front.
579 auto processSegment = [&](int start, int length) noexcept {
580 float* sub[2] = { out[0] + start,
581 nCh > 1 ? out[1] + start : out[0] + start };
582 dspark::AudioBufferView<float> view(sub, static_cast<int>(nCh), length);
583 if constexpr (HasSidechain<P>)
584 {
585 // The key view mirrors the main width (mono main, mono key).
586 float* scSub[2] = { scPtrs[0] + start, scPtrs[1] + start };
588 static_cast<int>(nCh), length);
589 s->user.processBlock(view, scView);
590 }
591 else
592 s->user.processBlock(view);
593 };
594
595 const int total = static_cast<int>(n);
596 int evIdx = 0;
597 if (!sampleAccurateOf<P>())
598 {
599 for (; evIdx < eventCount; ++evIdx)
600 paramsChanged |= s->applyBlockEvent(events[evIdx], 0);
601 processSegment(0, total);
602 }
603 else
604 {
605 int pos = 0;
606 while (pos < total)
607 {
608 while (evIdx < eventCount
609 && (events[evIdx].offset / kAutomationQuantum)
610 * kAutomationQuantum <= pos)
611 paramsChanged |= s->applyBlockEvent(events[evIdx++], pos);
612 int next = total;
613 if (evIdx < eventCount)
614 {
615 const int snapped = (events[evIdx].offset / kAutomationQuantum)
617 if (snapped < next) next = snapped;
618 }
619 if (next <= pos) next = pos + kAutomationQuantum < total
620 ? pos + kAutomationQuantum : total;
621 processSegment(pos, next - pos);
622 pos = next;
623 }
624 for (; evIdx < eventCount; ++evIdx)
625 paramsChanged |= s->applyBlockEvent(events[evIdx], total);
626 }
627
628 const float target = s->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
629 if (s->bypassMix != target || target > 0.0f)
630 {
631 const float step = 1.0f / static_cast<float>(kBypassRampSamples);
632 float mix = s->bypassMix;
633 for (uint32_t i = 0; i < n; ++i)
634 {
635 mix += (target > mix) ? step : ((target < mix) ? -step : 0.0f);
636 mix = mix < 0.0f ? 0.0f : (mix > 1.0f ? 1.0f : mix);
637 for (uint32_t ch = 0; ch < nCh; ++ch)
638 out[ch][i] += (dry[ch][i] - out[ch][i]) * mix;
639 }
640 s->bypassMix = mix;
641 }
642
643 if (paramsChanged) s->refreshLatency();
644 return CLAP_PROCESS_CONTINUE;
645 }
646
647 static const void* sGetExtension(const clap_plugin_t*, const char* id) noexcept
648 {
649 if (std::strcmp(id, CLAP_EXT_AUDIO_PORTS) == 0) return &kAudioPorts;
650 if (std::strcmp(id, CLAP_EXT_PARAMS) == 0) return &kParams;
651 if (std::strcmp(id, CLAP_EXT_STATE) == 0) return &kState;
652 if (std::strcmp(id, CLAP_EXT_LATENCY) == 0) return &kLatency;
653 if (std::strcmp(id, CLAP_EXT_TAIL) == 0) return &kTail;
654 if (channelSupportOf<P>() == ChannelSupport::MonoAndStereo)
655 {
656 if (std::strcmp(id, CLAP_EXT_AUDIO_PORTS_CONFIG) == 0)
657 return &kPortsConfig;
658 if (std::strcmp(id, CLAP_EXT_AUDIO_PORTS_CONFIG_INFO) == 0
659 || std::strcmp(id, CLAP_EXT_AUDIO_PORTS_CONFIG_INFO_COMPAT) == 0)
660 return &kPortsConfigInfo;
661 }
662 if constexpr (HasMidi<P>)
663 if (std::strcmp(id, CLAP_EXT_NOTE_PORTS) == 0) return &kNotePorts;
664 if constexpr (HasOfflineMode<P>)
665 if (std::strcmp(id, CLAP_EXT_RENDER) == 0) return &kRender;
666 if constexpr (kNumPresets > 0)
667 if (std::strcmp(id, CLAP_EXT_PRESET_LOAD) == 0
668 || std::strcmp(id, CLAP_EXT_PRESET_LOAD_COMPAT) == 0)
669 return &kPresetLoad;
670#if defined(DSPARK_PLUGIN_WEBVIEW)
671 if constexpr (HasEditor<P>)
672 {
673 if (webview_ui::Editor<P>::available() && std::strcmp(id, CLAP_EXT_GUI) == 0)
674 return &kGui;
675#if defined(__linux__)
677 && std::strcmp(id, CLAP_EXT_TIMER_SUPPORT) == 0)
678 return &kTimer;
679#endif
680 }
681#endif
682 return nullptr;
683 }
684
685 static void sOnMainThread(const clap_plugin_t* p) noexcept
686 {
687 // Deferred latency notification (the audio thread only flags it).
688 auto* s = self(p);
689 if (s->latencyDirty.exchange(false, std::memory_order_acq_rel)
690 && s->hostLatency != nullptr && s->hostLatency->changed != nullptr)
691 s->hostLatency->changed(s->host);
692 }
693
694 // --- ext: audio ports ---------------------------------------------------------
695
696 static uint32_t sPortCount(const clap_plugin_t*, bool isInput) noexcept
697 {
698 if (isInput)
699 {
700 if (kIsInstrument) return 0;
701 return HasSidechain<P> ? 2 : 1;
702 }
703 return 1;
704 }
705
706 static bool sPortGet(const clap_plugin_t* p, uint32_t index, bool isInput,
707 clap_audio_port_info_t* info) noexcept
708 {
709 if (info == nullptr || index >= sPortCount(p, isInput)) return false;
710 const int width = self(p)->currentChannels;
711 std::memset(info, 0, sizeof(*info));
712 if (isInput && index == 1) // the sidechain: routable, never the main pair
713 {
714 info->id = 2;
715 std::snprintf(info->name, sizeof(info->name), "Sidechain");
716 info->flags = 0;
717 info->channel_count = static_cast<uint32_t>(width);
718 info->port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
719 info->in_place_pair = CLAP_INVALID_ID;
720 return true;
721 }
722 info->id = isInput ? 0 : 1;
723 std::snprintf(info->name, sizeof(info->name), "%s",
724 isInput ? "Input" : "Output");
725 info->flags = CLAP_AUDIO_PORT_IS_MAIN;
726 info->channel_count = static_cast<uint32_t>(width);
727 info->port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
728 info->in_place_pair = kIsInstrument ? CLAP_INVALID_ID
729 : (isInput ? 1u : 0u);
730 return true;
731 }
732
733 inline static const clap_plugin_audio_ports_t kAudioPorts = { &sPortCount, &sPortGet };
734
735 // --- ext: audio ports config (mono / stereo selection) --------------------------
736
737 static uint32_t sConfigCount(const clap_plugin_t*) noexcept { return 2; }
738
739 static void fillConfig(clap_audio_ports_config_t* config, int width) noexcept
740 {
741 std::memset(config, 0, sizeof(*config));
742 config->id = static_cast<clap_id>(width);
743 std::snprintf(config->name, sizeof(config->name), "%s",
744 width == 1 ? "Mono" : "Stereo");
745 config->input_port_count = sPortCount(nullptr, true);
746 config->output_port_count = 1;
747 config->has_main_input = !kIsInstrument;
748 config->main_input_channel_count = static_cast<uint32_t>(width);
749 config->main_input_port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
750 config->has_main_output = true;
751 config->main_output_channel_count = static_cast<uint32_t>(width);
752 config->main_output_port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
753 }
754
755 static bool sConfigGet(const clap_plugin_t*, uint32_t index,
756 clap_audio_ports_config_t* config) noexcept
757 {
758 if (config == nullptr || index >= 2) return false;
759 fillConfig(config, index == 0 ? 1 : 2);
760 return true;
761 }
762
763 static bool sConfigSelect(const clap_plugin_t* p, clap_id configId) noexcept
764 {
765 const int width = static_cast<int>(configId);
766 if (!supportsChannelCount<P>(width)) return false;
767 self(p)->currentChannels = width;
768 return true;
769 }
770
771 inline static const clap_plugin_audio_ports_config_t kPortsConfig = {
773 };
774
775 static clap_id sConfigCurrent(const clap_plugin_t* p) noexcept
776 {
777 return static_cast<clap_id>(self(p)->currentChannels);
778 }
779
780 static bool sConfigInfoGet(const clap_plugin_t* p, clap_id configId,
781 uint32_t portIndex, bool isInput,
782 clap_audio_port_info_t* info) noexcept
783 {
784 const int width = static_cast<int>(configId);
785 if (info == nullptr || !supportsChannelCount<P>(width)
786 || portIndex >= sPortCount(p, isInput))
787 return false;
788 // Same shape as sPortGet, with the width of the asked-about config.
789 std::memset(info, 0, sizeof(*info));
790 if (isInput && portIndex == 1)
791 {
792 info->id = 2;
793 std::snprintf(info->name, sizeof(info->name), "Sidechain");
794 info->channel_count = static_cast<uint32_t>(width);
795 info->port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
796 info->in_place_pair = CLAP_INVALID_ID;
797 return true;
798 }
799 info->id = isInput ? 0 : 1;
800 std::snprintf(info->name, sizeof(info->name), "%s",
801 isInput ? "Input" : "Output");
802 info->flags = CLAP_AUDIO_PORT_IS_MAIN;
803 info->channel_count = static_cast<uint32_t>(width);
804 info->port_type = width == 1 ? CLAP_PORT_MONO : CLAP_PORT_STEREO;
805 info->in_place_pair = kIsInstrument ? CLAP_INVALID_ID
806 : (isInput ? 1u : 0u);
807 return true;
808 }
809
810 inline static const clap_plugin_audio_ports_config_info_t kPortsConfigInfo = {
812 };
813
814 // --- ext: note ports (HasMidi) ----------------------------------------------------
815
816 static uint32_t sNotePortCount(const clap_plugin_t*, bool isInput) noexcept
817 {
818 return (HasMidi<P> && isInput) ? 1 : 0;
819 }
820
821 static bool sNotePortGet(const clap_plugin_t*, uint32_t index, bool isInput,
822 clap_note_port_info_t* info) noexcept
823 {
824 if (info == nullptr || !isInput || index != 0 || !HasMidi<P>) return false;
825 std::memset(info, 0, sizeof(*info));
826 info->id = 10;
827 info->supported_dialects = CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI;
828 info->preferred_dialect = CLAP_NOTE_DIALECT_CLAP;
829 std::snprintf(info->name, sizeof(info->name), "MIDI In");
830 return true;
831 }
832
833 inline static const clap_plugin_note_ports_t kNotePorts = {
835 };
836
837 // --- ext: render (HasOfflineMode) ---------------------------------------------------
838
839 static bool sRenderHardRealtime(const clap_plugin_t*) noexcept { return false; }
840
841 static bool sRenderSet(const clap_plugin_t* p, clap_plugin_render_mode mode) noexcept
842 {
843 auto* s = self(p);
844 s->offlineRender = mode == CLAP_RENDER_OFFLINE;
845 if constexpr (HasOfflineMode<P>)
846 s->user.setOfflineRendering(s->offlineRender);
847 return true;
848 }
849
850 inline static const clap_plugin_render_t kRender = {
852 };
853
854 // --- ext: preset load (factory presets; the DSO is the container) -------------------
855
856 static bool sPresetFromLocation(const clap_plugin_t* p, uint32_t locationKind,
857 const char* location, const char* loadKey) noexcept
858 {
859 if constexpr (kNumPresets > 0)
860 {
861 auto* s = self(p);
862 if (locationKind != CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN
863 || loadKey == nullptr)
864 return false;
865 const int idx = std::atoi(loadKey);
866 if (idx < 0 || idx >= kNumPresets) return false;
867 s->applyFactoryPresetIdx(idx);
868 s->refreshLatency();
869 if (s->hostParams != nullptr && s->hostParams->rescan != nullptr)
870 s->hostParams->rescan(s->host, CLAP_PARAM_RESCAN_VALUES);
871 if (s->hostPresetLoad != nullptr && s->hostPresetLoad->loaded != nullptr)
872 s->hostPresetLoad->loaded(s->host, locationKind, location, loadKey);
873 return true;
874 }
875 else
876 {
877 (void) p;
878 (void) locationKind;
879 (void) location;
880 (void) loadKey;
881 return false;
882 }
883 }
884
885 inline static const clap_plugin_preset_load_t kPresetLoad = {
887 };
888
889 // --- ext: params ----------------------------------------------------------------
890
891 static uint32_t sParamCount(const clap_plugin_t*) noexcept
892 {
893 return static_cast<uint32_t>(kNumParams) + 1; // + Bypass
894 }
895
896 static bool sParamInfo(const clap_plugin_t*, uint32_t index,
897 clap_param_info_t* info) noexcept
898 {
899 if (info == nullptr || index > kNumParams) return false;
900 std::memset(info, 0, sizeof(*info));
901 if (index == kNumParams)
902 {
903 info->id = kBypassParamId;
904 std::snprintf(info->name, sizeof(info->name), "Bypass");
905 info->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_STEPPED
906 | CLAP_PARAM_IS_BYPASS;
907 info->min_value = 0.0;
908 info->max_value = 1.0;
909 info->default_value = 0.0;
910 return true;
911 }
912 const auto& spec = P::parameters[index];
913 info->id = hash32(spec.id);
914 std::snprintf(info->name, sizeof(info->name), "%s", spec.name);
915 info->flags = CLAP_PARAM_IS_AUTOMATABLE;
916 if (spec.steps > 0) info->flags |= CLAP_PARAM_IS_STEPPED;
917 info->min_value = spec.minValue;
918 info->max_value = spec.maxValue;
919 info->default_value = spec.defValue;
920 return true;
921 }
922
923 static bool sParamValue(const clap_plugin_t* p, clap_id id, double* out) noexcept
924 {
925 auto* s = self(p);
926 if (out == nullptr) return false;
927 if (id == kBypassParamId)
928 {
929 *out = s->bypass.load(std::memory_order_relaxed) ? 1.0 : 0.0;
930 return true;
931 }
932 const int idx = indexOfParamId(id);
933 if (idx < 0) return false;
934 *out = toPlain(P::parameters[static_cast<size_t>(idx)],
935 s->shadow[static_cast<size_t>(idx)].load(std::memory_order_relaxed));
936 return true;
937 }
938
939 static bool sParamValueToText(const clap_plugin_t*, clap_id id, double value,
940 char* out, uint32_t size) noexcept
941 {
942 if (out == nullptr || size == 0) return false;
943 if (id == kBypassParamId)
944 {
945 std::snprintf(out, size, "%s", value >= 0.5 ? "On" : "Off");
946 return true;
947 }
948 const int idx = indexOfParamId(id);
949 if (idx < 0) return false;
950 formatValue(P::parameters[static_cast<size_t>(idx)], value, out,
951 static_cast<int>(size));
952 return true;
953 }
954
955 static bool sParamTextToValue(const clap_plugin_t*, clap_id id,
956 const char* text, double* out) noexcept
957 {
958 if (text == nullptr || out == nullptr) return false;
959 const int toggle = parseToggleText(text);
960 if (id == kBypassParamId)
961 {
962 *out = toggle >= 0 ? toggle
963 : (std::strtod(text, nullptr) >= 0.5 ? 1.0 : 0.0);
964 return true;
965 }
966 const int idx = indexOfParamId(id);
967 if (idx < 0) return false;
968 const Param& spec = P::parameters[static_cast<size_t>(idx)];
969 if (spec.steps == 1 && toggle >= 0)
970 *out = toggle != 0 ? spec.maxValue : spec.minValue;
971 else
972 {
973 // min/max ordered so a "nan" string resolves to a bound instead
974 // of passing NaN through to the host.
975 const double v = std::strtod(text, nullptr);
976 *out = std::max(static_cast<double>(spec.minValue),
977 std::min(static_cast<double>(spec.maxValue), v));
978 }
979 return true;
980 }
981
982 static void sParamFlush(const clap_plugin_t* p, const clap_input_events_t* in,
983 const clap_output_events_t* out) noexcept
984 {
985 auto* s = self(p);
987 int eventCount = 0;
988 s->collectEvents(in, 0, events, eventCount);
989 sortBlockEvents(events, eventCount);
990 bool paramsChanged = false;
991 for (int i = 0; i < eventCount; ++i)
992 paramsChanged |= s->applyBlockEvent(events[i], events[i].offset);
993 if (paramsChanged) s->refreshLatency();
994#if defined(DSPARK_PLUGIN_WEBVIEW)
995 s->drainUiEvents(out);
996#else
997 (void) out;
998#endif
999 }
1000
1001 inline static const clap_plugin_params_t kParams = {
1004 };
1005
1006 // --- ext: state -------------------------------------------------------------------
1007
1008 static bool sStateSave(const clap_plugin_t* p, const clap_ostream_t* stream) noexcept
1009 {
1010 auto* s = self(p);
1011 double norm[kNumParams == 0 ? 1 : kNumParams];
1012 for (size_t i = 0; i < kNumParams; ++i)
1013 norm[i] = s->shadow[i].load(std::memory_order_relaxed);
1014 const std::vector<uint8_t> blob = buildState(
1015 s->user, norm, kNumParams,
1016 kNumPresets > 0 ? s->currentProgram.load(std::memory_order_relaxed) : -1,
1017 s->bypass.load(std::memory_order_relaxed) ? 1 : 0);
1018 size_t pos = 0;
1019 while (pos < blob.size())
1020 {
1021 const int64_t put = stream->write(stream, blob.data() + pos,
1022 blob.size() - pos);
1023 if (put <= 0) return false;
1024 pos += static_cast<size_t>(put);
1025 }
1026 return true;
1027 }
1028
1029 static bool sStateLoad(const clap_plugin_t* p, const clap_istream_t* stream) noexcept
1030 {
1031 auto* s = self(p);
1032 std::vector<uint8_t> blob;
1033 uint8_t chunk[4096];
1034 for (;;)
1035 {
1036 const int64_t got = stream->read(stream, chunk, sizeof(chunk));
1037 if (got < 0) return false;
1038 if (got == 0) break;
1039 blob.insert(blob.end(), chunk, chunk + got);
1040 if (blob.size() > (16u << 20)) return false;
1041 }
1042 double norm[kNumParams == 0 ? 1 : kNumParams];
1043 for (size_t i = 0; i < kNumParams; ++i)
1044 norm[i] = s->shadow[i].load(std::memory_order_relaxed);
1045 int program = -1;
1046 int bypassState = -1;
1047 if (!applyState(s->user, blob.data(), blob.size(), norm, &program,
1048 &bypassState))
1049 return false;
1050 for (size_t i = 0; i < kNumParams; ++i)
1051 s->applyNormalized(static_cast<int>(i), norm[i]);
1052 if (kNumPresets > 0 && program >= 0 && program < kNumPresets)
1053 s->currentProgram.store(program, std::memory_order_relaxed);
1054 if (bypassState >= 0)
1055 s->bypass.store(bypassState != 0, std::memory_order_relaxed);
1056 s->refreshLatency(); // restored state may imply a new lookahead
1057 return true;
1058 }
1059
1060 inline static const clap_plugin_state_t kState = { &sStateSave, &sStateLoad };
1061
1062 // --- ext: latency / tail --------------------------------------------------------
1063
1064 static uint32_t sLatencyGet(const clap_plugin_t* p) noexcept
1065 {
1066 return static_cast<uint32_t>(self(p)->cachedLatency);
1067 }
1068
1069 inline static const clap_plugin_latency_t kLatency = { &sLatencyGet };
1070
1071 static uint32_t sTailGet(const clap_plugin_t* p) noexcept
1072 {
1073 auto* s = self(p);
1074 if constexpr (HasTail<P>)
1075 {
1076 const double seconds = s->user.getTailSeconds();
1077 if (!(seconds > 0.0)) return 0; // negative/NaN -> no tail
1078 const double samples = seconds * s->sampleRate;
1079 // CLAP treats anything >= INT32_MAX as an infinite tail; the cap
1080 // also avoids the double->uint32 overflow (UB).
1081 if (samples >= 4294967295.0) return 0xFFFFFFFFu;
1082 return static_cast<uint32_t>(samples);
1083 }
1084 else
1085 {
1086 (void) s;
1087 return 0;
1088 }
1089 }
1090
1091 inline static const clap_plugin_tail_t kTail = { &sTailGet };
1092
1093#if defined(DSPARK_PLUGIN_WEBVIEW)
1094 // --- ext: gui (WebView editor layer) ----------------------------------------------
1095 //
1096 // CLAP hands the parent window AFTER create(), so create() only arms the
1097 // state and set_parent() builds the actual web engine. All [main-thread].
1098
1099 static const char* clapWindowApi() noexcept
1100 {
1101#if defined(_WIN32)
1102 return CLAP_WINDOW_API_WIN32;
1103#elif defined(__APPLE__)
1104 return CLAP_WINDOW_API_COCOA;
1105#else
1106 return CLAP_WINDOW_API_X11;
1107#endif
1108 }
1109
1110 static void cbGuiSetParam(void* context, int index, double plain) noexcept
1111 {
1112 auto* s = static_cast<Plugin*>(context);
1113 const Param& spec = P::parameters[static_cast<size_t>(index)];
1114 const double normalized = toNormalized(spec, plain);
1115 s->applyNormalized(index, normalized);
1116 const double snapped = toPlain(spec, normalized);
1117 const uint32_t id = hash32(spec.id);
1118 if (s->guiEditActive[static_cast<size_t>(index)])
1119 s->pushUiEvent(id, kUiValue, snapped);
1120 else
1121 {
1122 // Edits outside an explicit gesture still get one (host undo).
1123 s->pushUiEvent(id, kUiGestureBegin, 0.0);
1124 s->pushUiEvent(id, kUiValue, snapped);
1125 s->pushUiEvent(id, kUiGestureEnd, 0.0);
1126 }
1127 s->requestUiFlush();
1128 }
1129
1130 static void cbGuiBeginEdit(void* context, int index) noexcept
1131 {
1132 auto* s = static_cast<Plugin*>(context);
1133 s->guiEditActive[static_cast<size_t>(index)] = true;
1134 s->pushUiEvent(hash32(P::parameters[static_cast<size_t>(index)].id),
1135 kUiGestureBegin, 0.0);
1136 s->requestUiFlush();
1137 }
1138
1139 static void cbGuiEndEdit(void* context, int index) noexcept
1140 {
1141 auto* s = static_cast<Plugin*>(context);
1142 s->guiEditActive[static_cast<size_t>(index)] = false;
1143 s->pushUiEvent(hash32(P::parameters[static_cast<size_t>(index)].id),
1144 kUiGestureEnd, 0.0);
1145 s->requestUiFlush();
1146 }
1147
1148 static bool sGuiIsApiSupported(const clap_plugin_t*, const char* api,
1149 bool isFloating) noexcept
1150 {
1151 return !isFloating && api != nullptr && std::strcmp(api, clapWindowApi()) == 0
1153 }
1154
1155 static bool sGuiGetPreferredApi(const clap_plugin_t*, const char** api,
1156 bool* isFloating) noexcept
1157 {
1158 if (api == nullptr || isFloating == nullptr) return false;
1159 *api = clapWindowApi();
1160 *isFloating = false;
1162 }
1163
1164 static bool sGuiCreate(const clap_plugin_t* p, const char* api,
1165 bool isFloating) noexcept
1166 {
1167 auto* s = self(p);
1168 if (!sGuiIsApiSupported(p, api, isFloating)) return false;
1169#if defined(__linux__)
1170 // GTK breathes only when pumped from the host's run loop; without
1171 // timer-support the page would freeze - fall back to generic UI.
1172 s->hostTimer = s->host != nullptr
1173 ? static_cast<const clap_host_timer_support_t*>(
1174 s->host->get_extension(s->host, CLAP_EXT_TIMER_SUPPORT))
1175 : nullptr;
1176 if (s->hostTimer == nullptr || s->hostTimer->register_timer == nullptr
1177 || !s->hostTimer->register_timer(s->host, 33, &s->guiTimerId))
1178 {
1179 webview_ui::debugLog("clap gui create: no host timer-support -> no editor");
1180 s->hostTimer = nullptr;
1181 s->guiTimerId = CLAP_INVALID_ID;
1182 return false;
1183 }
1184#endif
1185 const EditorSize logical = editorSizeOf<P>();
1186 s->guiWidth = static_cast<int>(logical.width * s->guiScale + 0.5);
1187 s->guiHeight = static_cast<int>(logical.height * s->guiScale + 0.5);
1188 s->guiActive = true;
1189 webview_ui::debugLog("clap gui create %dx%d scale=%.2f",
1190 s->guiWidth, s->guiHeight, s->guiScale);
1191 return true;
1192 }
1193
1194 static void sGuiDestroy(const clap_plugin_t* p) noexcept
1195 {
1196 auto* s = self(p);
1197#if defined(__linux__)
1198 if (s->hostTimer != nullptr && s->guiTimerId != CLAP_INVALID_ID)
1199 s->hostTimer->unregister_timer(s->host, s->guiTimerId);
1200 s->hostTimer = nullptr;
1201 s->guiTimerId = CLAP_INVALID_ID;
1202#endif
1203 s->guiEditor.destroy();
1204 s->guiActive = false;
1205 }
1206
1207 static bool sGuiSetScale(const clap_plugin_t* p, double scale) noexcept
1208 {
1209#if defined(__APPLE__)
1210 (void) p; (void) scale;
1211 return false; // cocoa uses logical sizes; scaling is the OS's job
1212#else
1213 auto* s = self(p);
1214 if (!(scale > 0.0)) return false; // NaN included
1215 // Only the physical-size negotiation changes; the page itself never
1216 // zooms (the web engine applies the window DPI to CSS pixels itself).
1217 const double previous = s->guiScale;
1218 s->guiScale = scale;
1219 s->guiWidth = static_cast<int>(s->guiWidth * (scale / previous) + 0.5);
1220 s->guiHeight = static_cast<int>(s->guiHeight * (scale / previous) + 0.5);
1221 s->guiEditor.setBounds(s->guiWidth, s->guiHeight);
1222 return true;
1223#endif
1224 }
1225
1226 static bool sGuiGetSize(const clap_plugin_t* p, uint32_t* width,
1227 uint32_t* height) noexcept
1228 {
1229 auto* s = self(p);
1230 if (width == nullptr || height == nullptr || !s->guiActive) return false;
1231 *width = static_cast<uint32_t>(s->guiWidth);
1232 *height = static_cast<uint32_t>(s->guiHeight);
1233 return true;
1234 }
1235
1236 static bool sGuiCanResize(const clap_plugin_t*) noexcept
1237 {
1238 return editorResizeOf<P>() != EditorResize::Fixed;
1239 }
1240
1241 static bool sGuiGetResizeHints(const clap_plugin_t*,
1242 clap_gui_resize_hints_t* hints) noexcept
1243 {
1244 if (hints == nullptr) return false;
1245 constexpr EditorResize mode = editorResizeOf<P>();
1246 const EditorSize logical = editorSizeOf<P>();
1247 hints->can_resize_horizontally = mode != EditorResize::Fixed;
1248 hints->can_resize_vertically = mode != EditorResize::Fixed;
1249 hints->preserve_aspect_ratio = mode == EditorResize::KeepAspect;
1250 hints->aspect_ratio_width = static_cast<uint32_t>(logical.width);
1251 hints->aspect_ratio_height = static_cast<uint32_t>(logical.height);
1252 return true;
1253 }
1254
1255 static bool sGuiAdjustSize(const clap_plugin_t* p, uint32_t* width,
1256 uint32_t* height) noexcept
1257 {
1258 auto* s = self(p);
1259 if (width == nullptr || height == nullptr) return false;
1260 double w = *width;
1261 double h = *height;
1262 constrainEditorSize<P>(w, h, s->guiScale);
1263 *width = static_cast<uint32_t>(w + 0.5);
1264 *height = static_cast<uint32_t>(h + 0.5);
1265 return true;
1266 }
1267
1268 static bool sGuiSetSize(const clap_plugin_t* p, uint32_t width,
1269 uint32_t height) noexcept
1270 {
1271 auto* s = self(p);
1272 if (!s->guiActive) return false;
1273 // Clamp here too: not every host runs the size through adjust_size.
1274 double w = width;
1275 double h = height;
1276 constrainEditorSize<P>(w, h, s->guiScale);
1277 webview_ui::debugLog("clap gui set_size %ux%u -> %.0fx%.0f", width, height, w, h);
1278 s->guiWidth = static_cast<int>(w + 0.5);
1279 s->guiHeight = static_cast<int>(h + 0.5);
1280 s->guiEditor.setBounds(s->guiWidth, s->guiHeight);
1281 return true;
1282 }
1283
1284 static bool sGuiSetParent(const clap_plugin_t* p,
1285 const clap_window_t* window) noexcept
1286 {
1287 auto* s = self(p);
1288 if (window == nullptr || !s->guiActive) return false;
1289#if defined(__linux__)
1290 // The X11 window id rides the union's dedicated field.
1291 void* parentHandle = reinterpret_cast<void*>(
1292 static_cast<std::uintptr_t>(window->x11));
1293#else
1294 void* parentHandle = window->ptr;
1295#endif
1296 webview_ui::debugLog("clap gui set_parent %p negotiated=%dx%d",
1297 parentHandle, s->guiWidth, s->guiHeight);
1298 const webview_ui::HostCallbacks callbacks {
1299 s, &cbGuiSetParam, &cbGuiBeginEdit, &cbGuiEndEdit
1300 };
1301 if (!s->guiEditor.create(parentHandle, s->shadow, callbacks))
1302 return false;
1303 // Fill whatever box the host actually built (call order differs
1304 // between hosts); fall back to the negotiated size otherwise.
1305 int parentW = 0, parentH = 0;
1306 if (s->guiEditor.queryParentSize(parentW, parentH))
1307 {
1308 s->guiWidth = parentW;
1309 s->guiHeight = parentH;
1310 }
1311 s->guiEditor.setBounds(s->guiWidth, s->guiHeight);
1312 return true;
1313 }
1314
1315 static bool sGuiSetTransient(const clap_plugin_t*, const clap_window_t*) noexcept
1316 {
1317 return false; // embedded only; no floating window mode
1318 }
1319
1320 static void sGuiSuggestTitle(const clap_plugin_t*, const char*) noexcept {}
1321
1322 static bool sGuiShow(const clap_plugin_t* p) noexcept
1323 {
1324 self(p)->guiEditor.setVisible(true);
1325 return true;
1326 }
1327
1328 static bool sGuiHide(const clap_plugin_t* p) noexcept
1329 {
1330 self(p)->guiEditor.setVisible(false);
1331 return true;
1332 }
1333
1334 inline static const clap_plugin_gui_t kGui = {
1335 &sGuiIsApiSupported, &sGuiGetPreferredApi, &sGuiCreate, &sGuiDestroy,
1336 &sGuiSetScale, &sGuiGetSize, &sGuiCanResize, &sGuiGetResizeHints,
1337 &sGuiAdjustSize, &sGuiSetSize, &sGuiSetParent, &sGuiSetTransient,
1338 &sGuiSuggestTitle, &sGuiShow, &sGuiHide
1339 };
1340
1341#if defined(__linux__)
1342
1343 // --- ext: timer-support (drives the GTK main context for the editor) -----------
1344
1345 static void sOnTimer(const clap_plugin_t* p, clap_id) noexcept
1346 {
1347 self(p)->guiEditor.pump();
1348 }
1349
1350 inline static const clap_plugin_timer_support_t kTimer = { &sOnTimer };
1351
1352#endif // __linux__
1353#endif // DSPARK_PLUGIN_WEBVIEW
1354
1355 // --- descriptor & factory ----------------------------------------------------------
1356
1357 static const clap_plugin_descriptor_t* descriptor() noexcept
1358 {
1359 static const char* features[] = {
1360 kIsInstrument ? CLAP_PLUGIN_FEATURE_INSTRUMENT
1361 : CLAP_PLUGIN_FEATURE_AUDIO_EFFECT,
1362 channelSupportOf<P>() == ChannelSupport::MonoOnly
1363 ? CLAP_PLUGIN_FEATURE_MONO : CLAP_PLUGIN_FEATURE_STEREO,
1364 nullptr
1365 };
1366 static const clap_plugin_descriptor_t desc = {
1367 CLAP_VERSION_INIT,
1368 P::descriptor.productId,
1369 P::descriptor.name,
1370 P::descriptor.vendor,
1371 P::descriptor.url,
1372 "", // manual_url
1373 P::descriptor.url, // support_url
1374 P::descriptor.version,
1375 "Built with DSPark", // description
1376 features
1377 };
1378 return &desc;
1379 }
1380
1381 static const clap_plugin_t* create(const clap_host_t* host) noexcept
1382 {
1383 auto* s = new (std::nothrow) Plugin(host);
1384 if (s == nullptr) return nullptr;
1385 s->plugin.desc = descriptor();
1386 s->plugin.plugin_data = s;
1387 s->plugin.init = &sInit;
1388 s->plugin.destroy = &sDestroy;
1389 s->plugin.activate = &sActivate;
1390 s->plugin.deactivate = &sDeactivate;
1391 s->plugin.start_processing = &sStartProcessing;
1392 s->plugin.stop_processing = &sStopProcessing;
1393 s->plugin.reset = &sReset;
1394 s->plugin.process = &sProcess;
1395 s->plugin.get_extension = &sGetExtension;
1396 s->plugin.on_main_thread = &sOnMainThread;
1397 return &s->plugin;
1398 }
1399};
1400
1401template <typename P>
1403{
1404 static uint32_t sCount(const clap_plugin_factory_t*) noexcept { return 1; }
1405
1406 static const clap_plugin_descriptor_t* sDescriptor(const clap_plugin_factory_t*,
1407 uint32_t index) noexcept
1408 {
1409 return index == 0 ? Plugin<P>::descriptor() : nullptr;
1410 }
1411
1412 static const clap_plugin_t* sCreate(const clap_plugin_factory_t*,
1413 const clap_host_t* host,
1414 const char* pluginId) noexcept
1415 {
1416 if (pluginId == nullptr
1417 || std::strcmp(pluginId, P::descriptor.productId) != 0)
1418 return nullptr;
1419 return Plugin<P>::create(host);
1420 }
1421
1422 inline static const clap_plugin_factory_t kFactory = {
1424 };
1425
1426 // --- preset discovery (factory presets live inside the plugin DSO) -------------
1427 //
1428 // The provider declares ONE location of kind PLUGIN and reports each
1429 // factory preset with its table index as the load key; preset-load's
1430 // from_location() applies it. Hosts with a preset browser (e.g. Bitwig)
1431 // index these without instantiating the plugin.
1432
1433 static constexpr int kNumPresets = factoryPresetCountOf<P>();
1434
1435 static const char* providerId() noexcept
1436 {
1437 // Magic static: the discovery factory contract is [thread-safe].
1438 static const std::array<char, 256> id = [] {
1439 std::array<char, 256> buf {};
1440 std::snprintf(buf.data(), buf.size(), "%s.presets",
1441 P::descriptor.productId);
1442 return buf;
1443 }();
1444 return id.data();
1445 }
1446
1447 static const clap_preset_discovery_provider_descriptor_t* providerDescriptor() noexcept
1448 {
1449 static const clap_preset_discovery_provider_descriptor_t desc = {
1450 CLAP_VERSION_INIT, providerId(), "DSPark Factory Presets",
1451 P::descriptor.vendor
1452 };
1453 return &desc;
1454 }
1455
1457 {
1458 clap_preset_discovery_provider_t provider {};
1459 const clap_preset_discovery_indexer_t* indexer = nullptr;
1460 };
1461
1462 static bool sProviderInit(const clap_preset_discovery_provider_t* provider) noexcept
1463 {
1464 const auto* state = static_cast<const ProviderState*>(provider->provider_data);
1465 if (state->indexer == nullptr || state->indexer->declare_location == nullptr)
1466 return false;
1467 clap_preset_discovery_location_t location {};
1468 location.flags = CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT;
1469 location.name = "Factory Presets";
1470 location.kind = CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN;
1471 location.location = nullptr;
1472 return state->indexer->declare_location(state->indexer, &location);
1473 }
1474
1475 static void sProviderDestroy(const clap_preset_discovery_provider_t* provider) noexcept
1476 {
1477 delete static_cast<ProviderState*>(provider->provider_data);
1478 }
1479
1480 static bool sProviderGetMetadata(const clap_preset_discovery_provider_t*,
1481 uint32_t locationKind, const char*,
1482 const clap_preset_discovery_metadata_receiver_t*
1483 receiver) noexcept
1484 {
1485 if constexpr (kNumPresets > 0)
1486 {
1487 if (locationKind != CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN
1488 || receiver == nullptr)
1489 return false;
1490 const clap_universal_plugin_id_t pluginId {
1491 "clap", P::descriptor.productId
1492 };
1493 for (int i = 0; i < kNumPresets; ++i)
1494 {
1495 char key[16];
1496 std::snprintf(key, sizeof(key), "%d", i);
1497 if (!receiver->begin_preset(receiver,
1498 P::factoryPresets[static_cast<size_t>(i)].name, key))
1499 return true; // the indexer asked to stop; not an error
1500 receiver->add_plugin_id(receiver, &pluginId);
1501 receiver->set_flags(receiver,
1502 CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT);
1503 }
1504 return true;
1505 }
1506 else
1507 {
1508 (void) locationKind;
1509 (void) receiver;
1510 return false;
1511 }
1512 }
1513
1514 static const void* sProviderGetExtension(const clap_preset_discovery_provider_t*,
1515 const char*) noexcept
1516 { return nullptr; }
1517
1518 static uint32_t sDiscoveryCount(const clap_preset_discovery_factory_t*) noexcept
1519 { return kNumPresets > 0 ? 1u : 0u; }
1520
1521 static const clap_preset_discovery_provider_descriptor_t* sDiscoveryDescriptor(
1522 const clap_preset_discovery_factory_t*, uint32_t index) noexcept
1523 {
1524 return (kNumPresets > 0 && index == 0) ? providerDescriptor() : nullptr;
1525 }
1526
1527 static const clap_preset_discovery_provider_t* sDiscoveryCreate(
1528 const clap_preset_discovery_factory_t*,
1529 const clap_preset_discovery_indexer_t* indexer,
1530 const char* providerIdAsked) noexcept
1531 {
1532 if (kNumPresets == 0 || indexer == nullptr || providerIdAsked == nullptr
1533 || std::strcmp(providerIdAsked, providerId()) != 0)
1534 return nullptr;
1535 auto* state = new (std::nothrow) ProviderState();
1536 if (state == nullptr) return nullptr;
1537 state->indexer = indexer;
1538 state->provider.desc = providerDescriptor();
1539 state->provider.provider_data = state;
1540 state->provider.init = &sProviderInit;
1541 state->provider.destroy = &sProviderDestroy;
1542 state->provider.get_metadata = &sProviderGetMetadata;
1543 state->provider.get_extension = &sProviderGetExtension;
1544 return &state->provider;
1545 }
1546
1547 inline static const clap_preset_discovery_factory_t kDiscoveryFactory = {
1549 };
1550
1551 static bool sEntryInit(const char*) noexcept { return true; }
1552 static void sEntryDeinit() noexcept {}
1553
1554 static const void* sEntryGetFactory(const char* factoryId) noexcept
1555 {
1556 if (factoryId == nullptr) return nullptr;
1557 if (std::strcmp(factoryId, CLAP_PLUGIN_FACTORY_ID) == 0)
1558 return &kFactory;
1559 if (kNumPresets > 0
1560 && (std::strcmp(factoryId, CLAP_PRESET_DISCOVERY_FACTORY_ID) == 0
1561 || std::strcmp(factoryId, CLAP_PRESET_DISCOVERY_FACTORY_ID_COMPAT) == 0))
1562 return &kDiscoveryFactory;
1563 return nullptr;
1564 }
1565};
1566
1567} // namespace dspark::plugin::clap_backend
1568
1574#define DSPARK_CLAP_PLUGIN(PluginClass) \
1575 extern "C" CLAP_EXPORT const clap_plugin_entry_t clap_entry = { \
1576 CLAP_VERSION_INIT, \
1577 &dspark::plugin::clap_backend::Factory<PluginClass>::sEntryInit, \
1578 &dspark::plugin::clap_backend::Factory<PluginClass>::sEntryDeinit, \
1579 &dspark::plugin::clap_backend::Factory<PluginClass>::sEntryGetFactory \
1580 };
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
One embedded WebView editor instance for plugin class P. Owned by the format backends (the VST3 IPlug...
Custom-editor switch. With hasEditor = false (or absent) hosts show their generic parameter UI....
MIDI capability: void handleMidiEvent(const MidiEvent&) noexcept. Its presence grows a note/event inp...
Render-mode capability: void setOfflineRendering(bool) noexcept. The host flips it to true for non-re...
Sidechain capability. Implement the two-buffer process - the same shape DSPark's own dynamics take:
Transport capability: void setTransport(const TransportInfo&) noexcept. Called on the audio thread,...
constexpr int kBypassRampSamples
Definition DSParkClap.h:70
constexpr uint32_t kBypassParamId
Definition DSParkClap.h:69
void debugLog(const char *format,...) noexcept
EditorResize
How the host may resize the editor window. Declare static constexpr EditorResize editorResize = ....
@ Fixed
The window is exactly editorSize; hosts cannot drag-resize it.
@ KeepAspect
Drag-resizable, locked to the declared width:height ratio.
@ Instrument
Audio generator (MIDI in -> audio out, no audio input).
std::vector< uint8_t > buildState(const P &user, const double *normalized, size_t numParams, int programIndex=-1, int bypassState=-1)
constexpr Param toggle(const char *id, const char *name, bool defaultOn) noexcept
On/off parameter helper.
constexpr double toNormalized(const Param &p, double plain) noexcept
Plain [min, max] -> normalized [0, 1] (steps snap on the way back).
constexpr double toPlain(const Param &p, double normalized) noexcept
Normalized [0, 1] -> plain [min, max], snapped for stepped params.
int parseToggleText(const char *text) noexcept
Case-insensitive "On"/"Off" recognition - the inverse of the toggle display below....
constexpr int kAutomationQuantum
Sub-block grain: automation/event splits snap to this many frames (bounds the per-call overhead; ~0....
constexpr uint32_t hash32(const char *s) noexcept
FNV-1a 32-bit over a C string - the per-parameter host id.
constexpr int kMaxBlockEvents
Hard cap of timestamped events handled per block; the (very unlikely) excess applies at the position ...
void formatValue(const Param &p, double plain, char *out, int outSize) noexcept
Formats a plain value for host display ("3.5 dB", "On", "440 Hz").
void sortBlockEvents(BlockEvent *events, int count) noexcept
Stable insertion sort by offset (tiny N, allocation-free - audio-thread safe; equal offsets keep arri...
@ MonoOnly
Strictly 1->1 (rare; metering/utility plugins).
@ MonoAndStereo
Negotiates 1->1 or 2->2 with the host (default).
bool applyState(P &user, const uint8_t *data, size_t size, double *normalized, int *programIndex=nullptr, int *bypassState=nullptr)
Parses a state blob; fills normalized (defaults pre-loaded by the caller), reports a persisted progra...
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
One timestamped in-block event, normalised across formats: a parameter point, a bypass point,...
int32_t offset
Frame position within the block.
One incoming MIDI-ish event, normalised across formats.
@ NoteOn
note = key, value = velocity 0..1.
@ PolyPressure
note = key, value = pressure 0..1.
@ NoteOff
note = key, value = release velocity 0..1.
@ ControlChange
note = controller number, value = 0..1.
@ ChannelPressure
value = pressure 0..1 (note unused).
@ PitchBend
value = bend -1..+1 (note unused).
int sampleOffset
Frames into the NEXT processBlock call.
One automatable parameter. Plain values run [min, max]; hosts see the normalized [0,...
int steps
0 continuous, 1 toggle, N discrete.
const char * id
Stable text id (state + automation identity).
Host transport snapshot, delivered once per audio block.
bool playing
Transport is rolling.
const clap_preset_discovery_indexer_t * indexer
static bool sProviderInit(const clap_preset_discovery_provider_t *provider) noexcept
static const clap_preset_discovery_provider_descriptor_t * providerDescriptor() noexcept
static const char * providerId() noexcept
static const void * sEntryGetFactory(const char *factoryId) noexcept
static const clap_preset_discovery_provider_t * sDiscoveryCreate(const clap_preset_discovery_factory_t *, const clap_preset_discovery_indexer_t *indexer, const char *providerIdAsked) noexcept
static void sEntryDeinit() noexcept
static uint32_t sDiscoveryCount(const clap_preset_discovery_factory_t *) noexcept
static uint32_t sCount(const clap_plugin_factory_t *) noexcept
static bool sProviderGetMetadata(const clap_preset_discovery_provider_t *, uint32_t locationKind, const char *, const clap_preset_discovery_metadata_receiver_t *receiver) noexcept
static void sProviderDestroy(const clap_preset_discovery_provider_t *provider) noexcept
static bool sEntryInit(const char *) noexcept
static const clap_preset_discovery_provider_descriptor_t * sDiscoveryDescriptor(const clap_preset_discovery_factory_t *, uint32_t index) noexcept
static const clap_plugin_factory_t kFactory
static const void * sProviderGetExtension(const clap_preset_discovery_provider_t *, const char *) noexcept
static const clap_preset_discovery_factory_t kDiscoveryFactory
static const clap_plugin_t * sCreate(const clap_plugin_factory_t *, const clap_host_t *host, const char *pluginId) noexcept
static const clap_plugin_descriptor_t * sDescriptor(const clap_plugin_factory_t *, uint32_t index) noexcept
static uint32_t sConfigCount(const clap_plugin_t *) noexcept
Definition DSParkClap.h:737
static void sStopProcessing(const clap_plugin_t *) noexcept
Definition DSParkClap.h:480
const clap_host_params_t * hostParams
Definition DSParkClap.h:93
static void sReset(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:482
static const clap_plugin_audio_ports_t kAudioPorts
Definition DSParkClap.h:733
static constexpr int kNumPresets
Definition DSParkClap.h:78
static bool sConfigSelect(const clap_plugin_t *p, clap_id configId) noexcept
Definition DSParkClap.h:763
static const clap_plugin_state_t kState
static Plugin * self(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:145
void collectEvent(const clap_event_header_t *ev, uint32_t numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkClap.h:202
const clap_host_preset_load_t * hostPresetLoad
Definition DSParkClap.h:94
static uint32_t sParamCount(const clap_plugin_t *) noexcept
Definition DSParkClap.h:891
static bool sParamValue(const clap_plugin_t *p, clap_id id, double *out) noexcept
Definition DSParkClap.h:923
static uint32_t sNotePortCount(const clap_plugin_t *, bool isInput) noexcept
Definition DSParkClap.h:816
static bool sConfigInfoGet(const clap_plugin_t *p, clap_id configId, uint32_t portIndex, bool isInput, clap_audio_port_info_t *info) noexcept
Definition DSParkClap.h:780
static clap_process_status sProcess(const clap_plugin_t *p, const clap_process_t *process) noexcept
Definition DSParkClap.h:490
void forwardTransport(const clap_event_transport_t *t) noexcept
Definition DSParkClap.h:338
static bool sPortGet(const clap_plugin_t *p, uint32_t index, bool isInput, clap_audio_port_info_t *info) noexcept
Definition DSParkClap.h:706
static const clap_plugin_latency_t kLatency
static bool sActivate(const clap_plugin_t *p, double sr, uint32_t, uint32_t maxFrames) noexcept
Definition DSParkClap.h:453
Plugin(const clap_host_t *h) noexcept
Definition DSParkClap.h:138
static void sOnMainThread(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:685
static bool sRenderSet(const clap_plugin_t *p, clap_plugin_render_mode mode) noexcept
Definition DSParkClap.h:841
static constexpr bool kIsInstrument
Definition DSParkClap.h:76
void applyFactoryPresetIdx(int idx) noexcept
Definition DSParkClap.h:168
static uint32_t sTailGet(const clap_plugin_t *p) noexcept
static bool sNotePortGet(const clap_plugin_t *, uint32_t index, bool isInput, clap_note_port_info_t *info) noexcept
Definition DSParkClap.h:821
static clap_id sConfigCurrent(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:775
static constexpr size_t kNumParams
Definition DSParkClap.h:75
const clap_host_latency_t * hostLatency
Definition DSParkClap.h:92
std::atomic< double > shadow[kNumParams==0 ? 1 :kNumParams]
Definition DSParkClap.h:105
static bool sStateSave(const clap_plugin_t *p, const clap_ostream_t *stream) noexcept
static const clap_plugin_audio_ports_config_t kPortsConfig
Definition DSParkClap.h:771
static void sDestroy(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:451
static bool sParamTextToValue(const clap_plugin_t *, clap_id id, const char *text, double *out) noexcept
Definition DSParkClap.h:955
void applyNormalized(int index, double normalized) noexcept
Definition DSParkClap.h:150
static void sDeactivate(const clap_plugin_t *) noexcept
Definition DSParkClap.h:478
static void sParamFlush(const clap_plugin_t *p, const clap_input_events_t *in, const clap_output_events_t *out) noexcept
Definition DSParkClap.h:982
static const clap_plugin_render_t kRender
Definition DSParkClap.h:850
static const clap_plugin_audio_ports_config_info_t kPortsConfigInfo
Definition DSParkClap.h:810
bool applyBlockEvent(const BlockEvent &ev, int blockStart) noexcept
Definition DSParkClap.h:310
static bool sRenderHardRealtime(const clap_plugin_t *) noexcept
Definition DSParkClap.h:839
static const void * sGetExtension(const clap_plugin_t *, const char *id) noexcept
Definition DSParkClap.h:647
static bool sPresetFromLocation(const clap_plugin_t *p, uint32_t locationKind, const char *location, const char *loadKey) noexcept
Definition DSParkClap.h:856
static bool sParamInfo(const clap_plugin_t *, uint32_t index, clap_param_info_t *info) noexcept
Definition DSParkClap.h:896
static const clap_plugin_descriptor_t * descriptor() noexcept
static bool sStartProcessing(const clap_plugin_t *) noexcept
Definition DSParkClap.h:479
static bool sInit(const clap_plugin_t *p) noexcept
Definition DSParkClap.h:435
static const clap_plugin_t * create(const clap_host_t *host) noexcept
static int indexOfParamId(uint32_t id) noexcept
Definition DSParkClap.h:160
void collectEvents(const clap_input_events_t *in, uint32_t numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkClap.h:299
static uint32_t sLatencyGet(const clap_plugin_t *p) noexcept
static const clap_plugin_preset_load_t kPresetLoad
Definition DSParkClap.h:885
static const clap_plugin_params_t kParams
static const clap_plugin_tail_t kTail
static bool sStateLoad(const clap_plugin_t *p, const clap_istream_t *stream) noexcept
static void fillConfig(clap_audio_ports_config_t *config, int width) noexcept
Definition DSParkClap.h:739
static bool sParamValueToText(const clap_plugin_t *, clap_id id, double value, char *out, uint32_t size) noexcept
Definition DSParkClap.h:939
static bool sConfigGet(const clap_plugin_t *, uint32_t index, clap_audio_ports_config_t *config) noexcept
Definition DSParkClap.h:755
static uint32_t sPortCount(const clap_plugin_t *, bool isInput) noexcept
Definition DSParkClap.h:696
static const clap_plugin_note_ports_t kNotePorts
Definition DSParkClap.h:833