DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
DSParkVst3.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
63#define DSPARK_PLUGIN_VST3_INCLUDED 1
64
65#include "../DSParkPlugin.h"
66
67#include "vst3_c_api.h"
68
69#include <atomic>
70#include <cstdint>
71#include <cstring>
72#include <new>
73#include <thread>
74#include <utility>
75#include <vector>
76
77#if defined(__APPLE__)
78 #include <dispatch/dispatch.h> // libSystem: no extra link flag
79#elif defined(_WIN32)
80// The two user32 calls the UI tick needs, declared exactly as <windows.h>
81// declares them (so either include order compiles) instead of pulling the
82// whole Windows header - and its macros - into every plugin translation unit.
83struct HWND__;
84extern "C" __declspec(dllimport) std::uintptr_t __stdcall SetTimer(
85 HWND__* window, std::uintptr_t id, unsigned int milliseconds,
86 void (__stdcall* callback)(HWND__*, unsigned int, std::uintptr_t, unsigned long));
87extern "C" __declspec(dllimport) int __stdcall KillTimer(HWND__* window, std::uintptr_t id);
88#if defined(_MSC_VER)
89#pragma comment(lib, "user32.lib")
90#endif
91#endif
92
94
95// -- UI-thread tick ------------------------------------------------------------------
96
110{
111public:
112 using Callback = void (*)(void* context) noexcept;
113
114 UiTicker() noexcept = default;
115 UiTicker(const UiTicker&) = delete;
116 UiTicker& operator=(const UiTicker&) = delete;
117 ~UiTicker() { stop(); }
118
121 bool start(Steinberg_FUnknown* hostContext, Callback callback, void* context) noexcept
122 {
123 if (running_.load(std::memory_order_relaxed)) return true;
124 callback_ = callback;
125 context_ = context;
126#if defined(__APPLE__)
127 (void) hostContext;
128 source_ = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
129 dispatch_get_main_queue());
130 if (source_ == nullptr) return false;
131 dispatch_set_context(source_, this);
132 dispatch_source_set_event_handler_f(source_, &onDispatchTick);
133 dispatch_source_set_timer(source_,
134 dispatch_time(DISPATCH_TIME_NOW,
135 static_cast<int64_t>(kPeriodMs) * 1000000),
136 static_cast<uint64_t>(kPeriodMs) * 1000000u,
137 5u * 1000000u);
138 dispatch_resume(source_);
139#elif defined(_WIN32)
140 (void) hostContext;
141 timerId_ = SetTimer(nullptr, 0, kPeriodMs, &onWindowsTick);
142 if (timerId_ == 0) return false;
143 registry().emplace_back(timerId_, this);
144#elif defined(__linux__)
145 if (hostContext == nullptr) return false;
146 void* loop = nullptr;
147 if (hostContext->lpVtbl->queryInterface(hostContext, Steinberg_Linux_IRunLoop_iid,
148 &loop) != Steinberg_kResultOk
149 || loop == nullptr)
150 return false;
151 runLoop_ = static_cast<Steinberg_Linux_IRunLoop*>(loop);
152 if (runLoop_->lpVtbl->registerTimer(
153 runLoop_, reinterpret_cast<Steinberg_Linux_ITimerHandler*>(&timer_),
154 kPeriodMs) != Steinberg_kResultOk)
155 {
156 reinterpret_cast<Steinberg_FUnknown*>(runLoop_)->lpVtbl->release(runLoop_);
157 runLoop_ = nullptr;
158 return false;
159 }
160#else
161 (void) hostContext;
162 return false;
163#endif
164 running_.store(true, std::memory_order_release);
165 return true;
166 }
167
169 void stop() noexcept
170 {
171 if (!running_.load(std::memory_order_relaxed)) return;
172 running_.store(false, std::memory_order_release);
173#if defined(__APPLE__)
174 // Cancel on the main queue's own thread: no handler is mid-flight,
175 // and none is invoked after cancellation.
176 dispatch_source_cancel(source_);
177 dispatch_release(source_);
178 source_ = nullptr;
179#elif defined(_WIN32)
180 KillTimer(nullptr, timerId_);
181 auto& entries = registry();
182 for (size_t i = 0; i < entries.size(); ++i)
183 if (entries[i].first == timerId_)
184 {
185 entries.erase(entries.begin() + static_cast<std::ptrdiff_t>(i));
186 break;
187 }
188 timerId_ = 0;
189#elif defined(__linux__)
190 runLoop_->lpVtbl->unregisterTimer(
191 runLoop_, reinterpret_cast<Steinberg_Linux_ITimerHandler*>(&timer_));
192 reinterpret_cast<Steinberg_FUnknown*>(runLoop_)->lpVtbl->release(runLoop_);
193 runLoop_ = nullptr;
194#endif
195 }
196
197 [[nodiscard]] bool running() const noexcept
198 {
199 return running_.load(std::memory_order_acquire);
200 }
201
202private:
203 static constexpr unsigned int kPeriodMs = 33;
204
205 void tick() noexcept
206 {
207 if (running_.load(std::memory_order_relaxed) && callback_ != nullptr)
208 callback_(context_);
209 }
210
211 std::atomic<bool> running_ { false };
212 Callback callback_ = nullptr;
213 void* context_ = nullptr;
214
215#if defined(__APPLE__)
216 dispatch_source_t source_ = nullptr;
217 static void onDispatchTick(void* self) { static_cast<UiTicker*>(self)->tick(); }
218#elif defined(_WIN32)
219 std::uintptr_t timerId_ = 0;
220 // Thread timers carry no user pointer: map the id back (UI thread only).
221 static std::vector<std::pair<std::uintptr_t, UiTicker*>>& registry()
222 {
223 static std::vector<std::pair<std::uintptr_t, UiTicker*>> entries;
224 return entries;
225 }
226 static void __stdcall onWindowsTick(HWND__*, unsigned int, std::uintptr_t id,
227 unsigned long)
228 {
229 for (const auto& entry : registry())
230 if (entry.first == id)
231 {
232 entry.second->tick();
233 return;
234 }
235 }
236#elif defined(__linux__)
237 // A minimal ITimerHandler (static lifetime: owned by this ticker).
238 struct TimerObject
239 {
240 const Steinberg_Linux_ITimerHandlerVtbl* vtbl;
241 UiTicker* owner;
242 };
243 static Steinberg_tresult SMTG_STDMETHODCALLTYPE timerQuery(void* self_,
244 const Steinberg_TUID iid, void** obj)
245 {
246 if (obj == nullptr) return Steinberg_kInvalidArgument;
247 if (std::memcmp(iid, Steinberg_FUnknown_iid, sizeof(Steinberg_TUID)) == 0
248 || std::memcmp(iid, Steinberg_Linux_ITimerHandler_iid,
249 sizeof(Steinberg_TUID)) == 0)
250 {
251 *obj = self_;
252 return Steinberg_kResultOk;
253 }
254 *obj = nullptr;
255 return Steinberg_kNoInterface;
256 }
257 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE timerRef(void*) { return 1; }
258 static void SMTG_STDMETHODCALLTYPE timerTick(void* self_)
259 {
260 static_cast<TimerObject*>(self_)->owner->tick();
261 }
262 inline static const Steinberg_Linux_ITimerHandlerVtbl kTimerVtbl = {
263 &timerQuery, &timerRef, &timerRef, &timerTick
264 };
265 TimerObject timer_ { &kTimerVtbl, this };
266 Steinberg_Linux_IRunLoop* runLoop_ = nullptr;
267#endif
268};
269
270// -- small helpers -------------------------------------------------------------
271
272inline bool tuidEqual(const Steinberg_TUID a, const Steinberg_TUID b) noexcept
273{
274 return std::memcmp(a, b, sizeof(Steinberg_TUID)) == 0;
275}
276
277inline void asciiToString128(const char* src, Steinberg_Vst_String128 dst) noexcept
278{
279 int i = 0;
280 if (src != nullptr)
281 for (; i < 127 && src[i] != '\0'; ++i)
282 dst[i] = static_cast<Steinberg_char16>(static_cast<unsigned char>(src[i]));
283 dst[i] = 0;
284}
285
286inline void copyAscii(const char* src, char* dst, size_t cap) noexcept
287{
288 std::snprintf(dst, cap, "%s", src ? src : "");
289}
290
292inline bool streamRead(Steinberg_IBStream* s, void* dst, Steinberg_int32 bytes) noexcept
293{
294 auto* p = static_cast<char*>(dst);
295 while (bytes > 0)
296 {
297 Steinberg_int32 got = 0;
298 if (s->lpVtbl->read(s, p, bytes, &got) != Steinberg_kResultOk || got <= 0)
299 return false;
300 p += got;
301 bytes -= got;
302 }
303 return true;
304}
305
306inline bool streamWrite(Steinberg_IBStream* s, const void* src, Steinberg_int32 bytes) noexcept
307{
308 auto* p = static_cast<const char*>(src);
309 while (bytes > 0)
310 {
311 Steinberg_int32 put = 0;
312 if (s->lpVtbl->write(s, const_cast<char*>(p), bytes, &put) != Steinberg_kResultOk
313 || put <= 0)
314 return false;
315 p += put;
316 bytes -= put;
317 }
318 return true;
319}
320
321// -- the plugin object ----------------------------------------------------------
322
323inline constexpr uint32_t kBypassParamId = 0x42595053u; // 'BYPS'
324inline constexpr uint32_t kProgramParamId = 0x5052474Du; // 'PRGM'
325inline constexpr int kBypassRampSamples = 256;
326inline constexpr Steinberg_int32 kPresetProgramListId = 1;
327
328// MIDI controllers travel as proxy parameters in VST3 (the IMidiMapping
329// scheme): one hidden ParamID per (channel, controller). Four controllers
330// cover playable MIDI: mod wheel, sustain, channel pressure, pitch bend.
331inline constexpr uint32_t kMidiProxyBase = 0x4D440000u; // 'MD' marker
332inline constexpr int kMidiProxyControllers[4] = { 1, 64, 128, 129 };
333inline constexpr int kNumMidiProxies = 16 * 4; // channels x controllers
334
335inline constexpr bool isMidiProxyId(uint32_t id) noexcept
336{
337 if ((id & 0xFFFF0000u) != kMidiProxyBase) return false;
338 const uint32_t ctrl = id & 0xFFu;
339 const uint32_t channel = (id >> 8) & 0xFFu;
340 return channel < 16
341 && (ctrl == 1 || ctrl == 64 || ctrl == 128 || ctrl == 129);
342}
343
344inline constexpr uint32_t midiProxyId(int channel, int controller) noexcept
345{
346 return kMidiProxyBase | (static_cast<uint32_t>(channel) << 8)
347 | static_cast<uint32_t>(controller);
348}
349
351inline MidiEvent midiProxyToEvent(uint32_t id, double normalized, int offset) noexcept
352{
353 MidiEvent ev {};
354 ev.channel = static_cast<uint8_t>((id >> 8) & 0x0Fu);
355 ev.sampleOffset = offset;
356 const uint32_t ctrl = id & 0xFFu;
357 if (ctrl == 129)
358 {
360 ev.value = static_cast<float>(normalized * 2.0 - 1.0);
361 }
362 else if (ctrl == 128)
363 {
365 ev.value = static_cast<float>(normalized);
366 }
367 else
368 {
370 ev.note = static_cast<uint8_t>(ctrl);
371 ev.value = static_cast<float>(normalized);
372 }
373 return ev;
374}
375
376template <typename P>
377struct Plugin
378{
379 static constexpr size_t kNumParams = P::parameters.size();
380 static constexpr bool kIsInstrument =
381 P::descriptor.category == Category::Instrument;
382 static constexpr int kNumPresets = factoryPresetCountOf<P>();
383 static_assert(!(kIsInstrument && HasSidechain<P>),
384 "an Instrument has no audio inputs; remove the sidechain "
385 "processBlock or use Category::Fx");
386 static_assert(!kIsInstrument || HasMidi<P>,
387 "an Instrument needs handleMidiEvent (see HasMidi): it has "
388 "no audio input to process");
389 static_assert(paramIdsUnique<P>(),
390 "two parameter ids share a hash32 (or collide with the "
391 "reserved PRGM/BYPS state ids): automation and state would "
392 "cross-wire. Rename one id.");
393
394 // COM lenses - consecutive vtable pointers; queryInterface returns the
395 // address of the matching slot. Standard layout is asserted below. The
396 // last two lenses surface only for the matching capability.
397 const Steinberg_Vst_IComponentVtbl* componentVtbl;
398 const Steinberg_Vst_IAudioProcessorVtbl* processorVtbl;
399 const Steinberg_Vst_IEditControllerVtbl* controllerVtbl;
400 const Steinberg_Vst_IProcessContextRequirementsVtbl* contextReqVtbl;
401 const Steinberg_Vst_IUnitInfoVtbl* unitVtbl;
402 const Steinberg_Vst_IMidiMappingVtbl* midiMapVtbl;
403
404 std::atomic<Steinberg_uint32> refs { 1 };
405
406 P user {};
407 Steinberg_Vst_ProcessSetup setup { 0, 0, 0, 48000.0 };
408 bool prepared = false;
415 std::atomic<int> cachedLatency { 0 };
416 int currentChannels = defaultChannelCount<P>(); // negotiated bus width
417
418 std::atomic<double> shadow[kNumParams == 0 ? 1 : kNumParams] {};
419 std::atomic<double> midiProxyShadow[HasMidi<P> ? kNumMidiProxies : 1] {};
420 std::atomic<bool> bypass { false };
421 std::atomic<int> currentProgram { 0 };
422 float bypassMix = 0.0f; // audio-thread crossfade state
423 std::vector<float> dryL, dryR; // pre-process copy for the bypass blend
424 std::vector<float> silence; // stand-in sidechain when not connected
425 DryDelay dryDelay; // latency-aligned dry path of the bypass
426
427 // Atomic pointer: refreshLatency() reads it from the audio thread while
428 // setComponentHandler writes it from the main thread. Per the VST3
429 // lifecycle the host swaps handlers only while the component is inactive,
430 // so the atomic removes the formally torn read without needing a lock.
431 std::atomic<Steinberg_Vst_IComponentHandler*> handler { nullptr };
432
433 // restartComponent() is a UI-thread call. The thread that creates and
434 // initializes the plugin is the host's UI thread; requests raised on any
435 // other thread (the audio thread, when automation moves the latency)
436 // wait in pendingRestart until the UI tick delivers them.
437 std::atomic<std::thread::id> uiThread { std::this_thread::get_id() };
438 std::atomic<Steinberg_int32> pendingRestart { 0 };
440
441 Plugin() noexcept
442 {
449 for (size_t i = 0; i < kNumParams; ++i)
450 shadow[i].store(toNormalized(P::parameters[i], P::parameters[i].defValue),
451 std::memory_order_relaxed);
452 }
453
454 // --- lens recovery ---------------------------------------------------------
455
456 static Plugin* fromLens(void* iface, int lens) noexcept
457 {
458 return reinterpret_cast<Plugin*>(static_cast<char*>(iface)
459 - static_cast<ptrdiff_t>(lens) * sizeof(void*));
460 }
461
462 void* lensPtr(int lens) noexcept
463 {
464 return reinterpret_cast<char*>(this) + static_cast<ptrdiff_t>(lens) * sizeof(void*);
465 }
466
467 // --- shared FUnknown --------------------------------------------------------
468
469 Steinberg_tresult query(const Steinberg_TUID iid, void** obj) noexcept
470 {
471 if (obj == nullptr) return Steinberg_kInvalidArgument;
472 *obj = nullptr;
473 if (tuidEqual(iid, Steinberg_FUnknown_iid)
474 || tuidEqual(iid, Steinberg_IPluginBase_iid)
475 || tuidEqual(iid, Steinberg_Vst_IComponent_iid))
476 *obj = lensPtr(0);
477 else if (tuidEqual(iid, Steinberg_Vst_IAudioProcessor_iid))
478 *obj = lensPtr(1);
479 else if (tuidEqual(iid, Steinberg_Vst_IEditController_iid))
480 *obj = lensPtr(2);
481 else if (tuidEqual(iid, Steinberg_Vst_IProcessContextRequirements_iid))
482 *obj = lensPtr(3);
483 else if (kNumPresets > 0 && tuidEqual(iid, Steinberg_Vst_IUnitInfo_iid))
484 *obj = lensPtr(4);
485 else if (HasMidi<P> && tuidEqual(iid, Steinberg_Vst_IMidiMapping_iid))
486 *obj = lensPtr(5);
487 if (*obj == nullptr) return Steinberg_kNoInterface;
488 refs.fetch_add(1, std::memory_order_relaxed);
489 return Steinberg_kResultOk;
490 }
491
492 Steinberg_uint32 addRefImpl() noexcept
493 {
494 return refs.fetch_add(1, std::memory_order_relaxed) + 1;
495 }
496
497 Steinberg_uint32 releaseImpl() noexcept
498 {
499 const Steinberg_uint32 left = refs.fetch_sub(1, std::memory_order_acq_rel) - 1;
500 if (left == 0) delete this;
501 return left;
502 }
503
504 template <int Lens>
505 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
506 const Steinberg_TUID iid,
507 void** obj)
508 { return fromLens(self_, Lens)->query(iid, obj); }
509
510 template <int Lens>
511 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void* self_)
512 { return fromLens(self_, Lens)->addRefImpl(); }
513
514 template <int Lens>
515 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void* self_)
516 { return fromLens(self_, Lens)->releaseImpl(); }
517
518 // --- parameter plumbing ------------------------------------------------------
519
520 static int indexOfParamId(Steinberg_Vst_ParamID id) noexcept
521 {
522 for (size_t i = 0; i < kNumParams; ++i)
523 if (hash32(P::parameters[i].id) == id) return static_cast<int>(i);
524 return -1;
525 }
526
527 void applyNormalized(int index, double normalized) noexcept
528 {
529 // Host values are untrusted doubles: a NaN would pass both range
530 // clamps, poison the shadow and reach the user's setter. Ignore it.
531 if (normalized != normalized) return;
532 const auto& spec = P::parameters[static_cast<size_t>(index)];
533 shadow[static_cast<size_t>(index)].store(normalized, std::memory_order_relaxed);
534 user.setParameter(index, static_cast<float>(toPlain(spec, normalized)));
535 }
536
537 void applyAllShadows() noexcept
538 {
539 for (size_t i = 0; i < kNumParams; ++i)
540 user.setParameter(static_cast<int>(i),
541 static_cast<float>(toPlain(P::parameters[i],
542 shadow[i].load(std::memory_order_relaxed))));
543 }
544
546 void applyFactoryPresetIdx(int idx) noexcept
547 {
548 if constexpr (kNumPresets > 0)
549 {
550 if (idx < 0 || idx >= kNumPresets) return;
551 for (size_t i = 0; i < kNumParams; ++i)
552 applyNormalized(static_cast<int>(i), presetNormalized<P>(idx, i));
553 currentProgram.store(idx, std::memory_order_relaxed);
554 }
555 else
556 (void) idx;
557 }
558
566 static int programIndexOf(Steinberg_Vst_ParamValue normalized) noexcept
567 {
568 if constexpr (kNumPresets <= 0)
569 return 0;
570 else
571 return toBoundedInt(normalized * (kNumPresets - 1) + 0.5,
572 0, kNumPresets - 1);
573 }
574
576 {
577 uiTicker.stop();
578 if (auto* h = handler.exchange(nullptr, std::memory_order_relaxed); h != nullptr)
579 reinterpret_cast<Steinberg_FUnknown*>(h)->lpVtbl->release(h);
580 }
581
585 void refreshLatency(Steinberg_int32 extraFlags = 0) noexcept
586 {
587 Steinberg_int32 flags = extraFlags;
588 if constexpr (HasLatency<P>)
589 {
590 const int now = user.getLatency();
591 if (prepared && now != cachedLatency.load(std::memory_order_relaxed))
592 {
593 cachedLatency.store(now, std::memory_order_relaxed);
594 flags |= Steinberg_Vst_RestartFlags_kLatencyChanged;
595 }
596 }
597 if (flags != 0) requestRestart(flags);
598 }
599
600 [[nodiscard]] bool onUiThread() const noexcept
601 {
602 return std::this_thread::get_id() == uiThread.load(std::memory_order_relaxed);
603 }
604
609 void requestRestart(Steinberg_int32 flags) noexcept
610 {
611 if (onUiThread() || !uiTicker.running())
612 {
613 flags |= pendingRestart.exchange(0, std::memory_order_acq_rel);
614 if (auto* h = handler.load(std::memory_order_relaxed); h != nullptr)
615 h->lpVtbl->restartComponent(h, flags);
616 return;
617 }
618 pendingRestart.fetch_or(flags, std::memory_order_release);
619 }
620
622 void deliverPendingRestart() noexcept
623 {
624 if (!onUiThread()) return;
625 const Steinberg_int32 flags = pendingRestart.exchange(0, std::memory_order_acq_rel);
626 if (flags != 0)
627 if (auto* h = handler.load(std::memory_order_relaxed); h != nullptr)
628 h->lpVtbl->restartComponent(h, flags);
629 }
630
631 static void onUiTick(void* self) noexcept
632 {
633 static_cast<Plugin*>(self)->deliverPendingRestart();
634 }
635
638 void attachToHost(Steinberg_FUnknown* context) noexcept
639 {
640 uiThread.store(std::this_thread::get_id(), std::memory_order_relaxed);
641 if constexpr (HasLatency<P> || kNumPresets > 0)
642 uiTicker.start(context, &onUiTick, this);
643 else
644 (void) context;
645 }
646
647 void detachFromHost() noexcept
648 {
650 uiTicker.stop();
651 }
652
653 // --- block event plumbing -------------------------------------------------------
654 //
655 // Every host input lands in ONE timestamped stream (parameter points,
656 // bypass points, note events), sorted and applied either at sub-block
657 // boundaries (sample-accurate, the default) or all up front.
658
661 void collectParameterChanges(Steinberg_Vst_IParameterChanges* changes,
662 Steinberg_int32 numSamples,
663 BlockEvent* events, int& count) noexcept
664 {
665 if (changes == nullptr) return;
666 const Steinberg_int32 queues = changes->lpVtbl->getParameterCount(changes);
667 for (Steinberg_int32 q = 0; q < queues; ++q)
668 {
669 Steinberg_Vst_IParamValueQueue* queue =
670 changes->lpVtbl->getParameterData(changes, q);
671 if (queue == nullptr) continue;
672 const Steinberg_int32 points = queue->lpVtbl->getPointCount(queue);
673 if (points <= 0) continue;
674 const Steinberg_Vst_ParamID id = queue->lpVtbl->getParameterId(queue);
675 const Steinberg_int32 first = sampleAccurateOf<P>() ? 0 : points - 1;
676 for (Steinberg_int32 i = first; i < points; ++i)
677 {
678 Steinberg_int32 offset = 0;
679 Steinberg_Vst_ParamValue value = 0.0;
680 if (queue->lpVtbl->getPoint(queue, i, &offset, &value)
681 != Steinberg_kResultOk)
682 continue;
683 if (offset < 0) offset = 0;
684 if (numSamples > 0 && offset >= numSamples) offset = numSamples - 1;
685 BlockEvent ev {};
686 ev.offset = offset;
689 ev.paramId = id;
690 ev.value = value;
691 if (count < kMaxBlockEvents) events[count++] = ev;
692 else events[kMaxBlockEvents - 1] = ev; // overflow: keep the latest
693 }
694 }
695 }
696
698 void collectInputEvents(Steinberg_Vst_IEventList* list,
699 Steinberg_int32 numSamples,
700 BlockEvent* events, int& count) noexcept
701 {
702 if constexpr (HasMidi<P>)
703 {
704 if (list == nullptr) return;
705 const Steinberg_int32 n = list->lpVtbl->getEventCount(list);
706 for (Steinberg_int32 i = 0; i < n; ++i)
707 {
708 Steinberg_Vst_Event raw {};
709 if (list->lpVtbl->getEvent(list, i, &raw) != Steinberg_kResultOk)
710 continue;
711 MidiEvent midi {};
712 if (raw.type == Steinberg_Vst_Event_EventTypes_kNoteOnEvent)
713 {
715 midi.channel = static_cast<uint8_t>(
716 raw.Steinberg_Vst_Event_noteOn.channel & 0x0F);
717 midi.note = static_cast<uint8_t>(
718 raw.Steinberg_Vst_Event_noteOn.pitch & 0x7F);
719 midi.value = raw.Steinberg_Vst_Event_noteOn.velocity;
720 }
721 else if (raw.type == Steinberg_Vst_Event_EventTypes_kNoteOffEvent)
722 {
723 midi.type = MidiEvent::Type::NoteOff;
724 midi.channel = static_cast<uint8_t>(
725 raw.Steinberg_Vst_Event_noteOff.channel & 0x0F);
726 midi.note = static_cast<uint8_t>(
727 raw.Steinberg_Vst_Event_noteOff.pitch & 0x7F);
728 midi.value = raw.Steinberg_Vst_Event_noteOff.velocity;
729 }
730 else if (raw.type == Steinberg_Vst_Event_EventTypes_kPolyPressureEvent)
731 {
733 midi.channel = static_cast<uint8_t>(
734 raw.Steinberg_Vst_Event_polyPressure.channel & 0x0F);
735 midi.note = static_cast<uint8_t>(
736 raw.Steinberg_Vst_Event_polyPressure.pitch & 0x7F);
737 midi.value = raw.Steinberg_Vst_Event_polyPressure.pressure;
738 }
739 else
740 continue;
741 Steinberg_int32 offset = raw.sampleOffset;
742 if (offset < 0) offset = 0;
743 if (numSamples > 0 && offset >= numSamples) offset = numSamples - 1;
744 BlockEvent ev {};
745 ev.offset = offset;
746 ev.kind = BlockEvent::Kind::Midi;
747 ev.midi = midi;
748 if (count < kMaxBlockEvents) events[count++] = ev;
749 else events[kMaxBlockEvents - 1] = ev;
750 }
751 }
752 else
753 {
754 (void) list;
755 (void) numSamples;
756 (void) events;
757 (void) count;
758 }
759 }
760
765 bool applyBlockEvent(const BlockEvent& ev, int blockStart,
766 bool& programChanged) noexcept
767 {
768 switch (ev.kind)
769 {
771 bypass.store(ev.value >= 0.5, std::memory_order_relaxed);
772 return false;
774 if constexpr (HasMidi<P>)
775 {
776 MidiEvent midi = ev.midi;
777 midi.sampleOffset = ev.offset - blockStart;
778 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
779 user.handleMidiEvent(midi);
780 }
781 return false;
783 default:
784 if constexpr (kNumPresets > 0)
785 {
786 if (ev.paramId == kProgramParamId)
787 {
789 programChanged = true;
790 return true;
791 }
792 }
793 if constexpr (HasMidi<P>)
794 {
795 if (isMidiProxyId(ev.paramId))
796 {
797 const int slot = midiProxySlot(ev.paramId);
798 midiProxyShadow[static_cast<size_t>(slot)].store(
799 ev.value, std::memory_order_relaxed);
800 MidiEvent midi = midiProxyToEvent(ev.paramId, ev.value,
801 ev.offset - blockStart);
802 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
803 user.handleMidiEvent(midi);
804 return false;
805 }
806 }
807 if (const int idx = indexOfParamId(ev.paramId); idx >= 0)
808 {
809 applyNormalized(idx, ev.value);
810 return true;
811 }
812 return false;
813 }
814 }
815
817 static int midiProxySlot(uint32_t id) noexcept
818 {
819 const int channel = static_cast<int>((id >> 8) & 0x0Fu);
820 const uint32_t ctrl = id & 0xFFu;
821 const int slot = ctrl == 1 ? 0 : (ctrl == 64 ? 1 : (ctrl == 128 ? 2 : 3));
822 return channel * 4 + slot;
823 }
824
825 // ==========================================================================
826 // Lens 0 - IComponent (+ IPluginBase)
827 // ==========================================================================
828
829 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sInitialize(void* self_,
830 Steinberg_FUnknown* context)
831 {
832 fromLens(self_, 0)->attachToHost(context);
833 return Steinberg_kResultOk;
834 }
835
836 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sTerminate(void* self_)
837 {
838 fromLens(self_, 0)->detachFromHost();
839 return Steinberg_kResultOk;
840 }
841
842 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetControllerClassId(void*, Steinberg_TUID)
843 { return Steinberg_kResultFalse; } // single component: no separate controller
844
845 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetIoMode(void*, Steinberg_Vst_IoMode)
846 { return Steinberg_kResultOk; }
847
850 static Steinberg_int32 numInputBuses() noexcept
851 {
852 if (kIsInstrument) return 0;
853 return HasSidechain<P> ? 2 : 1;
854 }
855
856 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetBusCount(void*,
857 Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir)
858 {
859 if (type == Steinberg_Vst_MediaTypes_kEvent)
860 return (HasMidi<P> && dir == Steinberg_Vst_BusDirections_kInput) ? 1 : 0;
861 if (type != Steinberg_Vst_MediaTypes_kAudio) return 0;
862 return dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
863 }
864
865 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusInfo(void* self_,
866 Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir,
867 Steinberg_int32 index, Steinberg_Vst_BusInfo* bus)
868 {
869 if (bus == nullptr) return Steinberg_kInvalidArgument;
870 if (type == Steinberg_Vst_MediaTypes_kEvent)
871 {
872 if (!HasMidi<P> || dir != Steinberg_Vst_BusDirections_kInput
873 || index != 0)
874 return Steinberg_kInvalidArgument;
875 bus->mediaType = Steinberg_Vst_MediaTypes_kEvent;
876 bus->direction = dir;
877 bus->channelCount = 16;
878 bus->busType = Steinberg_Vst_BusTypes_kMain;
879 bus->flags = Steinberg_Vst_BusInfo_BusFlags_kDefaultActive;
880 asciiToString128("MIDI In", bus->name);
881 return Steinberg_kResultOk;
882 }
883 const Steinberg_int32 count =
884 dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
885 if (type != Steinberg_Vst_MediaTypes_kAudio || index < 0 || index >= count)
886 return Steinberg_kInvalidArgument;
887 bus->mediaType = Steinberg_Vst_MediaTypes_kAudio;
888 bus->direction = dir;
889 bus->channelCount = fromLens(self_, 0)->currentChannels;
890 bus->busType = index == 0 ? Steinberg_Vst_BusTypes_kMain
891 : Steinberg_Vst_BusTypes_kAux;
892 bus->flags = Steinberg_Vst_BusInfo_BusFlags_kDefaultActive;
893 asciiToString128(index == 1 ? "Sidechain"
894 : (dir == Steinberg_Vst_BusDirections_kInput ? "Input"
895 : "Output"),
896 bus->name);
897 return Steinberg_kResultOk;
898 }
899
900 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetRoutingInfo(void*,
901 Steinberg_Vst_RoutingInfo*, Steinberg_Vst_RoutingInfo*)
902 { return Steinberg_kNotImplemented; }
903
904 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sActivateBus(void*,
905 Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32,
906 Steinberg_TBool)
907 { return Steinberg_kResultOk; }
908
909 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetActive(void* self_, Steinberg_TBool state)
910 {
911 auto* p = fromLens(self_, 0);
912 if (state)
913 {
914 const int maxBlock = p->setup.maxSamplesPerBlock > 0
915 ? p->setup.maxSamplesPerBlock : 512;
916 const double sr = p->setup.sampleRate > 0.0 ? p->setup.sampleRate : 48000.0;
917 dspark::AudioSpec spec { sr, maxBlock, p->currentChannels };
918 p->user.prepare(spec);
919 if constexpr (HasOfflineMode<P>)
920 p->user.setOfflineRendering(
921 p->setup.processMode == Steinberg_Vst_ProcessModes_kOffline);
922 p->applyAllShadows();
923 p->dryL.assign(static_cast<size_t>(maxBlock), 0.0f);
924 p->dryR.assign(static_cast<size_t>(maxBlock), 0.0f);
925 if constexpr (HasSidechain<P>)
926 p->silence.assign(static_cast<size_t>(maxBlock), 0.0f);
927 p->bypassMix = p->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
928 if constexpr (HasLatency<P>)
929 {
930 const int latency = p->user.getLatency();
931 p->cachedLatency.store(latency, std::memory_order_relaxed);
932 p->dryDelay.prepare(std::max(2 * latency, kDryDelayMinCapacity), maxBlock);
933 }
934 p->prepared = true;
935 }
936 return Steinberg_kResultOk;
937 }
938
939 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentSetState(void* self_,
940 Steinberg_IBStream* state)
941 {
942 auto* p = fromLens(self_, 0);
943 if (state == nullptr) return Steinberg_kInvalidArgument;
944 // Read whatever the host hands us (chunked); cap defensively at 16 MB.
945 std::vector<uint8_t> blob;
946 char chunk[4096];
947 for (;;)
948 {
949 Steinberg_int32 got = 0;
950 if (state->lpVtbl->read(state, chunk, static_cast<Steinberg_int32>(sizeof(chunk)),
951 &got) != Steinberg_kResultOk || got <= 0)
952 break;
953 blob.insert(blob.end(), chunk, chunk + got);
954 if (blob.size() > (16u << 20)) return Steinberg_kResultFalse;
955 }
956 double norm[kNumParams == 0 ? 1 : kNumParams];
957 for (size_t i = 0; i < kNumParams; ++i)
958 norm[i] = p->shadow[i].load(std::memory_order_relaxed);
959 int program = -1;
960 int bypassState = -1;
961 if (!applyState(p->user, blob.data(), blob.size(), norm, &program,
962 &bypassState))
963 return Steinberg_kResultFalse;
964 for (size_t i = 0; i < kNumParams; ++i)
965 p->applyNormalized(static_cast<int>(i), norm[i]);
966 if (kNumPresets > 0 && program >= 0 && program < kNumPresets)
967 p->currentProgram.store(program, std::memory_order_relaxed);
968 if (bypassState >= 0)
969 p->bypass.store(bypassState != 0, std::memory_order_relaxed);
970 p->refreshLatency(); // restored state may imply a new lookahead
971 return Steinberg_kResultOk;
972 }
973
974 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentGetState(void* self_,
975 Steinberg_IBStream* state)
976 {
977 auto* p = fromLens(self_, 0);
978 if (state == nullptr) return Steinberg_kInvalidArgument;
979 double norm[kNumParams == 0 ? 1 : kNumParams];
980 for (size_t i = 0; i < kNumParams; ++i)
981 norm[i] = p->shadow[i].load(std::memory_order_relaxed);
982 const std::vector<uint8_t> blob = buildState(
983 p->user, norm, kNumParams,
984 kNumPresets > 0 ? p->currentProgram.load(std::memory_order_relaxed) : -1,
985 p->bypass.load(std::memory_order_relaxed) ? 1 : 0);
986 return streamWrite(state, blob.data(), static_cast<Steinberg_int32>(blob.size()))
987 ? Steinberg_kResultOk : Steinberg_kResultFalse;
988 }
989
990 inline static const Steinberg_Vst_IComponentVtbl kComponentVtbl = {
991 &sQuery<0>, &sAddRef<0>, &sRelease<0>,
996 };
997
998 // ==========================================================================
999 // Lens 1 - IAudioProcessor
1000 // ==========================================================================
1001
1004 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetBusArrangements(void* self_,
1005 Steinberg_Vst_SpeakerArrangement* inputs, Steinberg_int32 numIns,
1006 Steinberg_Vst_SpeakerArrangement* outputs, Steinberg_int32 numOuts)
1007 {
1008 if (numIns != numInputBuses() || numOuts != 1
1009 || (numIns > 0 && inputs == nullptr) || outputs == nullptr)
1010 return Steinberg_kResultFalse;
1011 const Steinberg_Vst_SpeakerArrangement want = outputs[0];
1012 int channels = 0;
1013 if (want == Steinberg_Vst_SpeakerArr_kMono) channels = 1;
1014 else if (want == Steinberg_Vst_SpeakerArr_kStereo) channels = 2;
1015 if (channels == 0 || !supportsChannelCount<P>(channels))
1016 return Steinberg_kResultFalse;
1017 for (Steinberg_int32 i = 0; i < numIns; ++i)
1018 if (inputs[i] != want)
1019 return Steinberg_kResultFalse;
1020 fromLens(self_, 1)->currentChannels = channels;
1021 return Steinberg_kResultTrue;
1022 }
1023
1024 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusArrangement(void* self_,
1025 Steinberg_Vst_BusDirection dir, Steinberg_int32 index,
1026 Steinberg_Vst_SpeakerArrangement* arr)
1027 {
1028 const Steinberg_int32 count =
1029 dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
1030 if (index < 0 || index >= count || arr == nullptr)
1031 return Steinberg_kInvalidArgument;
1032 *arr = fromLens(self_, 1)->currentChannels == 1
1033 ? Steinberg_Vst_SpeakerArr_kMono : Steinberg_Vst_SpeakerArr_kStereo;
1034 return Steinberg_kResultOk;
1035 }
1036
1037 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanProcessSampleSize(void*,
1038 Steinberg_int32 size)
1039 {
1040 return size == Steinberg_Vst_SymbolicSampleSizes_kSample32
1041 ? Steinberg_kResultTrue : Steinberg_kResultFalse;
1042 }
1043
1044 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetLatencySamples(void* self_)
1045 {
1046 return static_cast<Steinberg_uint32>(
1047 fromLens(self_, 1)->cachedLatency.load(std::memory_order_relaxed));
1048 }
1049
1050 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetupProcessing(void* self_,
1051 Steinberg_Vst_ProcessSetup* setup)
1052 {
1053 if (setup == nullptr) return Steinberg_kInvalidArgument;
1054 fromLens(self_, 1)->setup = *setup;
1055 return Steinberg_kResultOk;
1056 }
1057
1058 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetProcessing(void*, Steinberg_TBool)
1059 { return Steinberg_kResultOk; }
1060
1062 void forwardTransport(const Steinberg_Vst_ProcessContext* ctx) noexcept
1063 {
1064 if constexpr (HasTransport<P>)
1065 {
1066 if (ctx == nullptr) return;
1067 TransportInfo t {};
1068 const Steinberg_uint32 s = ctx->state;
1069 t.playing = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kPlaying) != 0;
1070 t.recording = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kRecording) != 0;
1071 t.looping = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kCycleActive) != 0;
1072 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kTempoValid) != 0)
1073 {
1074 t.tempoBpm = ctx->tempo;
1075 t.tempoValid = true;
1076 }
1077 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kProjectTimeMusicValid) != 0)
1078 {
1079 t.ppqPosition = ctx->projectTimeMusic;
1080 t.positionValid = true;
1081 }
1082 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kBarPositionValid) != 0)
1083 t.barStartPpq = ctx->barPositionMusic;
1084 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kTimeSigValid) != 0)
1085 {
1086 t.timeSigNumerator = ctx->timeSigNumerator;
1087 t.timeSigDenominator = ctx->timeSigDenominator;
1088 t.timeSigValid = true;
1089 }
1090 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kCycleValid) != 0)
1091 {
1092 t.loopStartPpq = ctx->cycleStartMusic;
1093 t.loopEndPpq = ctx->cycleEndMusic;
1094 t.loopValid = true;
1095 }
1096 user.setTransport(t);
1097 }
1098 else
1099 (void) ctx;
1100 }
1101
1102 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sProcess(void* self_,
1103 Steinberg_Vst_ProcessData* data)
1104 {
1105 auto* p = fromLens(self_, 1);
1106 if (data == nullptr) return Steinberg_kInvalidArgument;
1107
1108 // FTZ/DAZ for the whole callback: DSPark processors guard their own
1109 // hot loops, this covers user DSP in hosts that do not set it.
1110 dspark::DenormalGuard denormalGuard;
1111
1112 const Steinberg_int32 n = data->numSamples;
1113
1114 // One timestamped stream for everything the host handed us.
1116 int eventCount = 0;
1117 p->collectParameterChanges(data->inputParameterChanges, n, events, eventCount);
1118 p->collectInputEvents(data->inputEvents, n, events, eventCount);
1119 sortBlockEvents(events, eventCount);
1120
1121 bool paramsChanged = false;
1122 bool programChanged = false;
1123
1124 const bool canRender = n > 0
1125 && data->symbolicSampleSize == Steinberg_Vst_SymbolicSampleSizes_kSample32
1126 && data->numOutputs >= 1 && data->outputs != nullptr && p->prepared;
1127 if (!canRender)
1128 {
1129 // Parameter flush (n == 0) or unrenderable block: still apply
1130 // every event so automation/state never desynchronises.
1131 for (int i = 0; i < eventCount; ++i)
1132 paramsChanged |= p->applyBlockEvent(events[i], events[i].offset,
1133 programChanged);
1134 if (paramsChanged || programChanged)
1135 p->refreshLatency(programChanged
1136 ? Steinberg_Vst_RestartFlags_kParamValuesChanged : 0);
1137 if (n <= 0) return Steinberg_kResultOk;
1138 return data->symbolicSampleSize
1139 == Steinberg_Vst_SymbolicSampleSizes_kSample32
1140 ? Steinberg_kResultOk : Steinberg_kResultFalse;
1141 }
1142
1143 p->forwardTransport(data->processContext);
1144
1145 auto& outBus = data->outputs[0];
1146 float** out = outBus.Steinberg_Vst_AudioBusBuffers_channelBuffers32;
1147 if (out == nullptr || outBus.numChannels < 1) return Steinberg_kResultOk;
1148 const int width = p->currentChannels;
1149 int nCh = outBus.numChannels < width ? outBus.numChannels : width;
1150 while (nCh > 0 && out[nCh - 1] == nullptr) --nCh; // degenerate hosts
1151 if (nCh < 1) return Steinberg_kResultOk;
1152
1153 const bool haveIn = !kIsInstrument
1154 && data->numInputs >= 1 && data->inputs != nullptr
1155 && data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32 != nullptr
1156 && data->inputs[0].numChannels >= 1;
1157 float** in = haveIn
1158 ? data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32 : nullptr;
1159 const int inCh = haveIn ? static_cast<int>(data->inputs[0].numChannels) : 0;
1160
1161 // Sidechain: aux bus 1, pre-allocated silence when nothing is
1162 // routed (same frame count, no branches user-side). Read-only.
1163 float* scHost[2] = { nullptr, nullptr };
1164 if constexpr (HasSidechain<P>)
1165 {
1166 if (data->numInputs >= 2 && data->inputs != nullptr
1167 && data->inputs[1].Steinberg_Vst_AudioBusBuffers_channelBuffers32
1168 != nullptr)
1169 {
1170 const auto& scBus = data->inputs[1];
1171 float** sc = scBus.Steinberg_Vst_AudioBusBuffers_channelBuffers32;
1172 const int scCh = scBus.numChannels < 2 ? scBus.numChannels : 2;
1173 for (int ch = 0; ch < scCh; ++ch)
1174 scHost[ch] = sc[ch];
1175 if (scCh == 1)
1176 scHost[1] = sc[0]; // mono key feeds both detector ears
1177 }
1178 }
1179
1180 // A block longer than the prepared maximum (hosts that ignore
1181 // maxSamplesPerBlock) runs as consecutive chunks of at most that
1182 // size: the user DSP never sees more frames than it prepared for.
1183 const int chunkMax = static_cast<int>(p->dryL.size());
1184 if (chunkMax < 1) return Steinberg_kResultOk;
1185 float* dry[2] = { p->dryL.data(), p->dryR.data() };
1186 int evIdx = 0;
1187
1188 for (int c0 = 0; c0 < n; c0 += chunkMax)
1189 {
1190 const int len = n - c0 < chunkMax ? n - c0 : chunkMax;
1191 const int cEnd = c0 + len;
1192
1193 // Keep the dry signal for the bypass blend, then process the
1194 // output buffers in place (copying input over first when
1195 // distinct). An instrument has no input: its output starts
1196 // cleared (voices ADD) and its bypass blends toward silence (the
1197 // dry vectors stay zero). Output channels past a narrower input
1198 // bus (a host that ignored the arrangement) repeat its last one.
1199 const size_t bytes = sizeof(float) * static_cast<size_t>(len);
1200 for (int ch = 0; ch < nCh; ++ch)
1201 {
1202 if (kIsInstrument)
1203 {
1204 std::memset(out[ch] + c0, 0, bytes);
1205 continue;
1206 }
1207 const float* src = out[ch];
1208 if (haveIn)
1209 {
1210 const float* s = in[ch < inCh ? ch : inCh - 1];
1211 if (s != nullptr) src = s;
1212 }
1213 std::memcpy(dry[ch], src + c0, bytes);
1214 if (out[ch] != src)
1215 std::memcpy(out[ch] + c0, src + c0, bytes);
1216 }
1217 if constexpr (HasLatency<P> && !kIsInstrument)
1218 p->dryDelay.process(dry, nCh, len,
1219 p->cachedLatency.load(std::memory_order_relaxed));
1220
1221 float* scChunk[2] = { nullptr, nullptr };
1222 if constexpr (HasSidechain<P>)
1223 for (int ch = 0; ch < 2; ++ch)
1224 scChunk[ch] = scHost[ch] != nullptr ? scHost[ch] + c0
1225 : p->silence.data();
1226
1227 // Process in sub-blocks split at quantum-aligned event positions
1228 // (sample-accurate default); without splitting, apply the
1229 // chunk's events up front and run it in one call.
1230 auto processSegment = [&](int start, int length) noexcept {
1231 float* sub[2] = { out[0] + start,
1232 nCh > 1 ? out[1] + start : out[0] + start };
1233 dspark::AudioBufferView<float> view(sub, nCh, length);
1234 if constexpr (HasSidechain<P>)
1235 {
1236 // The key view mirrors the main width (mono main, mono key).
1237 float* scSub[2] = { scChunk[0] + (start - c0),
1238 scChunk[1] + (start - c0) };
1239 dspark::AudioBufferView<float> scView(scSub, nCh, length);
1240 p->user.processBlock(view, scView);
1241 }
1242 else
1243 p->user.processBlock(view);
1244 };
1245
1246 if (!sampleAccurateOf<P>())
1247 {
1248 for (; evIdx < eventCount && events[evIdx].offset < cEnd; ++evIdx)
1249 paramsChanged |= p->applyBlockEvent(events[evIdx], c0, programChanged);
1250 processSegment(c0, len);
1251 }
1252 else
1253 {
1254 int pos = c0;
1255 while (pos < cEnd)
1256 {
1257 while (evIdx < eventCount
1258 && (events[evIdx].offset / kAutomationQuantum)
1259 * kAutomationQuantum <= pos)
1260 paramsChanged |= p->applyBlockEvent(events[evIdx++], pos,
1261 programChanged);
1262 int next = cEnd;
1263 if (evIdx < eventCount)
1264 {
1265 const int snapped = (events[evIdx].offset / kAutomationQuantum)
1267 if (snapped < next) next = snapped;
1268 }
1269 if (next <= pos) next = pos + kAutomationQuantum < cEnd
1270 ? pos + kAutomationQuantum : cEnd;
1271 processSegment(pos, next - pos);
1272 pos = next;
1273 }
1274 }
1275
1276 // Soft bypass: short linear crossfade toward the (latency-
1277 // aligned) dry signal.
1278 const float target = p->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
1279 if (p->bypassMix != target || target > 0.0f)
1280 {
1281 const float step = 1.0f / static_cast<float>(kBypassRampSamples);
1282 float mix = p->bypassMix;
1283 for (int i = 0; i < len; ++i)
1284 {
1285 mix += (target > mix) ? step : ((target < mix) ? -step : 0.0f);
1286 mix = mix < 0.0f ? 0.0f : (mix > 1.0f ? 1.0f : mix);
1287 for (int ch = 0; ch < nCh; ++ch)
1288 out[ch][c0 + i] += (dry[ch][i] - out[ch][c0 + i]) * mix;
1289 }
1290 p->bypassMix = mix;
1291 }
1292 }
1293 for (; evIdx < eventCount; ++evIdx) // safety: events at block end
1294 paramsChanged |= p->applyBlockEvent(events[evIdx], n, programChanged);
1295
1296 if (paramsChanged || programChanged)
1297 p->refreshLatency(programChanged
1298 ? Steinberg_Vst_RestartFlags_kParamValuesChanged : 0);
1299
1300 outBus.silenceFlags = 0;
1301 return Steinberg_kResultOk;
1302 }
1303
1304 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetTailSamples(void* self_)
1305 {
1306 auto* p = fromLens(self_, 1);
1307 if constexpr (HasTail<P>)
1308 {
1309 const double seconds = p->user.getTailSeconds();
1310 if (!(seconds > 0.0)) return 0; // negative/NaN -> no tail
1311 const double samples = seconds
1312 * (p->setup.sampleRate > 0 ? p->setup.sampleRate : 48000.0);
1313 // Infinite or absurd tails map to kInfiniteTail instead of a
1314 // double->uint32 overflow (UB).
1315 if (samples >= 4294967295.0) return 0xFFFFFFFFu;
1316 return static_cast<Steinberg_uint32>(samples);
1317 }
1318 else
1319 {
1320 (void) p;
1321 return 0;
1322 }
1323 }
1324
1325 inline static const Steinberg_Vst_IAudioProcessorVtbl kProcessorVtbl = {
1326 &sQuery<1>, &sAddRef<1>, &sRelease<1>,
1330 };
1331
1332 // ==========================================================================
1333 // Lens 2 - IEditController (+ IPluginBase)
1334 // ==========================================================================
1335
1336 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlInitialize(void* self_,
1337 Steinberg_FUnknown* context)
1338 {
1339 fromLens(self_, 2)->attachToHost(context);
1340 return Steinberg_kResultOk;
1341 }
1342
1343 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlTerminate(void* self_)
1344 {
1345 fromLens(self_, 2)->detachFromHost();
1346 return Steinberg_kResultOk;
1347 }
1348
1349 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentState(void* self_,
1350 Steinberg_IBStream* state)
1351 {
1352 // Single component: identical to IComponent::setState (lens offset!).
1353 return sComponentSetState(fromLens(self_, 2)->lensPtr(0), state);
1354 }
1355
1356 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlSetState(void*, Steinberg_IBStream*)
1357 { return Steinberg_kResultOk; } // no controller-only state
1358
1359 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlGetState(void*, Steinberg_IBStream*)
1360 { return Steinberg_kResultOk; }
1361
1362 // Parameter index layout: [0, N) the user table, N the bypass, N+1 the
1363 // program change (factory presets only), then the hidden MIDI proxies.
1364 static constexpr Steinberg_int32 kBypassIndex =
1365 static_cast<Steinberg_int32>(kNumParams);
1366 static constexpr Steinberg_int32 kProgramIndex =
1367 kNumPresets > 0 ? kBypassIndex + 1 : -1;
1368 static constexpr Steinberg_int32 kFirstProxyIndex =
1369 kBypassIndex + 1 + (kNumPresets > 0 ? 1 : 0);
1370
1371 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetParameterCount(void*)
1372 {
1374 }
1375
1376 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParameterInfo(void* self_,
1377 Steinberg_int32 index, Steinberg_Vst_ParameterInfo* info)
1378 {
1379 (void) self_;
1380 if (info == nullptr || index < 0 || index >= sGetParameterCount(self_))
1381 return Steinberg_kInvalidArgument;
1382 std::memset(info, 0, sizeof(*info));
1383 if (index == kBypassIndex)
1384 {
1385 info->id = kBypassParamId;
1386 asciiToString128("Bypass", info->title);
1387 asciiToString128("Byp", info->shortTitle);
1388 info->stepCount = 1;
1389 info->defaultNormalizedValue = 0.0;
1390 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate
1391 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsBypass;
1392 return Steinberg_kResultOk;
1393 }
1394 if (kNumPresets > 0 && index == kProgramIndex)
1395 {
1396 info->id = kProgramParamId;
1397 asciiToString128("Program", info->title);
1398 asciiToString128("Prog", info->shortTitle);
1399 info->stepCount = kNumPresets - 1;
1400 info->defaultNormalizedValue = 0.0;
1401 info->unitId = Steinberg_Vst_kRootUnitId;
1402 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate
1403 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsList
1404 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsProgramChange;
1405 return Steinberg_kResultOk;
1406 }
1407 if (HasMidi<P> && index >= kFirstProxyIndex)
1408 {
1409 // Hidden conduits for IMidiMapping: hosts write MIDI controller
1410 // motion into them; they never show in generic UIs.
1411 const int slot = index - kFirstProxyIndex;
1412 const int channel = slot / 4;
1413 const int ctrl = kMidiProxyControllers[slot % 4];
1414 info->id = midiProxyId(channel, ctrl);
1415 asciiToString128("MIDI", info->title);
1416 asciiToString128("MIDI", info->shortTitle);
1417 info->stepCount = 0;
1418 info->defaultNormalizedValue = ctrl == 129 ? 0.5 : 0.0;
1419 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kIsHidden;
1420 return Steinberg_kResultOk;
1421 }
1422 const auto& spec = P::parameters[static_cast<size_t>(index)];
1423 info->id = hash32(spec.id);
1424 asciiToString128(spec.name, info->title);
1425 asciiToString128(spec.name, info->shortTitle);
1426 asciiToString128(spec.unit, info->units);
1427 info->stepCount = spec.steps;
1428 info->defaultNormalizedValue = toNormalized(spec, spec.defValue);
1429 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate;
1430 if (spec.labels != nullptr) // named choice: hosts offer a list
1431 info->flags |= Steinberg_Vst_ParameterInfo_ParameterFlags_kIsList;
1432 return Steinberg_kResultOk;
1433 }
1434
1435 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamStringByValue(void* self_,
1436 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized,
1437 Steinberg_Vst_String128 out)
1438 {
1439 (void) self_;
1440 if (out == nullptr) return Steinberg_kInvalidArgument;
1441 char text[64] = "";
1442 if (id == kBypassParamId)
1443 std::snprintf(text, sizeof(text), "%s", normalized >= 0.5 ? "On" : "Off");
1444 else if (kNumPresets > 0 && id == kProgramParamId)
1445 {
1446 if constexpr (HasFactoryPresets<P>)
1447 {
1448 const int idx = programIndexOf(normalized);
1449 std::snprintf(text, sizeof(text), "%s",
1450 P::factoryPresets[static_cast<size_t>(idx)].name);
1451 }
1452 }
1453 else if (HasMidi<P> && isMidiProxyId(id))
1454 std::snprintf(text, sizeof(text), "%.3f", normalized);
1455 else
1456 {
1457 const int idx = indexOfParamId(id);
1458 if (idx < 0) return Steinberg_kInvalidArgument;
1459 const auto& spec = P::parameters[static_cast<size_t>(idx)];
1460 formatValue(spec, toPlain(spec, normalized), text, sizeof(text));
1461 }
1462 asciiToString128(text, out);
1463 return Steinberg_kResultOk;
1464 }
1465
1466 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamValueByString(void* self_,
1467 Steinberg_Vst_ParamID id, Steinberg_Vst_TChar* string,
1468 Steinberg_Vst_ParamValue* normalized)
1469 {
1470 (void) self_;
1471 if (string == nullptr || normalized == nullptr) return Steinberg_kInvalidArgument;
1472 char ascii[64];
1473 int i = 0;
1474 for (; i < 63 && string[i] != 0; ++i)
1475 ascii[i] = static_cast<char>(string[i]);
1476 ascii[i] = '\0';
1477 const int toggle = parseToggleText(ascii);
1478 if (id == kBypassParamId)
1479 {
1480 *normalized = toggle >= 0 ? toggle
1481 : (std::strtod(ascii, nullptr) >= 0.5 ? 1.0 : 0.0);
1482 return Steinberg_kResultOk;
1483 }
1484 if (kNumPresets > 0 && id == kProgramParamId)
1485 {
1486 if constexpr (HasFactoryPresets<P>)
1487 {
1488 for (int presetIdx = 0; presetIdx < kNumPresets; ++presetIdx)
1489 if (std::strcmp(ascii,
1490 P::factoryPresets[static_cast<size_t>(presetIdx)].name)
1491 == 0)
1492 {
1493 *normalized = kNumPresets > 1
1494 ? static_cast<double>(presetIdx) / (kNumPresets - 1)
1495 : 0.0;
1496 return Steinberg_kResultOk;
1497 }
1498 }
1499 const double v = std::strtod(ascii, nullptr);
1500 *normalized = kNumPresets > 1 ? v / (kNumPresets - 1) : 0.0;
1501 return Steinberg_kResultOk;
1502 }
1503 if (HasMidi<P> && isMidiProxyId(id))
1504 {
1505 const double v = std::strtod(ascii, nullptr);
1506 *normalized = v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v);
1507 return Steinberg_kResultOk;
1508 }
1509 const int idx = indexOfParamId(id);
1510 if (idx < 0) return Steinberg_kInvalidArgument;
1511 const Param& spec = P::parameters[static_cast<size_t>(idx)];
1512 double plain = 0.0;
1513 if (!parseValue(spec, ascii, plain)) return Steinberg_kResultFalse;
1514 *normalized = toNormalized(spec, plain);
1515 return Steinberg_kResultOk;
1516 }
1517
1518 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sNormalizedParamToPlain(void*,
1519 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized)
1520 {
1521 if (id == kBypassParamId) return normalized >= 0.5 ? 1.0 : 0.0;
1522 if (kNumPresets > 0 && id == kProgramParamId)
1523 return static_cast<double>(programIndexOf(normalized));
1524 if (HasMidi<P> && isMidiProxyId(id)) return normalized;
1525 const int idx = indexOfParamId(id);
1526 return idx < 0 ? 0.0 : toPlain(P::parameters[static_cast<size_t>(idx)], normalized);
1527 }
1528
1529 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sPlainParamToNormalized(void*,
1530 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue plain)
1531 {
1532 if (id == kBypassParamId) return plain >= 0.5 ? 1.0 : 0.0;
1533 if (kNumPresets > 0 && id == kProgramParamId)
1534 return kNumPresets > 1 ? plain / (kNumPresets - 1) : 0.0;
1535 if (HasMidi<P> && isMidiProxyId(id)) return plain;
1536 const int idx = indexOfParamId(id);
1537 return idx < 0 ? 0.0 : toNormalized(P::parameters[static_cast<size_t>(idx)], plain);
1538 }
1539
1540 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sGetParamNormalized(void* self_,
1541 Steinberg_Vst_ParamID id)
1542 {
1543 auto* p = fromLens(self_, 2);
1544 p->deliverPendingRestart(); // hosts poll this from the UI thread
1545 if (id == kBypassParamId)
1546 return p->bypass.load(std::memory_order_relaxed) ? 1.0 : 0.0;
1547 if (kNumPresets > 0 && id == kProgramParamId)
1548 return kNumPresets > 1
1549 ? static_cast<double>(p->currentProgram.load(std::memory_order_relaxed))
1550 / (kNumPresets - 1)
1551 : 0.0;
1552 if constexpr (HasMidi<P>)
1553 {
1554 if (isMidiProxyId(id))
1555 return p->midiProxyShadow[static_cast<size_t>(midiProxySlot(id))]
1556 .load(std::memory_order_relaxed);
1557 }
1558 const int idx = indexOfParamId(id);
1559 return idx < 0 ? 0.0
1560 : p->shadow[static_cast<size_t>(idx)].load(std::memory_order_relaxed);
1561 }
1562
1563 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetParamNormalized(void* self_,
1564 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue value)
1565 {
1566 auto* p = fromLens(self_, 2);
1567 if (id == kBypassParamId)
1568 {
1569 p->bypass.store(value >= 0.5, std::memory_order_relaxed);
1570 return Steinberg_kResultOk;
1571 }
1572 if (kNumPresets > 0 && id == kProgramParamId)
1573 {
1574 // A program change from the host UI: apply the preset and tell
1575 // the host every other parameter moved (main-thread call).
1576 p->applyFactoryPresetIdx(programIndexOf(value));
1577 p->refreshLatency(Steinberg_Vst_RestartFlags_kParamValuesChanged);
1578 return Steinberg_kResultOk;
1579 }
1580 if constexpr (HasMidi<P>)
1581 {
1582 // Live MIDI rides the process() queues; a main-thread write only
1583 // keeps the proxy shadow coherent for host round-trips.
1584 if (isMidiProxyId(id))
1585 {
1586 p->midiProxyShadow[static_cast<size_t>(midiProxySlot(id))]
1587 .store(value, std::memory_order_relaxed);
1588 return Steinberg_kResultOk;
1589 }
1590 }
1591 const int idx = indexOfParamId(id);
1592 if (idx < 0) return Steinberg_kInvalidArgument;
1593 p->applyNormalized(idx, value); // user setters are atomic by contract
1594 p->refreshLatency();
1595 return Steinberg_kResultOk;
1596 }
1597
1598 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentHandler(void* self_,
1599 Steinberg_Vst_IComponentHandler* handler)
1600 {
1601 auto* p = fromLens(self_, 2);
1602 // Take the new reference before dropping the old one: a host that
1603 // re-sets the same handler must not see it released to zero first.
1604 if (handler != nullptr)
1605 reinterpret_cast<Steinberg_FUnknown*>(handler)->lpVtbl->addRef(handler);
1606 auto* old = p->handler.exchange(handler, std::memory_order_relaxed);
1607 if (old != nullptr)
1608 reinterpret_cast<Steinberg_FUnknown*>(old)->lpVtbl->release(old);
1609 p->deliverPendingRestart();
1610 return Steinberg_kResultOk;
1611 }
1612
1613 static Steinberg_IPlugView* SMTG_STDMETHODCALLTYPE sCreateView(void* self_,
1614 Steinberg_FIDString name);
1615
1626
1627 // ==========================================================================
1628 // Lens 3 - IProcessContextRequirements
1629 // ==========================================================================
1630
1631 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetProcessContextRequirements(void*)
1632 {
1633 if constexpr (HasTransport<P>)
1634 return Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTempo
1635 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTransportState
1636 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedProjectTimeMusic
1637 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedBarPositionMusic
1638 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTimeSignature
1639 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedCycleMusic;
1640 else
1641 return 0;
1642 }
1643
1644 inline static const Steinberg_Vst_IProcessContextRequirementsVtbl kContextReqVtbl = {
1645 &sQuery<3>, &sAddRef<3>, &sRelease<3>,
1647 };
1648
1649 // ==========================================================================
1650 // Lens 4 - IUnitInfo (factory presets as a program list; surfaced by
1651 // queryInterface only when the class declares factoryPresets)
1652 // ==========================================================================
1653
1654 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetUnitCount(void*) { return 1; }
1655
1656 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitInfo(void*,
1657 Steinberg_int32 unitIndex, Steinberg_Vst_UnitInfo* info)
1658 {
1659 if (unitIndex != 0 || info == nullptr) return Steinberg_kInvalidArgument;
1660 std::memset(info, 0, sizeof(*info));
1661 info->id = Steinberg_Vst_kRootUnitId;
1662 info->parentUnitId = Steinberg_Vst_kNoParentUnitId;
1663 info->programListId = kNumPresets > 0 ? kPresetProgramListId
1664 : Steinberg_Vst_kNoProgramListId;
1665 asciiToString128("Root", info->name);
1666 return Steinberg_kResultOk;
1667 }
1668
1669 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetProgramListCount(void*)
1670 {
1671 return kNumPresets > 0 ? 1 : 0;
1672 }
1673
1674 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramListInfo(void*,
1675 Steinberg_int32 listIndex, Steinberg_Vst_ProgramListInfo* info)
1676 {
1677 if (listIndex != 0 || info == nullptr || kNumPresets == 0)
1678 return Steinberg_kInvalidArgument;
1679 std::memset(info, 0, sizeof(*info));
1680 info->id = kPresetProgramListId;
1681 info->programCount = kNumPresets;
1682 asciiToString128("Factory Presets", info->name);
1683 return Steinberg_kResultOk;
1684 }
1685
1686 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramName(void*,
1687 Steinberg_Vst_ProgramListID listId, Steinberg_int32 programIndex,
1688 Steinberg_Vst_String128 name)
1689 {
1690 if constexpr (HasFactoryPresets<P>)
1691 {
1692 if (listId != kPresetProgramListId || name == nullptr
1693 || programIndex < 0 || programIndex >= kNumPresets)
1694 return Steinberg_kInvalidArgument;
1696 P::factoryPresets[static_cast<size_t>(programIndex)].name, name);
1697 return Steinberg_kResultOk;
1698 }
1699 else
1700 {
1701 (void) listId;
1702 (void) programIndex;
1703 (void) name;
1704 return Steinberg_kInvalidArgument;
1705 }
1706 }
1707
1708 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramInfo(void*,
1709 Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_Vst_CString,
1710 Steinberg_Vst_String128)
1711 { return Steinberg_kResultFalse; }
1712
1713 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sHasProgramPitchNames(void*,
1714 Steinberg_Vst_ProgramListID, Steinberg_int32)
1715 { return Steinberg_kResultFalse; }
1716
1717 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramPitchName(void*,
1718 Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_int16,
1719 Steinberg_Vst_String128)
1720 { return Steinberg_kResultFalse; }
1721
1722 static Steinberg_Vst_UnitID SMTG_STDMETHODCALLTYPE sGetSelectedUnit(void*)
1723 { return Steinberg_Vst_kRootUnitId; }
1724
1725 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSelectUnit(void*,
1726 Steinberg_Vst_UnitID unitId)
1727 { return unitId == Steinberg_Vst_kRootUnitId ? Steinberg_kResultOk
1728 : Steinberg_kInvalidArgument; }
1729
1730 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitByBus(void*,
1731 Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32,
1732 Steinberg_int32, Steinberg_Vst_UnitID* unitId)
1733 {
1734 if (unitId == nullptr) return Steinberg_kInvalidArgument;
1735 *unitId = Steinberg_Vst_kRootUnitId;
1736 return Steinberg_kResultOk;
1737 }
1738
1739 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetUnitProgramData(void*,
1740 Steinberg_int32, Steinberg_int32, struct Steinberg_IBStream*)
1741 { return Steinberg_kNotImplemented; }
1742
1743 inline static const Steinberg_Vst_IUnitInfoVtbl kUnitVtbl = {
1744 &sQuery<4>, &sAddRef<4>, &sRelease<4>,
1749 };
1750
1751 // ==========================================================================
1752 // Lens 5 - IMidiMapping (pitch bend / mod / sustain / channel pressure
1753 // arrive as proxy-parameter automation; surfaced only for HasMidi)
1754 // ==========================================================================
1755
1756 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetMidiControllerAssignment(void*,
1757 Steinberg_int32 busIndex, Steinberg_int16 channel,
1758 Steinberg_Vst_CtrlNumber midiControllerNumber, Steinberg_Vst_ParamID* id)
1759 {
1760 if (!HasMidi<P> || id == nullptr || busIndex != 0
1761 || channel < 0 || channel >= 16)
1762 return Steinberg_kResultFalse;
1763 const int ctrl = midiControllerNumber;
1764 if (ctrl != 1 && ctrl != 64 && ctrl != 128 && ctrl != 129)
1765 return Steinberg_kResultFalse;
1766 *id = midiProxyId(channel, ctrl);
1767 return Steinberg_kResultTrue;
1768 }
1769
1770 inline static const Steinberg_Vst_IMidiMappingVtbl kMidiMapVtbl = {
1771 &sQuery<5>, &sAddRef<5>, &sRelease<5>,
1773 };
1774};
1775
1776// -- the editor view (WebView editor layer) ---------------------------------------
1777//
1778// Compiled only when plugin/webview/DSParkWebViewEditor.h was included before
1779// this header, and created only for plugin classes that declare an editor.
1780// Without either, createView answers nullptr and hosts show their generic UI.
1781
1782#if defined(DSPARK_PLUGIN_WEBVIEW)
1783
1789template <typename P>
1790struct View
1791{
1792 static constexpr size_t kNumParams = Plugin<P>::kNumParams;
1793
1794 // COM lenses, same layout trick as the plugin object. The timer lens is
1795 // Linux-only: it receives the host IRunLoop ticks that drive GTK.
1796 const Steinberg_IPlugViewVtbl* viewVtbl;
1797 const Steinberg_IPlugViewContentScaleSupportVtbl* scaleVtbl;
1798#if defined(__linux__)
1799 const Steinberg_Linux_ITimerHandlerVtbl* timerVtbl;
1800#endif
1801
1802 std::atomic<Steinberg_uint32> refs { 1 };
1803 Plugin<P>* owner;
1804 Steinberg_IPlugFrame* frame = nullptr;
1805 webview_ui::Editor<P> editor;
1806 int width; // current physical size (host units)
1807 int height;
1808 double scale = 1.0;
1809 bool correctingSize = false; // re-entrancy guard for resizeView round-trips
1810 bool editActive[kNumParams == 0 ? 1 : kNumParams] {};
1811#if defined(__linux__)
1812 Steinberg_Linux_IRunLoop* runLoop = nullptr;
1813 bool timerRegistered = false;
1814#endif
1815
1816 explicit View(Plugin<P>* plugin) noexcept : owner(plugin)
1817 {
1818 viewVtbl = &kViewVtbl;
1819 scaleVtbl = &kScaleVtbl;
1820#if defined(__linux__)
1821 timerVtbl = &kTimerVtbl;
1822#endif
1823 owner->addRefImpl();
1824 const EditorSize logical = editorSizeOf<P>();
1825 width = logical.width;
1826 height = logical.height;
1827 }
1828
1829 ~View()
1830 {
1831#if defined(__linux__)
1832 releaseRunLoop();
1833#endif
1834 editor.destroy();
1835 if (frame != nullptr)
1836 reinterpret_cast<Steinberg_FUnknown*>(frame)->lpVtbl->release(frame);
1837 owner->releaseImpl();
1838 }
1839
1840#if defined(__linux__)
1841
1842 // --- host run loop: GTK breathes only when pumped from here ---------------------
1843
1844 void acquireRunLoop() noexcept
1845 {
1846 if (frame == nullptr || runLoop != nullptr) return;
1847 void* loop = nullptr;
1848 if (reinterpret_cast<Steinberg_FUnknown*>(frame)->lpVtbl->queryInterface(
1849 frame, Steinberg_Linux_IRunLoop_iid, &loop) == Steinberg_kResultOk)
1850 runLoop = static_cast<Steinberg_Linux_IRunLoop*>(loop);
1851 }
1852
1853 void startTimer() noexcept
1854 {
1855 acquireRunLoop();
1856 if (runLoop != nullptr && !timerRegistered && editor.created())
1857 timerRegistered = runLoop->lpVtbl->registerTimer(runLoop,
1858 reinterpret_cast<Steinberg_Linux_ITimerHandler*>(lensPtr(2)),
1859 33) == Steinberg_kResultOk;
1860 }
1861
1862 void stopTimer() noexcept
1863 {
1864 if (runLoop != nullptr && timerRegistered)
1865 runLoop->lpVtbl->unregisterTimer(runLoop,
1866 reinterpret_cast<Steinberg_Linux_ITimerHandler*>(lensPtr(2)));
1867 timerRegistered = false;
1868 }
1869
1870 void releaseRunLoop() noexcept
1871 {
1872 stopTimer();
1873 if (runLoop != nullptr)
1874 {
1875 reinterpret_cast<Steinberg_FUnknown*>(runLoop)->lpVtbl->release(runLoop);
1876 runLoop = nullptr;
1877 }
1878 }
1879
1880 static void SMTG_STDMETHODCALLTYPE sOnTimer(void* self_)
1881 {
1882 View* view = fromLens(self_, 2);
1883 view->editor.pump();
1884 view->owner->deliverPendingRestart(); // the frame's run loop ticks too
1885 }
1886
1887#endif // __linux__
1888
1889 static View* fromLens(void* iface, int lens) noexcept
1890 {
1891 return reinterpret_cast<View*>(static_cast<char*>(iface)
1892 - static_cast<ptrdiff_t>(lens) * sizeof(void*));
1893 }
1894
1895 void* lensPtr(int lens) noexcept
1896 {
1897 return reinterpret_cast<char*>(this) + static_cast<ptrdiff_t>(lens) * sizeof(void*);
1898 }
1899
1900 static const char* platformType() noexcept
1901 {
1902#if defined(_WIN32)
1903 return Steinberg_kPlatformTypeHWND;
1904#elif defined(__APPLE__)
1905 return Steinberg_kPlatformTypeNSView;
1906#else
1907 return Steinberg_kPlatformTypeX11EmbedWindowID;
1908#endif
1909 }
1910
1911 // --- editor -> host/DSP bridge ------------------------------------------------
1912
1913 static void cbSetParam(void* context, int index, double plain) noexcept
1914 {
1915 auto* view = static_cast<View*>(context);
1916 const Param& spec = P::parameters[static_cast<size_t>(index)];
1917 const double normalized = toNormalized(spec, plain);
1918 view->owner->applyNormalized(index, normalized);
1919 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1920 {
1921 const Steinberg_Vst_ParamID id = hash32(spec.id);
1922 if (view->editActive[static_cast<size_t>(index)])
1923 handler->lpVtbl->performEdit(handler, id, normalized);
1924 else
1925 {
1926 // Edits outside an explicit gesture still need one for host undo.
1927 handler->lpVtbl->beginEdit(handler, id);
1928 handler->lpVtbl->performEdit(handler, id, normalized);
1929 handler->lpVtbl->endEdit(handler, id);
1930 }
1931 }
1932 }
1933
1934 static void cbBeginEdit(void* context, int index) noexcept
1935 {
1936 auto* view = static_cast<View*>(context);
1937 view->editActive[static_cast<size_t>(index)] = true;
1938 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1939 handler->lpVtbl->beginEdit(handler,
1940 hash32(P::parameters[static_cast<size_t>(index)].id));
1941 }
1942
1943 static void cbEndEdit(void* context, int index) noexcept
1944 {
1945 auto* view = static_cast<View*>(context);
1946 view->editActive[static_cast<size_t>(index)] = false;
1947 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1948 handler->lpVtbl->endEdit(handler,
1949 hash32(P::parameters[static_cast<size_t>(index)].id));
1950 }
1951
1952 // --- FUnknown -------------------------------------------------------------------
1953
1954 Steinberg_tresult query(const Steinberg_TUID iid, void** obj) noexcept
1955 {
1956 if (obj == nullptr) return Steinberg_kInvalidArgument;
1957 *obj = nullptr;
1958 if (tuidEqual(iid, Steinberg_FUnknown_iid)
1959 || tuidEqual(iid, Steinberg_IPlugView_iid))
1960 *obj = lensPtr(0);
1961 else if (tuidEqual(iid, Steinberg_IPlugViewContentScaleSupport_iid))
1962 *obj = lensPtr(1);
1963#if defined(__linux__)
1964 else if (tuidEqual(iid, Steinberg_Linux_ITimerHandler_iid))
1965 *obj = lensPtr(2);
1966#endif
1967 if (*obj == nullptr) return Steinberg_kNoInterface;
1968 refs.fetch_add(1, std::memory_order_relaxed);
1969 return Steinberg_kResultOk;
1970 }
1971
1972 template <int Lens>
1973 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
1974 const Steinberg_TUID iid,
1975 void** obj)
1976 { return fromLens(self_, Lens)->query(iid, obj); }
1977
1978 template <int Lens>
1979 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void* self_)
1980 { return fromLens(self_, Lens)->refs.fetch_add(1, std::memory_order_relaxed) + 1; }
1981
1982 template <int Lens>
1983 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void* self_)
1984 {
1985 auto* view = fromLens(self_, Lens);
1986 const Steinberg_uint32 left = view->refs.fetch_sub(1, std::memory_order_acq_rel) - 1;
1987 if (left == 0) delete view;
1988 return left;
1989 }
1990
1991 // --- IPlugView --------------------------------------------------------------------
1992
1993 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sIsPlatformTypeSupported(void*,
1994 Steinberg_FIDString type)
1995 {
1996 return (webview_ui::Editor<P>::available() && type != nullptr
1997 && std::strcmp(type, platformType()) == 0)
1998 ? Steinberg_kResultTrue : Steinberg_kResultFalse;
1999 }
2000
2001 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sAttached(void* self_, void* parent,
2002 Steinberg_FIDString type)
2003 {
2004 auto* view = fromLens(self_, 0);
2005 webview_ui::debugLog("vst3 attached(parent=%p type=%s) negotiated=%dx%d scale=%.2f",
2006 parent, type != nullptr ? type : "?",
2007 view->width, view->height, view->scale);
2008 if (sIsPlatformTypeSupported(self_, type) != Steinberg_kResultTrue
2009 || parent == nullptr)
2010 return Steinberg_kResultFalse;
2011 const webview_ui::HostCallbacks callbacks {
2012 view, &cbSetParam, &cbBeginEdit, &cbEndEdit
2013 };
2014 if (!view->editor.create(parent, view->owner->shadow, callbacks))
2015 return Steinberg_kResultFalse;
2016 // Fill the box the host ACTUALLY built - hosts differ in whether
2017 // getSize/setContentScaleFactor/attached arrive in spec order, so the
2018 // parent's real client size wins over the negotiated one.
2019 int parentW = 0, parentH = 0;
2020 if (view->editor.queryParentSize(parentW, parentH))
2021 {
2022 view->width = parentW;
2023 view->height = parentH;
2024 }
2025 view->editor.setBounds(view->width, view->height);
2026 view->editor.setVisible(true);
2027#if defined(__linux__)
2028 // GTK breathes only when pumped from the host's run loop; without a
2029 // usable IRunLoop the page would freeze, so fall back to generic UI.
2030 view->startTimer();
2031 if (!view->timerRegistered)
2032 {
2033 webview_ui::debugLog("vst3 attached: no usable IRunLoop -> no editor");
2034 view->editor.destroy();
2035 return Steinberg_kResultFalse;
2036 }
2037#endif
2038 return Steinberg_kResultOk;
2039 }
2040
2041 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sRemoved(void* self_)
2042 {
2043 auto* view = fromLens(self_, 0);
2044#if defined(__linux__)
2045 view->stopTimer();
2046#endif
2047 view->editor.destroy();
2048 return Steinberg_kResultOk;
2049 }
2050
2051 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnWheel(void*, float)
2052 { return Steinberg_kResultFalse; }
2053
2054 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnKeyDown(void*, Steinberg_char16,
2055 Steinberg_int16, Steinberg_int16)
2056 { return Steinberg_kResultFalse; }
2057
2058 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnKeyUp(void*, Steinberg_char16,
2059 Steinberg_int16, Steinberg_int16)
2060 { return Steinberg_kResultFalse; }
2061
2062 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetSize(void* self_,
2063 Steinberg_ViewRect* size)
2064 {
2065 auto* view = fromLens(self_, 0);
2066 if (size == nullptr) return Steinberg_kInvalidArgument;
2067 size->left = 0;
2068 size->top = 0;
2069 size->right = view->width;
2070 size->bottom = view->height;
2071 webview_ui::debugLog("vst3 getSize -> %dx%d", view->width, view->height);
2072 return Steinberg_kResultOk;
2073 }
2074
2075 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnSize(void* self_,
2076 Steinberg_ViewRect* newSize)
2077 {
2078 auto* view = fromLens(self_, 0);
2079 if (newSize == nullptr) return Steinberg_kInvalidArgument;
2080 const double proposedW = newSize->right - newSize->left;
2081 const double proposedH = newSize->bottom - newSize->top;
2082 double w = proposedW;
2083 double h = proposedH;
2084 // Enforce the resize policy HERE, not only in checkSizeConstraint:
2085 // some hosts drag-resize freely and only report the result. When the
2086 // host overshot, apply the constrained size and ask it to follow.
2087 if (!view->correctingSize)
2088 constrainEditorSize<P>(w, h, view->scale);
2089 webview_ui::debugLog("vst3 onSize %.0fx%.0f -> %.0fx%.0f%s",
2090 proposedW, proposedH, w, h,
2091 view->correctingSize ? " (correction round-trip)" : "");
2092 view->width = toBoundedInt(w + 0.5, 1, static_cast<int>(kEditorMaxPixels));
2093 view->height = toBoundedInt(h + 0.5, 1, static_cast<int>(kEditorMaxPixels));
2094 view->editor.setBounds(view->width, view->height);
2095 if (!view->correctingSize && view->frame != nullptr
2096 && (view->width - proposedW > 1.0 || proposedW - view->width > 1.0
2097 || view->height - proposedH > 1.0 || proposedH - view->height > 1.0))
2098 {
2099 view->correctingSize = true;
2100 Steinberg_ViewRect corrected { 0, 0, view->width, view->height };
2101 view->frame->lpVtbl->resizeView(view->frame,
2102 reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0)), &corrected);
2103 view->correctingSize = false;
2104 }
2105 return Steinberg_kResultOk;
2106 }
2107
2108 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnFocus(void*, Steinberg_TBool)
2109 { return Steinberg_kResultOk; }
2110
2111 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetFrame(void* self_,
2112 Steinberg_IPlugFrame* frame)
2113 {
2114 auto* view = fromLens(self_, 0);
2115#if defined(__linux__)
2116 view->releaseRunLoop(); // the run loop belongs to the old frame
2117#endif
2118 if (view->frame != nullptr)
2119 reinterpret_cast<Steinberg_FUnknown*>(view->frame)
2120 ->lpVtbl->release(view->frame);
2121 view->frame = frame;
2122 if (view->frame != nullptr)
2123 reinterpret_cast<Steinberg_FUnknown*>(view->frame)
2124 ->lpVtbl->addRef(view->frame);
2125#if defined(__linux__)
2126 if (view->editor.created())
2127 view->startTimer(); // frame arrived after attach (rare order)
2128#endif
2129 return Steinberg_kResultOk;
2130 }
2131
2132 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanResize(void*)
2133 {
2134 return editorResizeOf<P>() != EditorResize::Fixed ? Steinberg_kResultTrue
2135 : Steinberg_kResultFalse;
2136 }
2137
2138 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCheckSizeConstraint(void* self_,
2139 Steinberg_ViewRect* rect)
2140 {
2141 auto* view = fromLens(self_, 0);
2142 if (rect == nullptr) return Steinberg_kInvalidArgument;
2143 const double proposedW = rect->right - rect->left;
2144 const double proposedH = rect->bottom - rect->top;
2145 double w = proposedW;
2146 double h = proposedH;
2147 constrainEditorSize<P>(w, h, view->scale);
2148 rect->right = rect->left
2149 + toBoundedInt(w + 0.5, 1, static_cast<int>(kEditorMaxPixels));
2150 rect->bottom = rect->top
2151 + toBoundedInt(h + 0.5, 1, static_cast<int>(kEditorMaxPixels));
2152 webview_ui::debugLog("vst3 checkSizeConstraint %.0fx%.0f -> %.0fx%.0f",
2153 proposedW, proposedH, w, h);
2154 return Steinberg_kResultTrue;
2155 }
2156
2157 inline static const Steinberg_IPlugViewVtbl kViewVtbl = {
2158 &sQuery<0>, &sAddRef<0>, &sRelease<0>,
2159 &sIsPlatformTypeSupported, &sAttached, &sRemoved,
2160 &sOnWheel, &sOnKeyDown, &sOnKeyUp,
2161 &sGetSize, &sOnSize, &sOnFocus,
2162 &sSetFrame, &sCanResize, &sCheckSizeConstraint
2163 };
2164
2165 // --- IPlugViewContentScaleSupport ----------------------------------------------
2166
2167 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetContentScaleFactor(void* self_,
2168 Steinberg_IPlugViewContentScaleSupport_ScaleFactor factor)
2169 {
2170 auto* view = fromLens(self_, 1);
2171 if (!(factor > 0.0f)) return Steinberg_kInvalidArgument; // NaN included
2172 const double previous = view->scale;
2173 view->scale = factor;
2174 webview_ui::debugLog("vst3 setContentScaleFactor %.2f (was %.2f)",
2175 view->scale, previous);
2176 // Rescale the negotiated physical size (the page itself never zooms:
2177 // the web engine applies the window DPI to CSS pixels on its own).
2178 view->width = toBoundedInt(view->width * (view->scale / previous) + 0.5,
2179 1, static_cast<int>(kEditorMaxPixels));
2180 view->height = toBoundedInt(view->height * (view->scale / previous) + 0.5,
2181 1, static_cast<int>(kEditorMaxPixels));
2182 if (view->editor.created() && view->frame != nullptr)
2183 {
2184 // Already on screen (e.g. dragged to another monitor): ask the
2185 // host to rebuild the window at the new physical size.
2186 Steinberg_ViewRect rect { 0, 0, view->width, view->height };
2187 view->frame->lpVtbl->resizeView(view->frame,
2188 reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0)), &rect);
2189 }
2190 return Steinberg_kResultOk;
2191 }
2192
2193 inline static const Steinberg_IPlugViewContentScaleSupportVtbl kScaleVtbl = {
2194 &sQuery<1>, &sAddRef<1>, &sRelease<1>,
2195 &sSetContentScaleFactor
2196 };
2197
2198#if defined(__linux__)
2199 // Defined after the FUnknown thunks: a static member INITIALIZER has no
2200 // complete-class context, so every name it uses must be declared above.
2201 inline static const Steinberg_Linux_ITimerHandlerVtbl kTimerVtbl = {
2202 &sQuery<2>, &sAddRef<2>, &sRelease<2>, &sOnTimer
2203 };
2204#endif
2205};
2206
2207#endif // DSPARK_PLUGIN_WEBVIEW
2208
2209template <typename P>
2210Steinberg_IPlugView* SMTG_STDMETHODCALLTYPE Plugin<P>::sCreateView(void* self_,
2211 Steinberg_FIDString name)
2212{
2213 (void) self_;
2214 (void) name;
2215#if defined(DSPARK_PLUGIN_WEBVIEW)
2216 if constexpr (HasEditor<P>)
2217 {
2218 if (webview_ui::Editor<P>::available() && name != nullptr
2219 && std::strcmp(name, "editor") == 0)
2220 {
2221 auto* view = new (std::nothrow) View<P>(fromLens(self_, 2));
2222 if (view != nullptr)
2223 return reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0));
2224 }
2225 }
2226#endif
2227 return nullptr; // no editor: the host shows its generic parameter UI
2228}
2229
2230// -- factory --------------------------------------------------------------------
2231
2232template <typename P>
2234{
2235 const Steinberg_IPluginFactory3Vtbl* vtbl;
2236
2237 Factory() noexcept { vtbl = &kVtbl; }
2238
2239 static std::array<uint8_t, 16> classUid() noexcept
2240 {
2241 return makeUid(P::descriptor.productId, 0x56535433ull); // 'VST3'
2242 }
2243
2244 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
2245 const Steinberg_TUID iid, void** obj)
2246 {
2247 if (obj == nullptr) return Steinberg_kInvalidArgument;
2248 if (tuidEqual(iid, Steinberg_FUnknown_iid)
2249 || tuidEqual(iid, Steinberg_IPluginFactory_iid)
2250 || tuidEqual(iid, Steinberg_IPluginFactory2_iid)
2251 || tuidEqual(iid, Steinberg_IPluginFactory3_iid))
2252 {
2253 *obj = self_;
2254 return Steinberg_kResultOk;
2255 }
2256 *obj = nullptr;
2257 return Steinberg_kNoInterface;
2258 }
2259
2260 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void*) { return 100; }
2261 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void*) { return 100; }
2262
2263 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetFactoryInfo(void*,
2264 Steinberg_PFactoryInfo* info)
2265 {
2266 if (info == nullptr) return Steinberg_kInvalidArgument;
2267 std::memset(info, 0, sizeof(*info));
2268 copyAscii(P::descriptor.vendor, info->vendor, sizeof(info->vendor));
2269 copyAscii(P::descriptor.url, info->url, sizeof(info->url));
2270 copyAscii(P::descriptor.email, info->email, sizeof(info->email));
2271 info->flags = Steinberg_PFactoryInfo_FactoryFlags_kUnicode;
2272 return Steinberg_kResultOk;
2273 }
2274
2275 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sCountClasses(void*) { return 1; }
2276
2277 static void fillClassCommon(Steinberg_TUID cid) noexcept
2278 {
2279 const auto uid = classUid();
2280 std::memcpy(cid, uid.data(), 16);
2281 }
2282
2283 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo(void*,
2284 Steinberg_int32 index, Steinberg_PClassInfo* info)
2285 {
2286 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
2287 std::memset(info, 0, sizeof(*info));
2288 fillClassCommon(info->cid);
2289 info->cardinality = 0x7FFFFFFF; // kManyInstances
2290 copyAscii("Audio Module Class", info->category, sizeof(info->category));
2291 copyAscii(P::descriptor.name, info->name, sizeof(info->name));
2292 return Steinberg_kResultOk;
2293 }
2294
2295 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo2(void*,
2296 Steinberg_int32 index, Steinberg_PClassInfo2* info)
2297 {
2298 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
2299 std::memset(info, 0, sizeof(*info));
2300 fillClassCommon(info->cid);
2301 info->cardinality = 0x7FFFFFFF;
2302 copyAscii("Audio Module Class", info->category, sizeof(info->category));
2303 copyAscii(P::descriptor.name, info->name, sizeof(info->name));
2304 info->classFlags = 0;
2305 copyAscii(P::descriptor.category == Category::Instrument ? "Instrument" : "Fx",
2306 info->subCategories, sizeof(info->subCategories));
2307 copyAscii(P::descriptor.vendor, info->vendor, sizeof(info->vendor));
2308 copyAscii(P::descriptor.version, info->version, sizeof(info->version));
2309 copyAscii("VST 3.7.9", info->sdkVersion, sizeof(info->sdkVersion));
2310 return Steinberg_kResultOk;
2311 }
2312
2313 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfoUnicode(void*,
2314 Steinberg_int32 index, Steinberg_PClassInfoW* info)
2315 {
2316 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
2317 std::memset(info, 0, sizeof(*info));
2318 fillClassCommon(info->cid);
2319 info->cardinality = 0x7FFFFFFF;
2320 copyAscii("Audio Module Class", info->category, sizeof(info->category));
2321 asciiToString128(P::descriptor.name, info->name);
2322 info->classFlags = 0;
2323 copyAscii(P::descriptor.category == Category::Instrument ? "Instrument" : "Fx",
2324 info->subCategories, sizeof(info->subCategories));
2325 asciiToString128(P::descriptor.vendor, info->vendor);
2326 asciiToString128(P::descriptor.version, info->version);
2327 asciiToString128("VST 3.7.9", info->sdkVersion);
2328 return Steinberg_kResultOk;
2329 }
2330
2331 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCreateInstance(void*,
2332 Steinberg_FIDString cid, Steinberg_FIDString iid, void** obj)
2333 {
2334 if (cid == nullptr || iid == nullptr || obj == nullptr)
2335 return Steinberg_kInvalidArgument;
2336 *obj = nullptr;
2337 const auto uid = classUid();
2338 if (std::memcmp(cid, uid.data(), 16) != 0)
2339 return Steinberg_kNoInterface;
2340
2341 auto* plugin = new (std::nothrow) Plugin<P>();
2342 if (plugin == nullptr) return Steinberg_kResultFalse;
2343
2344 Steinberg_TUID requested;
2345 std::memcpy(requested, iid, sizeof(requested));
2346 const Steinberg_tresult r = plugin->query(requested, obj);
2347 plugin->releaseImpl(); // drop the construction reference
2348 return r == Steinberg_kResultOk ? Steinberg_kResultOk : Steinberg_kNoInterface;
2349 }
2350
2351 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetHostContext(void*, Steinberg_FUnknown*)
2352 { return Steinberg_kResultOk; }
2353
2354 inline static const Steinberg_IPluginFactory3Vtbl kVtbl = {
2355 &sQuery, &sAddRef, &sRelease,
2359 };
2360};
2361
2362} // namespace dspark::plugin::vst3
2363
2364// -- module entry ----------------------------------------------------------------
2365
2366#if defined(_WIN32)
2367#define DSPARK_VST3_EXPORT __declspec(dllexport)
2368#else
2369#define DSPARK_VST3_EXPORT __attribute__((visibility("default")))
2370#endif
2371
2376#define DSPARK_VST3_PLUGIN(PluginClass) \
2377 static dspark::plugin::vst3::Factory<PluginClass> gDsparkVst3Factory; \
2378 extern "C" { \
2379 DSPARK_VST3_EXPORT Steinberg_IPluginFactory* SMTG_STDMETHODCALLTYPE \
2380 GetPluginFactory() \
2381 { \
2382 return reinterpret_cast<Steinberg_IPluginFactory*>(&gDsparkVst3Factory); \
2383 } \
2384 DSPARK_VST3_EXPORT bool InitDll() { return true; } \
2385 DSPARK_VST3_EXPORT bool ExitDll() { return true; } \
2386 DSPARK_VST3_EXPORT bool ModuleEntry(void*) { return true; } \
2387 DSPARK_VST3_EXPORT bool ModuleExit() { return true; } \
2388 DSPARK_VST3_EXPORT bool bundleEntry(void*) { return true; } \
2389 DSPARK_VST3_EXPORT bool bundleExit() { return true; } \
2390 }
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
The dry signal of the wrappers' soft bypass, delayed by the plugin's latency.
A ~30 Hz tick on the host's UI thread.
Definition DSParkVst3.h:110
bool start(Steinberg_FUnknown *hostContext, Callback callback, void *context) noexcept
Definition DSParkVst3.h:121
bool running() const noexcept
Definition DSParkVst3.h:197
void(*)(void *context) noexcept Callback
Definition DSParkVst3.h:112
UiTicker() noexcept=default
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....
Factory-preset capability: a static constexpr auto factoryPresets table built with presets()....
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 uint32_t midiProxyId(int channel, int controller) noexcept
Definition DSParkVst3.h:344
bool streamRead(Steinberg_IBStream *s, void *dst, Steinberg_int32 bytes) noexcept
Reads the full requested range from an IBStream (hosts may chunk).
Definition DSParkVst3.h:292
constexpr uint32_t kBypassParamId
Definition DSParkVst3.h:323
constexpr uint32_t kMidiProxyBase
Definition DSParkVst3.h:331
constexpr int kNumMidiProxies
Definition DSParkVst3.h:333
bool tuidEqual(const Steinberg_TUID a, const Steinberg_TUID b) noexcept
Definition DSParkVst3.h:272
bool streamWrite(Steinberg_IBStream *s, const void *src, Steinberg_int32 bytes) noexcept
Definition DSParkVst3.h:306
constexpr Steinberg_int32 kPresetProgramListId
Definition DSParkVst3.h:326
void asciiToString128(const char *src, Steinberg_Vst_String128 dst) noexcept
Definition DSParkVst3.h:277
constexpr int kMidiProxyControllers[4]
Definition DSParkVst3.h:332
constexpr bool isMidiProxyId(uint32_t id) noexcept
Definition DSParkVst3.h:335
MidiEvent midiProxyToEvent(uint32_t id, double normalized, int offset) noexcept
Proxy parameter (normalized) -> the framework-neutral MidiEvent.
Definition DSParkVst3.h:351
constexpr uint32_t kProgramParamId
Definition DSParkVst3.h:324
constexpr int kBypassRampSamples
Definition DSParkVst3.h:325
void copyAscii(const char *src, char *dst, size_t cap) noexcept
Definition DSParkVst3.h:286
@ 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 int kDryDelayMinCapacity
Ring headroom of the bypass dry path: latencies up to this (or twice the latency at activation,...
constexpr std::array< uint8_t, 16 > makeUid(const char *productId, uint64_t salt) noexcept
Deterministic 16-byte class UID from the stable productId. Each backend salts differently so VST3/CLA...
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", "Saw", "4").
void sortBlockEvents(BlockEvent *events, int count) noexcept
Stable insertion sort by offset (tiny N, allocation-free - audio-thread safe; equal offsets keep arri...
constexpr int toBoundedInt(double value, int lo, int hi) noexcept
A floating value from outside converted to an integer, with the range settled BEFORE the conversion.
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...
bool parseValue(const Param &p, const char *text, double &plain) noexcept
Host text -> plain value, the inverse of formatValue(): a choice's label (any case),...
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
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,...
Host transport snapshot, delivered once per audio block.
bool playing
Transport is rolling.
static std::array< uint8_t, 16 > classUid() noexcept
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sCountClasses(void *)
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void *)
static void fillClassCommon(Steinberg_TUID cid) noexcept
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetHostContext(void *, Steinberg_FUnknown *)
static const Steinberg_IPluginFactory3Vtbl kVtbl
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void *)
const Steinberg_IPluginFactory3Vtbl * vtbl
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void *self_, const Steinberg_TUID iid, void **obj)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfoUnicode(void *, Steinberg_int32 index, Steinberg_PClassInfoW *info)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCreateInstance(void *, Steinberg_FIDString cid, Steinberg_FIDString iid, void **obj)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo(void *, Steinberg_int32 index, Steinberg_PClassInfo *info)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo2(void *, Steinberg_int32 index, Steinberg_PClassInfo2 *info)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetFactoryInfo(void *, Steinberg_PFactoryInfo *info)
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetProgramListCount(void *)
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetLatencySamples(void *self_)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentSetState(void *self_, Steinberg_IBStream *state)
Definition DSParkVst3.h:939
static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sGetParamNormalized(void *self_, Steinberg_Vst_ParamID id)
std::atomic< double > shadow[kNumParams==0 ? 1 :kNumParams]
Definition DSParkVst3.h:418
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetupProcessing(void *self_, Steinberg_Vst_ProcessSetup *setup)
static constexpr Steinberg_int32 kFirstProxyIndex
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitByBus(void *, Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32, Steinberg_int32, Steinberg_Vst_UnitID *unitId)
const Steinberg_Vst_IComponentVtbl * componentVtbl
Definition DSParkVst3.h:397
bool onUiThread() const noexcept
Definition DSParkVst3.h:600
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void *self_)
Definition DSParkVst3.h:515
const Steinberg_Vst_IProcessContextRequirementsVtbl * contextReqVtbl
Definition DSParkVst3.h:400
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetRoutingInfo(void *, Steinberg_Vst_RoutingInfo *, Steinberg_Vst_RoutingInfo *)
Definition DSParkVst3.h:900
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetMidiControllerAssignment(void *, Steinberg_int32 busIndex, Steinberg_int16 channel, Steinberg_Vst_CtrlNumber midiControllerNumber, Steinberg_Vst_ParamID *id)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlSetState(void *, Steinberg_IBStream *)
static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sPlainParamToNormalized(void *, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue plain)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentGetState(void *self_, Steinberg_IBStream *state)
Definition DSParkVst3.h:974
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sInitialize(void *self_, Steinberg_FUnknown *context)
Definition DSParkVst3.h:829
std::atomic< double > midiProxyShadow[HasMidi< P > ? kNumMidiProxies :1]
Definition DSParkVst3.h:419
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlGetState(void *, Steinberg_IBStream *)
static int programIndexOf(Steinberg_Vst_ParamValue normalized) noexcept
Definition DSParkVst3.h:566
static constexpr Steinberg_int32 kBypassIndex
const Steinberg_Vst_IMidiMappingVtbl * midiMapVtbl
Definition DSParkVst3.h:402
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sHasProgramPitchNames(void *, Steinberg_Vst_ProgramListID, Steinberg_int32)
static constexpr size_t kNumParams
Definition DSParkVst3.h:379
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sTerminate(void *self_)
Definition DSParkVst3.h:836
static const Steinberg_Vst_IUnitInfoVtbl kUnitVtbl
void applyAllShadows() noexcept
Definition DSParkVst3.h:537
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanProcessSampleSize(void *, Steinberg_int32 size)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetIoMode(void *, Steinberg_Vst_IoMode)
Definition DSParkVst3.h:845
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlInitialize(void *self_, Steinberg_FUnknown *context)
void applyFactoryPresetIdx(int idx) noexcept
Definition DSParkVst3.h:546
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void *self_, const Steinberg_TUID iid, void **obj)
Definition DSParkVst3.h:505
static int indexOfParamId(Steinberg_Vst_ParamID id) noexcept
Definition DSParkVst3.h:520
static Steinberg_int32 numInputBuses() noexcept
Definition DSParkVst3.h:850
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentHandler(void *self_, Steinberg_Vst_IComponentHandler *handler)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramName(void *, Steinberg_Vst_ProgramListID listId, Steinberg_int32 programIndex, Steinberg_Vst_String128 name)
std::atomic< std::thread::id > uiThread
Definition DSParkVst3.h:437
static const Steinberg_Vst_IComponentVtbl kComponentVtbl
Definition DSParkVst3.h:990
void refreshLatency(Steinberg_int32 extraFlags=0) noexcept
Definition DSParkVst3.h:585
bool applyBlockEvent(const BlockEvent &ev, int blockStart, bool &programChanged) noexcept
Definition DSParkVst3.h:765
void deliverPendingRestart() noexcept
Definition DSParkVst3.h:622
std::atomic< Steinberg_uint32 > refs
Definition DSParkVst3.h:404
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetActive(void *self_, Steinberg_TBool state)
Definition DSParkVst3.h:909
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetParameterCount(void *)
Steinberg_tresult query(const Steinberg_TUID iid, void **obj) noexcept
Definition DSParkVst3.h:469
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSelectUnit(void *, Steinberg_Vst_UnitID unitId)
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void *self_)
Definition DSParkVst3.h:511
static void onUiTick(void *self) noexcept
Definition DSParkVst3.h:631
const Steinberg_Vst_IUnitInfoVtbl * unitVtbl
Definition DSParkVst3.h:401
Steinberg_uint32 addRefImpl() noexcept
Definition DSParkVst3.h:492
static const Steinberg_Vst_IProcessContextRequirementsVtbl kContextReqVtbl
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetUnitCount(void *)
void forwardTransport(const Steinberg_Vst_ProcessContext *ctx) noexcept
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramListInfo(void *, Steinberg_int32 listIndex, Steinberg_Vst_ProgramListInfo *info)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusArrangement(void *self_, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_Vst_SpeakerArrangement *arr)
std::vector< float > silence
Definition DSParkVst3.h:424
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitInfo(void *, Steinberg_int32 unitIndex, Steinberg_Vst_UnitInfo *info)
std::atomic< int > currentProgram
Definition DSParkVst3.h:421
std::atomic< int > cachedLatency
Definition DSParkVst3.h:415
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamStringByValue(void *self_, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized, Steinberg_Vst_String128 out)
static const Steinberg_Vst_IAudioProcessorVtbl kProcessorVtbl
const Steinberg_Vst_IEditControllerVtbl * controllerVtbl
Definition DSParkVst3.h:399
static const Steinberg_Vst_IMidiMappingVtbl kMidiMapVtbl
static const Steinberg_Vst_IEditControllerVtbl kControllerVtbl
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sActivateBus(void *, Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32, Steinberg_TBool)
Definition DSParkVst3.h:904
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramPitchName(void *, Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_int16, Steinberg_Vst_String128)
static constexpr Steinberg_int32 kProgramIndex
static constexpr int kNumPresets
Definition DSParkVst3.h:382
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusInfo(void *self_, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir, Steinberg_int32 index, Steinberg_Vst_BusInfo *bus)
Definition DSParkVst3.h:865
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamValueByString(void *self_, Steinberg_Vst_ParamID id, Steinberg_Vst_TChar *string, Steinberg_Vst_ParamValue *normalized)
static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sNormalizedParamToPlain(void *, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramInfo(void *, Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_Vst_CString, Steinberg_Vst_String128)
std::vector< float > dryR
Definition DSParkVst3.h:423
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetUnitProgramData(void *, Steinberg_int32, Steinberg_int32, struct Steinberg_IBStream *)
Steinberg_Vst_ProcessSetup setup
Definition DSParkVst3.h:407
static Plugin * fromLens(void *iface, int lens) noexcept
Definition DSParkVst3.h:456
void attachToHost(Steinberg_FUnknown *context) noexcept
Definition DSParkVst3.h:638
std::vector< float > dryL
Definition DSParkVst3.h:423
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlTerminate(void *self_)
void collectParameterChanges(Steinberg_Vst_IParameterChanges *changes, Steinberg_int32 numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkVst3.h:661
void collectInputEvents(Steinberg_Vst_IEventList *list, Steinberg_int32 numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkVst3.h:698
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sProcess(void *self_, Steinberg_Vst_ProcessData *data)
std::atomic< bool > bypass
Definition DSParkVst3.h:420
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetTailSamples(void *self_)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetParamNormalized(void *self_, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue value)
static Steinberg_Vst_UnitID SMTG_STDMETHODCALLTYPE sGetSelectedUnit(void *)
Steinberg_uint32 releaseImpl() noexcept
Definition DSParkVst3.h:497
void * lensPtr(int lens) noexcept
Definition DSParkVst3.h:462
static Steinberg_IPlugView *SMTG_STDMETHODCALLTYPE sCreateView(void *self_, Steinberg_FIDString name)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParameterInfo(void *self_, Steinberg_int32 index, Steinberg_Vst_ParameterInfo *info)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentState(void *self_, Steinberg_IBStream *state)
std::atomic< Steinberg_int32 > pendingRestart
Definition DSParkVst3.h:438
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetControllerClassId(void *, Steinberg_TUID)
Definition DSParkVst3.h:842
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetProcessing(void *, Steinberg_TBool)
static constexpr bool kIsInstrument
Definition DSParkVst3.h:380
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetProcessContextRequirements(void *)
const Steinberg_Vst_IAudioProcessorVtbl * processorVtbl
Definition DSParkVst3.h:398
std::atomic< Steinberg_Vst_IComponentHandler * > handler
Definition DSParkVst3.h:431
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetBusCount(void *, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir)
Definition DSParkVst3.h:856
void applyNormalized(int index, double normalized) noexcept
Definition DSParkVst3.h:527
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetBusArrangements(void *self_, Steinberg_Vst_SpeakerArrangement *inputs, Steinberg_int32 numIns, Steinberg_Vst_SpeakerArrangement *outputs, Steinberg_int32 numOuts)
static int midiProxySlot(uint32_t id) noexcept
Definition DSParkVst3.h:817
void detachFromHost() noexcept
Definition DSParkVst3.h:647
void requestRestart(Steinberg_int32 flags) noexcept
Definition DSParkVst3.h:609