DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DSParkVst3.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
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 <cstring>
71#include <new>
72
74
75// -- small helpers -------------------------------------------------------------
76
77inline bool tuidEqual(const Steinberg_TUID a, const Steinberg_TUID b) noexcept
78{
79 return std::memcmp(a, b, sizeof(Steinberg_TUID)) == 0;
80}
81
82inline void asciiToString128(const char* src, Steinberg_Vst_String128 dst) noexcept
83{
84 int i = 0;
85 if (src != nullptr)
86 for (; i < 127 && src[i] != '\0'; ++i)
87 dst[i] = static_cast<Steinberg_char16>(static_cast<unsigned char>(src[i]));
88 dst[i] = 0;
89}
90
91inline void copyAscii(const char* src, char* dst, size_t cap) noexcept
92{
93 std::snprintf(dst, cap, "%s", src ? src : "");
94}
95
97inline bool streamRead(Steinberg_IBStream* s, void* dst, Steinberg_int32 bytes) noexcept
98{
99 auto* p = static_cast<char*>(dst);
100 while (bytes > 0)
101 {
102 Steinberg_int32 got = 0;
103 if (s->lpVtbl->read(s, p, bytes, &got) != Steinberg_kResultOk || got <= 0)
104 return false;
105 p += got;
106 bytes -= got;
107 }
108 return true;
109}
110
111inline bool streamWrite(Steinberg_IBStream* s, const void* src, Steinberg_int32 bytes) noexcept
112{
113 auto* p = static_cast<const char*>(src);
114 while (bytes > 0)
115 {
116 Steinberg_int32 put = 0;
117 if (s->lpVtbl->write(s, const_cast<char*>(p), bytes, &put) != Steinberg_kResultOk
118 || put <= 0)
119 return false;
120 p += put;
121 bytes -= put;
122 }
123 return true;
124}
125
126// -- the plugin object ----------------------------------------------------------
127
128inline constexpr uint32_t kBypassParamId = 0x42595053u; // 'BYPS'
129inline constexpr uint32_t kProgramParamId = 0x5052474Du; // 'PRGM'
130inline constexpr int kBypassRampSamples = 256;
131inline constexpr Steinberg_int32 kPresetProgramListId = 1;
132
133// MIDI controllers travel as proxy parameters in VST3 (the IMidiMapping
134// scheme): one hidden ParamID per (channel, controller). Four controllers
135// cover playable MIDI: mod wheel, sustain, channel pressure, pitch bend.
136inline constexpr uint32_t kMidiProxyBase = 0x4D440000u; // 'MD' marker
137inline constexpr int kMidiProxyControllers[4] = { 1, 64, 128, 129 };
138inline constexpr int kNumMidiProxies = 16 * 4; // channels x controllers
139
140inline constexpr bool isMidiProxyId(uint32_t id) noexcept
141{
142 if ((id & 0xFFFF0000u) != kMidiProxyBase) return false;
143 const uint32_t ctrl = id & 0xFFu;
144 const uint32_t channel = (id >> 8) & 0xFFu;
145 return channel < 16
146 && (ctrl == 1 || ctrl == 64 || ctrl == 128 || ctrl == 129);
147}
148
149inline constexpr uint32_t midiProxyId(int channel, int controller) noexcept
150{
151 return kMidiProxyBase | (static_cast<uint32_t>(channel) << 8)
152 | static_cast<uint32_t>(controller);
153}
154
156inline MidiEvent midiProxyToEvent(uint32_t id, double normalized, int offset) noexcept
157{
158 MidiEvent ev {};
159 ev.channel = static_cast<uint8_t>((id >> 8) & 0x0Fu);
160 ev.sampleOffset = offset;
161 const uint32_t ctrl = id & 0xFFu;
162 if (ctrl == 129)
163 {
165 ev.value = static_cast<float>(normalized * 2.0 - 1.0);
166 }
167 else if (ctrl == 128)
168 {
170 ev.value = static_cast<float>(normalized);
171 }
172 else
173 {
175 ev.note = static_cast<uint8_t>(ctrl);
176 ev.value = static_cast<float>(normalized);
177 }
178 return ev;
179}
180
181template <typename P>
182struct Plugin
183{
184 static constexpr size_t kNumParams = P::parameters.size();
185 static constexpr bool kIsInstrument =
186 P::descriptor.category == Category::Instrument;
187 static constexpr int kNumPresets = factoryPresetCountOf<P>();
188 static_assert(!(kIsInstrument && HasSidechain<P>),
189 "an Instrument has no audio inputs; remove the sidechain "
190 "processBlock or use Category::Fx");
191 static_assert(!kIsInstrument || HasMidi<P>,
192 "an Instrument needs handleMidiEvent (see HasMidi): it has "
193 "no audio input to process");
194 static_assert(paramIdsUnique<P>(),
195 "two parameter ids share a hash32 (or collide with the "
196 "reserved PRGM/BYPS state ids): automation and state would "
197 "cross-wire. Rename one id.");
198
199 // COM lenses - consecutive vtable pointers; queryInterface returns the
200 // address of the matching slot. Standard layout is asserted below. The
201 // last two lenses surface only for the matching capability.
202 const Steinberg_Vst_IComponentVtbl* componentVtbl;
203 const Steinberg_Vst_IAudioProcessorVtbl* processorVtbl;
204 const Steinberg_Vst_IEditControllerVtbl* controllerVtbl;
205 const Steinberg_Vst_IProcessContextRequirementsVtbl* contextReqVtbl;
206 const Steinberg_Vst_IUnitInfoVtbl* unitVtbl;
207 const Steinberg_Vst_IMidiMappingVtbl* midiMapVtbl;
208
209 std::atomic<Steinberg_uint32> refs { 1 };
210
211 P user {};
212 Steinberg_Vst_ProcessSetup setup { 0, 0, 0, 48000.0 };
213 bool prepared = false;
215 int currentChannels = defaultChannelCount<P>(); // negotiated bus width
216
217 std::atomic<double> shadow[kNumParams == 0 ? 1 : kNumParams] {};
218 std::atomic<double> midiProxyShadow[HasMidi<P> ? kNumMidiProxies : 1] {};
219 std::atomic<bool> bypass { false };
220 std::atomic<int> currentProgram { 0 };
221 float bypassMix = 0.0f; // audio-thread crossfade state
222 std::vector<float> dryL, dryR; // pre-process copy for the bypass blend
223 std::vector<float> silence; // stand-in sidechain when not connected
224
225 // Atomic pointer: refreshLatency() reads it from the audio thread while
226 // setComponentHandler writes it from the main thread. Per the VST3
227 // lifecycle the host swaps handlers only while the component is inactive,
228 // so the atomic removes the formally torn read without needing a lock.
229 std::atomic<Steinberg_Vst_IComponentHandler*> handler { nullptr };
230
231 Plugin() noexcept
232 {
239 for (size_t i = 0; i < kNumParams; ++i)
240 shadow[i].store(toNormalized(P::parameters[i], P::parameters[i].defValue),
241 std::memory_order_relaxed);
242 }
243
244 // --- lens recovery ---------------------------------------------------------
245
246 static Plugin* fromLens(void* iface, int lens) noexcept
247 {
248 return reinterpret_cast<Plugin*>(static_cast<char*>(iface)
249 - static_cast<ptrdiff_t>(lens) * sizeof(void*));
250 }
251
252 void* lensPtr(int lens) noexcept
253 {
254 return reinterpret_cast<char*>(this) + static_cast<ptrdiff_t>(lens) * sizeof(void*);
255 }
256
257 // --- shared FUnknown --------------------------------------------------------
258
259 Steinberg_tresult query(const Steinberg_TUID iid, void** obj) noexcept
260 {
261 if (obj == nullptr) return Steinberg_kInvalidArgument;
262 *obj = nullptr;
263 if (tuidEqual(iid, Steinberg_FUnknown_iid)
264 || tuidEqual(iid, Steinberg_IPluginBase_iid)
265 || tuidEqual(iid, Steinberg_Vst_IComponent_iid))
266 *obj = lensPtr(0);
267 else if (tuidEqual(iid, Steinberg_Vst_IAudioProcessor_iid))
268 *obj = lensPtr(1);
269 else if (tuidEqual(iid, Steinberg_Vst_IEditController_iid))
270 *obj = lensPtr(2);
271 else if (tuidEqual(iid, Steinberg_Vst_IProcessContextRequirements_iid))
272 *obj = lensPtr(3);
273 else if (kNumPresets > 0 && tuidEqual(iid, Steinberg_Vst_IUnitInfo_iid))
274 *obj = lensPtr(4);
275 else if (HasMidi<P> && tuidEqual(iid, Steinberg_Vst_IMidiMapping_iid))
276 *obj = lensPtr(5);
277 if (*obj == nullptr) return Steinberg_kNoInterface;
278 refs.fetch_add(1, std::memory_order_relaxed);
279 return Steinberg_kResultOk;
280 }
281
282 Steinberg_uint32 addRefImpl() noexcept
283 {
284 return refs.fetch_add(1, std::memory_order_relaxed) + 1;
285 }
286
287 Steinberg_uint32 releaseImpl() noexcept
288 {
289 const Steinberg_uint32 left = refs.fetch_sub(1, std::memory_order_acq_rel) - 1;
290 if (left == 0) delete this;
291 return left;
292 }
293
294 template <int Lens>
295 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
296 const Steinberg_TUID iid,
297 void** obj)
298 { return fromLens(self_, Lens)->query(iid, obj); }
299
300 template <int Lens>
301 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void* self_)
302 { return fromLens(self_, Lens)->addRefImpl(); }
303
304 template <int Lens>
305 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void* self_)
306 { return fromLens(self_, Lens)->releaseImpl(); }
307
308 // --- parameter plumbing ------------------------------------------------------
309
310 static int indexOfParamId(Steinberg_Vst_ParamID id) noexcept
311 {
312 for (size_t i = 0; i < kNumParams; ++i)
313 if (hash32(P::parameters[i].id) == id) return static_cast<int>(i);
314 return -1;
315 }
316
317 void applyNormalized(int index, double normalized) noexcept
318 {
319 // Host values are untrusted doubles: a NaN would pass both range
320 // clamps, poison the shadow and reach the user's setter. Ignore it.
321 if (normalized != normalized) return;
322 const auto& spec = P::parameters[static_cast<size_t>(index)];
323 shadow[static_cast<size_t>(index)].store(normalized, std::memory_order_relaxed);
324 user.setParameter(index, static_cast<float>(toPlain(spec, normalized)));
325 }
326
327 void applyAllShadows() noexcept
328 {
329 for (size_t i = 0; i < kNumParams; ++i)
330 user.setParameter(static_cast<int>(i),
331 static_cast<float>(toPlain(P::parameters[i],
332 shadow[i].load(std::memory_order_relaxed))));
333 }
334
336 void applyFactoryPresetIdx(int idx) noexcept
337 {
338 if constexpr (kNumPresets > 0)
339 {
340 if (idx < 0 || idx >= kNumPresets) return;
341 for (size_t i = 0; i < kNumParams; ++i)
342 applyNormalized(static_cast<int>(i), presetNormalized<P>(idx, i));
343 currentProgram.store(idx, std::memory_order_relaxed);
344 }
345 else
346 (void) idx;
347 }
348
353 void refreshLatency(Steinberg_int32 extraFlags = 0) noexcept
354 {
355 Steinberg_int32 flags = extraFlags;
356 if constexpr (HasLatency<P>)
357 {
358 const int now = user.getLatency();
359 if (prepared && now != cachedLatency)
360 {
361 cachedLatency = now;
362 flags |= Steinberg_Vst_RestartFlags_kLatencyChanged;
363 }
364 }
365 if (flags != 0)
366 if (auto* h = handler.load(std::memory_order_relaxed); h != nullptr)
367 h->lpVtbl->restartComponent(h, flags);
368 }
369
370 // --- block event plumbing -------------------------------------------------------
371 //
372 // Every host input lands in ONE timestamped stream (parameter points,
373 // bypass points, note events), sorted and applied either at sub-block
374 // boundaries (sample-accurate, the default) or all up front.
375
378 void collectParameterChanges(Steinberg_Vst_IParameterChanges* changes,
379 Steinberg_int32 numSamples,
380 BlockEvent* events, int& count) noexcept
381 {
382 if (changes == nullptr) return;
383 const Steinberg_int32 queues = changes->lpVtbl->getParameterCount(changes);
384 for (Steinberg_int32 q = 0; q < queues; ++q)
385 {
386 Steinberg_Vst_IParamValueQueue* queue =
387 changes->lpVtbl->getParameterData(changes, q);
388 if (queue == nullptr) continue;
389 const Steinberg_int32 points = queue->lpVtbl->getPointCount(queue);
390 if (points <= 0) continue;
391 const Steinberg_Vst_ParamID id = queue->lpVtbl->getParameterId(queue);
392 const Steinberg_int32 first = sampleAccurateOf<P>() ? 0 : points - 1;
393 for (Steinberg_int32 i = first; i < points; ++i)
394 {
395 Steinberg_int32 offset = 0;
396 Steinberg_Vst_ParamValue value = 0.0;
397 if (queue->lpVtbl->getPoint(queue, i, &offset, &value)
398 != Steinberg_kResultOk)
399 continue;
400 if (offset < 0) offset = 0;
401 if (numSamples > 0 && offset >= numSamples) offset = numSamples - 1;
402 BlockEvent ev {};
403 ev.offset = offset;
406 ev.paramId = id;
407 ev.value = value;
408 if (count < kMaxBlockEvents) events[count++] = ev;
409 else events[kMaxBlockEvents - 1] = ev; // overflow: keep the latest
410 }
411 }
412 }
413
415 void collectInputEvents(Steinberg_Vst_IEventList* list,
416 Steinberg_int32 numSamples,
417 BlockEvent* events, int& count) noexcept
418 {
419 if constexpr (HasMidi<P>)
420 {
421 if (list == nullptr) return;
422 const Steinberg_int32 n = list->lpVtbl->getEventCount(list);
423 for (Steinberg_int32 i = 0; i < n; ++i)
424 {
425 Steinberg_Vst_Event raw {};
426 if (list->lpVtbl->getEvent(list, i, &raw) != Steinberg_kResultOk)
427 continue;
428 MidiEvent midi {};
429 if (raw.type == Steinberg_Vst_Event_EventTypes_kNoteOnEvent)
430 {
432 midi.channel = static_cast<uint8_t>(
433 raw.Steinberg_Vst_Event_noteOn.channel & 0x0F);
434 midi.note = static_cast<uint8_t>(
435 raw.Steinberg_Vst_Event_noteOn.pitch & 0x7F);
436 midi.value = raw.Steinberg_Vst_Event_noteOn.velocity;
437 }
438 else if (raw.type == Steinberg_Vst_Event_EventTypes_kNoteOffEvent)
439 {
440 midi.type = MidiEvent::Type::NoteOff;
441 midi.channel = static_cast<uint8_t>(
442 raw.Steinberg_Vst_Event_noteOff.channel & 0x0F);
443 midi.note = static_cast<uint8_t>(
444 raw.Steinberg_Vst_Event_noteOff.pitch & 0x7F);
445 midi.value = raw.Steinberg_Vst_Event_noteOff.velocity;
446 }
447 else if (raw.type == Steinberg_Vst_Event_EventTypes_kPolyPressureEvent)
448 {
450 midi.channel = static_cast<uint8_t>(
451 raw.Steinberg_Vst_Event_polyPressure.channel & 0x0F);
452 midi.note = static_cast<uint8_t>(
453 raw.Steinberg_Vst_Event_polyPressure.pitch & 0x7F);
454 midi.value = raw.Steinberg_Vst_Event_polyPressure.pressure;
455 }
456 else
457 continue;
458 Steinberg_int32 offset = raw.sampleOffset;
459 if (offset < 0) offset = 0;
460 if (numSamples > 0 && offset >= numSamples) offset = numSamples - 1;
461 BlockEvent ev {};
462 ev.offset = offset;
463 ev.kind = BlockEvent::Kind::Midi;
464 ev.midi = midi;
465 if (count < kMaxBlockEvents) events[count++] = ev;
466 else events[kMaxBlockEvents - 1] = ev;
467 }
468 }
469 else
470 {
471 (void) list;
472 (void) numSamples;
473 (void) events;
474 (void) count;
475 }
476 }
477
482 bool applyBlockEvent(const BlockEvent& ev, int blockStart,
483 bool& programChanged) noexcept
484 {
485 switch (ev.kind)
486 {
488 bypass.store(ev.value >= 0.5, std::memory_order_relaxed);
489 return false;
491 if constexpr (HasMidi<P>)
492 {
493 MidiEvent midi = ev.midi;
494 midi.sampleOffset = ev.offset - blockStart;
495 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
496 user.handleMidiEvent(midi);
497 }
498 return false;
500 default:
501 if constexpr (kNumPresets > 0)
502 {
503 if (ev.paramId == kProgramParamId)
504 {
505 applyFactoryPresetIdx(static_cast<int>(
506 ev.value * (kNumPresets - 1) + 0.5));
507 programChanged = true;
508 return true;
509 }
510 }
511 if constexpr (HasMidi<P>)
512 {
513 if (isMidiProxyId(ev.paramId))
514 {
515 const int slot = midiProxySlot(ev.paramId);
516 midiProxyShadow[static_cast<size_t>(slot)].store(
517 ev.value, std::memory_order_relaxed);
518 MidiEvent midi = midiProxyToEvent(ev.paramId, ev.value,
519 ev.offset - blockStart);
520 if (midi.sampleOffset < 0) midi.sampleOffset = 0;
521 user.handleMidiEvent(midi);
522 return false;
523 }
524 }
525 if (const int idx = indexOfParamId(ev.paramId); idx >= 0)
526 {
527 applyNormalized(idx, ev.value);
528 return true;
529 }
530 return false;
531 }
532 }
533
535 static int midiProxySlot(uint32_t id) noexcept
536 {
537 const int channel = static_cast<int>((id >> 8) & 0x0Fu);
538 const uint32_t ctrl = id & 0xFFu;
539 const int slot = ctrl == 1 ? 0 : (ctrl == 64 ? 1 : (ctrl == 128 ? 2 : 3));
540 return channel * 4 + slot;
541 }
542
543 // ==========================================================================
544 // Lens 0 - IComponent (+ IPluginBase)
545 // ==========================================================================
546
547 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sInitialize(void*, Steinberg_FUnknown*)
548 { return Steinberg_kResultOk; }
549
550 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sTerminate(void*)
551 { return Steinberg_kResultOk; }
552
553 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetControllerClassId(void*, Steinberg_TUID)
554 { return Steinberg_kResultFalse; } // single component: no separate controller
555
556 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetIoMode(void*, Steinberg_Vst_IoMode)
557 { return Steinberg_kResultOk; }
558
561 static Steinberg_int32 numInputBuses() noexcept
562 {
563 if (kIsInstrument) return 0;
564 return HasSidechain<P> ? 2 : 1;
565 }
566
567 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetBusCount(void*,
568 Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir)
569 {
570 if (type == Steinberg_Vst_MediaTypes_kEvent)
571 return (HasMidi<P> && dir == Steinberg_Vst_BusDirections_kInput) ? 1 : 0;
572 if (type != Steinberg_Vst_MediaTypes_kAudio) return 0;
573 return dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
574 }
575
576 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusInfo(void* self_,
577 Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir,
578 Steinberg_int32 index, Steinberg_Vst_BusInfo* bus)
579 {
580 if (bus == nullptr) return Steinberg_kInvalidArgument;
581 if (type == Steinberg_Vst_MediaTypes_kEvent)
582 {
583 if (!HasMidi<P> || dir != Steinberg_Vst_BusDirections_kInput
584 || index != 0)
585 return Steinberg_kInvalidArgument;
586 bus->mediaType = Steinberg_Vst_MediaTypes_kEvent;
587 bus->direction = dir;
588 bus->channelCount = 16;
589 bus->busType = Steinberg_Vst_BusTypes_kMain;
590 bus->flags = Steinberg_Vst_BusInfo_BusFlags_kDefaultActive;
591 asciiToString128("MIDI In", bus->name);
592 return Steinberg_kResultOk;
593 }
594 const Steinberg_int32 count =
595 dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
596 if (type != Steinberg_Vst_MediaTypes_kAudio || index < 0 || index >= count)
597 return Steinberg_kInvalidArgument;
598 bus->mediaType = Steinberg_Vst_MediaTypes_kAudio;
599 bus->direction = dir;
600 bus->channelCount = fromLens(self_, 0)->currentChannels;
601 bus->busType = index == 0 ? Steinberg_Vst_BusTypes_kMain
602 : Steinberg_Vst_BusTypes_kAux;
603 bus->flags = Steinberg_Vst_BusInfo_BusFlags_kDefaultActive;
604 asciiToString128(index == 1 ? "Sidechain"
605 : (dir == Steinberg_Vst_BusDirections_kInput ? "Input"
606 : "Output"),
607 bus->name);
608 return Steinberg_kResultOk;
609 }
610
611 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetRoutingInfo(void*,
612 Steinberg_Vst_RoutingInfo*, Steinberg_Vst_RoutingInfo*)
613 { return Steinberg_kNotImplemented; }
614
615 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sActivateBus(void*,
616 Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32,
617 Steinberg_TBool)
618 { return Steinberg_kResultOk; }
619
620 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetActive(void* self_, Steinberg_TBool state)
621 {
622 auto* p = fromLens(self_, 0);
623 if (state)
624 {
625 const int maxBlock = p->setup.maxSamplesPerBlock > 0
626 ? p->setup.maxSamplesPerBlock : 512;
627 const double sr = p->setup.sampleRate > 0.0 ? p->setup.sampleRate : 48000.0;
628 dspark::AudioSpec spec { sr, maxBlock, p->currentChannels };
629 p->user.prepare(spec);
630 if constexpr (HasOfflineMode<P>)
631 p->user.setOfflineRendering(
632 p->setup.processMode == Steinberg_Vst_ProcessModes_kOffline);
633 p->applyAllShadows();
634 p->dryL.assign(static_cast<size_t>(maxBlock), 0.0f);
635 p->dryR.assign(static_cast<size_t>(maxBlock), 0.0f);
636 if constexpr (HasSidechain<P>)
637 p->silence.assign(static_cast<size_t>(maxBlock), 0.0f);
638 p->bypassMix = p->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
639 if constexpr (HasLatency<P>)
640 p->cachedLatency = p->user.getLatency();
641 p->prepared = true;
642 }
643 return Steinberg_kResultOk;
644 }
645
646 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentSetState(void* self_,
647 Steinberg_IBStream* state)
648 {
649 auto* p = fromLens(self_, 0);
650 if (state == nullptr) return Steinberg_kInvalidArgument;
651 // Read whatever the host hands us (chunked); cap defensively at 16 MB.
652 std::vector<uint8_t> blob;
653 char chunk[4096];
654 for (;;)
655 {
656 Steinberg_int32 got = 0;
657 if (state->lpVtbl->read(state, chunk, static_cast<Steinberg_int32>(sizeof(chunk)),
658 &got) != Steinberg_kResultOk || got <= 0)
659 break;
660 blob.insert(blob.end(), chunk, chunk + got);
661 if (blob.size() > (16u << 20)) return Steinberg_kResultFalse;
662 }
663 double norm[kNumParams == 0 ? 1 : kNumParams];
664 for (size_t i = 0; i < kNumParams; ++i)
665 norm[i] = p->shadow[i].load(std::memory_order_relaxed);
666 int program = -1;
667 int bypassState = -1;
668 if (!applyState(p->user, blob.data(), blob.size(), norm, &program,
669 &bypassState))
670 return Steinberg_kResultFalse;
671 for (size_t i = 0; i < kNumParams; ++i)
672 p->applyNormalized(static_cast<int>(i), norm[i]);
673 if (kNumPresets > 0 && program >= 0 && program < kNumPresets)
674 p->currentProgram.store(program, std::memory_order_relaxed);
675 if (bypassState >= 0)
676 p->bypass.store(bypassState != 0, std::memory_order_relaxed);
677 p->refreshLatency(); // restored state may imply a new lookahead
678 return Steinberg_kResultOk;
679 }
680
681 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentGetState(void* self_,
682 Steinberg_IBStream* state)
683 {
684 auto* p = fromLens(self_, 0);
685 if (state == nullptr) return Steinberg_kInvalidArgument;
686 double norm[kNumParams == 0 ? 1 : kNumParams];
687 for (size_t i = 0; i < kNumParams; ++i)
688 norm[i] = p->shadow[i].load(std::memory_order_relaxed);
689 const std::vector<uint8_t> blob = buildState(
690 p->user, norm, kNumParams,
691 kNumPresets > 0 ? p->currentProgram.load(std::memory_order_relaxed) : -1,
692 p->bypass.load(std::memory_order_relaxed) ? 1 : 0);
693 return streamWrite(state, blob.data(), static_cast<Steinberg_int32>(blob.size()))
694 ? Steinberg_kResultOk : Steinberg_kResultFalse;
695 }
696
697 inline static const Steinberg_Vst_IComponentVtbl kComponentVtbl = {
698 &sQuery<0>, &sAddRef<0>, &sRelease<0>,
703 };
704
705 // ==========================================================================
706 // Lens 1 - IAudioProcessor
707 // ==========================================================================
708
711 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetBusArrangements(void* self_,
712 Steinberg_Vst_SpeakerArrangement* inputs, Steinberg_int32 numIns,
713 Steinberg_Vst_SpeakerArrangement* outputs, Steinberg_int32 numOuts)
714 {
715 if (numIns != numInputBuses() || numOuts != 1
716 || (numIns > 0 && inputs == nullptr) || outputs == nullptr)
717 return Steinberg_kResultFalse;
718 const Steinberg_Vst_SpeakerArrangement want = outputs[0];
719 int channels = 0;
720 if (want == Steinberg_Vst_SpeakerArr_kMono) channels = 1;
721 else if (want == Steinberg_Vst_SpeakerArr_kStereo) channels = 2;
722 if (channels == 0 || !supportsChannelCount<P>(channels))
723 return Steinberg_kResultFalse;
724 for (Steinberg_int32 i = 0; i < numIns; ++i)
725 if (inputs[i] != want)
726 return Steinberg_kResultFalse;
727 fromLens(self_, 1)->currentChannels = channels;
728 return Steinberg_kResultTrue;
729 }
730
731 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetBusArrangement(void* self_,
732 Steinberg_Vst_BusDirection dir, Steinberg_int32 index,
733 Steinberg_Vst_SpeakerArrangement* arr)
734 {
735 const Steinberg_int32 count =
736 dir == Steinberg_Vst_BusDirections_kInput ? numInputBuses() : 1;
737 if (index < 0 || index >= count || arr == nullptr)
738 return Steinberg_kInvalidArgument;
739 *arr = fromLens(self_, 1)->currentChannels == 1
740 ? Steinberg_Vst_SpeakerArr_kMono : Steinberg_Vst_SpeakerArr_kStereo;
741 return Steinberg_kResultOk;
742 }
743
744 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanProcessSampleSize(void*,
745 Steinberg_int32 size)
746 {
747 return size == Steinberg_Vst_SymbolicSampleSizes_kSample32
748 ? Steinberg_kResultTrue : Steinberg_kResultFalse;
749 }
750
751 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetLatencySamples(void* self_)
752 {
753 return static_cast<Steinberg_uint32>(fromLens(self_, 1)->cachedLatency);
754 }
755
756 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetupProcessing(void* self_,
757 Steinberg_Vst_ProcessSetup* setup)
758 {
759 if (setup == nullptr) return Steinberg_kInvalidArgument;
760 fromLens(self_, 1)->setup = *setup;
761 return Steinberg_kResultOk;
762 }
763
764 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetProcessing(void*, Steinberg_TBool)
765 { return Steinberg_kResultOk; }
766
768 void forwardTransport(const Steinberg_Vst_ProcessContext* ctx) noexcept
769 {
770 if constexpr (HasTransport<P>)
771 {
772 if (ctx == nullptr) return;
773 TransportInfo t {};
774 const Steinberg_uint32 s = ctx->state;
775 t.playing = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kPlaying) != 0;
776 t.recording = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kRecording) != 0;
777 t.looping = (s & Steinberg_Vst_ProcessContext_StatesAndFlags_kCycleActive) != 0;
778 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kTempoValid) != 0)
779 {
780 t.tempoBpm = ctx->tempo;
781 t.tempoValid = true;
782 }
783 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kProjectTimeMusicValid) != 0)
784 {
785 t.ppqPosition = ctx->projectTimeMusic;
786 t.positionValid = true;
787 }
788 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kBarPositionValid) != 0)
789 t.barStartPpq = ctx->barPositionMusic;
790 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kTimeSigValid) != 0)
791 {
792 t.timeSigNumerator = ctx->timeSigNumerator;
793 t.timeSigDenominator = ctx->timeSigDenominator;
794 t.timeSigValid = true;
795 }
796 if ((s & Steinberg_Vst_ProcessContext_StatesAndFlags_kCycleValid) != 0)
797 {
798 t.loopStartPpq = ctx->cycleStartMusic;
799 t.loopEndPpq = ctx->cycleEndMusic;
800 t.loopValid = true;
801 }
802 user.setTransport(t);
803 }
804 else
805 (void) ctx;
806 }
807
808 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sProcess(void* self_,
809 Steinberg_Vst_ProcessData* data)
810 {
811 auto* p = fromLens(self_, 1);
812 if (data == nullptr) return Steinberg_kInvalidArgument;
813
814 // FTZ/DAZ for the whole callback: DSPark processors guard their own
815 // hot loops, this covers user DSP in hosts that do not set it.
816 dspark::DenormalGuard denormalGuard;
817
818 const Steinberg_int32 n = data->numSamples;
819
820 // One timestamped stream for everything the host handed us.
822 int eventCount = 0;
823 p->collectParameterChanges(data->inputParameterChanges, n, events, eventCount);
824 p->collectInputEvents(data->inputEvents, n, events, eventCount);
825 sortBlockEvents(events, eventCount);
826
827 bool paramsChanged = false;
828 bool programChanged = false;
829
830 const bool canRender = n > 0
831 && data->symbolicSampleSize == Steinberg_Vst_SymbolicSampleSizes_kSample32
832 && data->numOutputs >= 1 && data->outputs != nullptr && p->prepared;
833 if (!canRender)
834 {
835 // Parameter flush (n == 0) or unrenderable block: still apply
836 // every event so automation/state never desynchronises.
837 for (int i = 0; i < eventCount; ++i)
838 paramsChanged |= p->applyBlockEvent(events[i], events[i].offset,
839 programChanged);
840 if (paramsChanged || programChanged)
841 p->refreshLatency(programChanged
842 ? Steinberg_Vst_RestartFlags_kParamValuesChanged : 0);
843 if (n <= 0) return Steinberg_kResultOk;
844 return data->symbolicSampleSize
845 == Steinberg_Vst_SymbolicSampleSizes_kSample32
846 ? Steinberg_kResultOk : Steinberg_kResultFalse;
847 }
848
849 p->forwardTransport(data->processContext);
850
851 auto& outBus = data->outputs[0];
852 float** out = outBus.Steinberg_Vst_AudioBusBuffers_channelBuffers32;
853 if (out == nullptr || outBus.numChannels < 1) return Steinberg_kResultOk;
854 const int width = p->currentChannels;
855 int nCh = outBus.numChannels < width ? outBus.numChannels : width;
856 while (nCh > 0 && out[nCh - 1] == nullptr) --nCh; // degenerate hosts
857 if (nCh < 1) return Steinberg_kResultOk;
858
859 const bool haveIn = !kIsInstrument
860 && data->numInputs >= 1 && data->inputs != nullptr
861 && data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32 != nullptr;
862 float** in = haveIn
863 ? data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32 : nullptr;
864
865 if (static_cast<size_t>(n) > p->dryL.size())
866 return Steinberg_kResultOk; // oversize block: pass through
867
868 // Keep the dry signal for the bypass blend, then process the output
869 // buffers in place (copying input over first when distinct). An
870 // instrument has no input: its output starts cleared (voices ADD)
871 // and its bypass blends toward silence (the dry vectors stay zero).
872 float* dry[2] = { p->dryL.data(), p->dryR.data() };
873 const size_t bytes = sizeof(float) * static_cast<size_t>(n);
874 for (int ch = 0; ch < nCh; ++ch)
875 {
876 if (kIsInstrument)
877 {
878 std::memset(out[ch], 0, bytes);
879 continue;
880 }
881 const float* src = (haveIn && in[ch] != nullptr) ? in[ch] : out[ch];
882 std::memcpy(dry[ch], src, bytes);
883 if (out[ch] != src)
884 std::memcpy(out[ch], src, bytes);
885 }
886
887 // Sidechain: aux bus 1, pre-allocated silence when nothing is
888 // routed (same frame count, no branches user-side). Read-only.
889 float* scPtrs[2] = { nullptr, nullptr };
890 if constexpr (HasSidechain<P>)
891 {
892 if (static_cast<size_t>(n) > p->silence.size())
893 return Steinberg_kResultOk;
894 scPtrs[0] = scPtrs[1] = p->silence.data();
895 if (data->numInputs >= 2 && data->inputs != nullptr
896 && data->inputs[1].Steinberg_Vst_AudioBusBuffers_channelBuffers32
897 != nullptr)
898 {
899 const auto& scBus = data->inputs[1];
900 float** sc = scBus.Steinberg_Vst_AudioBusBuffers_channelBuffers32;
901 const int scCh = scBus.numChannels < 2 ? scBus.numChannels : 2;
902 for (int ch = 0; ch < scCh; ++ch)
903 if (sc[ch] != nullptr)
904 scPtrs[ch] = sc[ch];
905 if (scCh == 1 && sc[0] != nullptr)
906 scPtrs[1] = sc[0]; // mono key feeds both detector ears
907 }
908 }
909
910 // Process in sub-blocks split at quantum-aligned event positions
911 // (sample-accurate default); without splitting, apply everything up
912 // front and run the block in one call.
913 auto processSegment = [&](int start, int length) noexcept {
914 float* sub[2] = { out[0] + start,
915 nCh > 1 ? out[1] + start : out[0] + start };
916 dspark::AudioBufferView<float> view(sub, nCh, length);
917 if constexpr (HasSidechain<P>)
918 {
919 // The key view mirrors the main width (mono main, mono key).
920 float* scSub[2] = { scPtrs[0] + start, scPtrs[1] + start };
921 dspark::AudioBufferView<float> scView(scSub, nCh, length);
922 p->user.processBlock(view, scView);
923 }
924 else
925 p->user.processBlock(view);
926 };
927
928 int evIdx = 0;
929 if (!sampleAccurateOf<P>())
930 {
931 for (; evIdx < eventCount; ++evIdx)
932 paramsChanged |= p->applyBlockEvent(events[evIdx], 0, programChanged);
933 processSegment(0, n);
934 }
935 else
936 {
937 int pos = 0;
938 while (pos < n)
939 {
940 while (evIdx < eventCount
941 && (events[evIdx].offset / kAutomationQuantum)
942 * kAutomationQuantum <= pos)
943 paramsChanged |= p->applyBlockEvent(events[evIdx++], pos,
944 programChanged);
945 int next = n;
946 if (evIdx < eventCount)
947 {
948 const int snapped = (events[evIdx].offset / kAutomationQuantum)
950 if (snapped < next) next = snapped;
951 }
952 if (next <= pos) next = pos + kAutomationQuantum < n
953 ? pos + kAutomationQuantum : n;
954 processSegment(pos, next - pos);
955 pos = next;
956 }
957 for (; evIdx < eventCount; ++evIdx) // safety: events at block end
958 paramsChanged |= p->applyBlockEvent(events[evIdx], n, programChanged);
959 }
960
961 // Soft bypass: short linear crossfade toward the dry signal.
962 const float target = p->bypass.load(std::memory_order_relaxed) ? 1.0f : 0.0f;
963 if (p->bypassMix != target || target > 0.0f)
964 {
965 const float step = 1.0f / static_cast<float>(kBypassRampSamples);
966 float mix = p->bypassMix;
967 for (Steinberg_int32 i = 0; i < n; ++i)
968 {
969 mix += (target > mix) ? step : ((target < mix) ? -step : 0.0f);
970 mix = mix < 0.0f ? 0.0f : (mix > 1.0f ? 1.0f : mix);
971 for (int ch = 0; ch < nCh; ++ch)
972 out[ch][i] += (dry[ch][i] - out[ch][i]) * mix;
973 }
974 p->bypassMix = mix;
975 }
976
977 if (paramsChanged || programChanged)
978 p->refreshLatency(programChanged
979 ? Steinberg_Vst_RestartFlags_kParamValuesChanged : 0);
980
981 outBus.silenceFlags = 0;
982 return Steinberg_kResultOk;
983 }
984
985 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetTailSamples(void* self_)
986 {
987 auto* p = fromLens(self_, 1);
988 if constexpr (HasTail<P>)
989 {
990 const double seconds = p->user.getTailSeconds();
991 if (!(seconds > 0.0)) return 0; // negative/NaN -> no tail
992 const double samples = seconds
993 * (p->setup.sampleRate > 0 ? p->setup.sampleRate : 48000.0);
994 // Infinite or absurd tails map to kInfiniteTail instead of a
995 // double->uint32 overflow (UB).
996 if (samples >= 4294967295.0) return 0xFFFFFFFFu;
997 return static_cast<Steinberg_uint32>(samples);
998 }
999 else
1000 {
1001 (void) p;
1002 return 0;
1003 }
1004 }
1005
1006 inline static const Steinberg_Vst_IAudioProcessorVtbl kProcessorVtbl = {
1007 &sQuery<1>, &sAddRef<1>, &sRelease<1>,
1011 };
1012
1013 // ==========================================================================
1014 // Lens 2 - IEditController (+ IPluginBase)
1015 // ==========================================================================
1016
1017 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlInitialize(void*, Steinberg_FUnknown*)
1018 { return Steinberg_kResultOk; }
1019
1020 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlTerminate(void*)
1021 { return Steinberg_kResultOk; }
1022
1023 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentState(void* self_,
1024 Steinberg_IBStream* state)
1025 {
1026 // Single component: identical to IComponent::setState (lens offset!).
1027 return sComponentSetState(fromLens(self_, 2)->lensPtr(0), state);
1028 }
1029
1030 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlSetState(void*, Steinberg_IBStream*)
1031 { return Steinberg_kResultOk; } // no controller-only state
1032
1033 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlGetState(void*, Steinberg_IBStream*)
1034 { return Steinberg_kResultOk; }
1035
1036 // Parameter index layout: [0, N) the user table, N the bypass, N+1 the
1037 // program change (factory presets only), then the hidden MIDI proxies.
1038 static constexpr Steinberg_int32 kBypassIndex =
1039 static_cast<Steinberg_int32>(kNumParams);
1040 static constexpr Steinberg_int32 kProgramIndex =
1041 kNumPresets > 0 ? kBypassIndex + 1 : -1;
1042 static constexpr Steinberg_int32 kFirstProxyIndex =
1043 kBypassIndex + 1 + (kNumPresets > 0 ? 1 : 0);
1044
1045 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetParameterCount(void*)
1046 {
1048 }
1049
1050 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParameterInfo(void* self_,
1051 Steinberg_int32 index, Steinberg_Vst_ParameterInfo* info)
1052 {
1053 (void) self_;
1054 if (info == nullptr || index < 0 || index >= sGetParameterCount(self_))
1055 return Steinberg_kInvalidArgument;
1056 std::memset(info, 0, sizeof(*info));
1057 if (index == kBypassIndex)
1058 {
1059 info->id = kBypassParamId;
1060 asciiToString128("Bypass", info->title);
1061 asciiToString128("Byp", info->shortTitle);
1062 info->stepCount = 1;
1063 info->defaultNormalizedValue = 0.0;
1064 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate
1065 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsBypass;
1066 return Steinberg_kResultOk;
1067 }
1068 if (kNumPresets > 0 && index == kProgramIndex)
1069 {
1070 info->id = kProgramParamId;
1071 asciiToString128("Program", info->title);
1072 asciiToString128("Prog", info->shortTitle);
1073 info->stepCount = kNumPresets - 1;
1074 info->defaultNormalizedValue = 0.0;
1075 info->unitId = Steinberg_Vst_kRootUnitId;
1076 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate
1077 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsList
1078 | Steinberg_Vst_ParameterInfo_ParameterFlags_kIsProgramChange;
1079 return Steinberg_kResultOk;
1080 }
1081 if (HasMidi<P> && index >= kFirstProxyIndex)
1082 {
1083 // Hidden conduits for IMidiMapping: hosts write MIDI controller
1084 // motion into them; they never show in generic UIs.
1085 const int slot = index - kFirstProxyIndex;
1086 const int channel = slot / 4;
1087 const int ctrl = kMidiProxyControllers[slot % 4];
1088 info->id = midiProxyId(channel, ctrl);
1089 asciiToString128("MIDI", info->title);
1090 asciiToString128("MIDI", info->shortTitle);
1091 info->stepCount = 0;
1092 info->defaultNormalizedValue = ctrl == 129 ? 0.5 : 0.0;
1093 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kIsHidden;
1094 return Steinberg_kResultOk;
1095 }
1096 const auto& spec = P::parameters[static_cast<size_t>(index)];
1097 info->id = hash32(spec.id);
1098 asciiToString128(spec.name, info->title);
1099 asciiToString128(spec.name, info->shortTitle);
1100 asciiToString128(spec.unit, info->units);
1101 info->stepCount = spec.steps;
1102 info->defaultNormalizedValue = toNormalized(spec, spec.defValue);
1103 info->flags = Steinberg_Vst_ParameterInfo_ParameterFlags_kCanAutomate;
1104 return Steinberg_kResultOk;
1105 }
1106
1107 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamStringByValue(void* self_,
1108 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized,
1109 Steinberg_Vst_String128 out)
1110 {
1111 (void) self_;
1112 if (out == nullptr) return Steinberg_kInvalidArgument;
1113 char text[64] = "";
1114 if (id == kBypassParamId)
1115 std::snprintf(text, sizeof(text), "%s", normalized >= 0.5 ? "On" : "Off");
1116 else if (kNumPresets > 0 && id == kProgramParamId)
1117 {
1118 if constexpr (HasFactoryPresets<P>)
1119 {
1120 int idx = static_cast<int>(normalized * (kNumPresets - 1) + 0.5);
1121 idx = idx < 0 ? 0 : (idx >= kNumPresets ? kNumPresets - 1 : idx);
1122 std::snprintf(text, sizeof(text), "%s",
1123 P::factoryPresets[static_cast<size_t>(idx)].name);
1124 }
1125 }
1126 else if (HasMidi<P> && isMidiProxyId(id))
1127 std::snprintf(text, sizeof(text), "%.3f", normalized);
1128 else
1129 {
1130 const int idx = indexOfParamId(id);
1131 if (idx < 0) return Steinberg_kInvalidArgument;
1132 const auto& spec = P::parameters[static_cast<size_t>(idx)];
1133 formatValue(spec, toPlain(spec, normalized), text, sizeof(text));
1134 }
1135 asciiToString128(text, out);
1136 return Steinberg_kResultOk;
1137 }
1138
1139 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetParamValueByString(void* self_,
1140 Steinberg_Vst_ParamID id, Steinberg_Vst_TChar* string,
1141 Steinberg_Vst_ParamValue* normalized)
1142 {
1143 (void) self_;
1144 if (string == nullptr || normalized == nullptr) return Steinberg_kInvalidArgument;
1145 char ascii[64];
1146 int i = 0;
1147 for (; i < 63 && string[i] != 0; ++i)
1148 ascii[i] = static_cast<char>(string[i]);
1149 ascii[i] = '\0';
1150 const int toggle = parseToggleText(ascii);
1151 if (id == kBypassParamId)
1152 {
1153 *normalized = toggle >= 0 ? toggle
1154 : (std::strtod(ascii, nullptr) >= 0.5 ? 1.0 : 0.0);
1155 return Steinberg_kResultOk;
1156 }
1157 if (kNumPresets > 0 && id == kProgramParamId)
1158 {
1159 if constexpr (HasFactoryPresets<P>)
1160 {
1161 for (int presetIdx = 0; presetIdx < kNumPresets; ++presetIdx)
1162 if (std::strcmp(ascii,
1163 P::factoryPresets[static_cast<size_t>(presetIdx)].name)
1164 == 0)
1165 {
1166 *normalized = kNumPresets > 1
1167 ? static_cast<double>(presetIdx) / (kNumPresets - 1)
1168 : 0.0;
1169 return Steinberg_kResultOk;
1170 }
1171 }
1172 const double v = std::strtod(ascii, nullptr);
1173 *normalized = kNumPresets > 1 ? v / (kNumPresets - 1) : 0.0;
1174 return Steinberg_kResultOk;
1175 }
1176 if (HasMidi<P> && isMidiProxyId(id))
1177 {
1178 const double v = std::strtod(ascii, nullptr);
1179 *normalized = v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v);
1180 return Steinberg_kResultOk;
1181 }
1182 const int idx = indexOfParamId(id);
1183 if (idx < 0) return Steinberg_kInvalidArgument;
1184 const Param& spec = P::parameters[static_cast<size_t>(idx)];
1185 if (spec.steps == 1 && toggle >= 0)
1186 *normalized = toggle;
1187 else
1188 *normalized = toNormalized(spec, std::strtod(ascii, nullptr));
1189 return Steinberg_kResultOk;
1190 }
1191
1192 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sNormalizedParamToPlain(void*,
1193 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue normalized)
1194 {
1195 if (id == kBypassParamId) return normalized >= 0.5 ? 1.0 : 0.0;
1196 if (kNumPresets > 0 && id == kProgramParamId)
1197 return static_cast<double>(
1198 static_cast<int>(normalized * (kNumPresets - 1) + 0.5));
1199 if (HasMidi<P> && isMidiProxyId(id)) return normalized;
1200 const int idx = indexOfParamId(id);
1201 return idx < 0 ? 0.0 : toPlain(P::parameters[static_cast<size_t>(idx)], normalized);
1202 }
1203
1204 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sPlainParamToNormalized(void*,
1205 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue plain)
1206 {
1207 if (id == kBypassParamId) return plain >= 0.5 ? 1.0 : 0.0;
1208 if (kNumPresets > 0 && id == kProgramParamId)
1209 return kNumPresets > 1 ? plain / (kNumPresets - 1) : 0.0;
1210 if (HasMidi<P> && isMidiProxyId(id)) return plain;
1211 const int idx = indexOfParamId(id);
1212 return idx < 0 ? 0.0 : toNormalized(P::parameters[static_cast<size_t>(idx)], plain);
1213 }
1214
1215 static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sGetParamNormalized(void* self_,
1216 Steinberg_Vst_ParamID id)
1217 {
1218 auto* p = fromLens(self_, 2);
1219 if (id == kBypassParamId)
1220 return p->bypass.load(std::memory_order_relaxed) ? 1.0 : 0.0;
1221 if (kNumPresets > 0 && id == kProgramParamId)
1222 return kNumPresets > 1
1223 ? static_cast<double>(p->currentProgram.load(std::memory_order_relaxed))
1224 / (kNumPresets - 1)
1225 : 0.0;
1226 if constexpr (HasMidi<P>)
1227 {
1228 if (isMidiProxyId(id))
1229 return p->midiProxyShadow[static_cast<size_t>(midiProxySlot(id))]
1230 .load(std::memory_order_relaxed);
1231 }
1232 const int idx = indexOfParamId(id);
1233 return idx < 0 ? 0.0
1234 : p->shadow[static_cast<size_t>(idx)].load(std::memory_order_relaxed);
1235 }
1236
1237 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetParamNormalized(void* self_,
1238 Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue value)
1239 {
1240 auto* p = fromLens(self_, 2);
1241 if (id == kBypassParamId)
1242 {
1243 p->bypass.store(value >= 0.5, std::memory_order_relaxed);
1244 return Steinberg_kResultOk;
1245 }
1246 if (kNumPresets > 0 && id == kProgramParamId)
1247 {
1248 // A program change from the host UI: apply the preset and tell
1249 // the host every other parameter moved (main-thread call).
1250 p->applyFactoryPresetIdx(
1251 static_cast<int>(value * (kNumPresets - 1) + 0.5));
1252 p->refreshLatency(Steinberg_Vst_RestartFlags_kParamValuesChanged);
1253 return Steinberg_kResultOk;
1254 }
1255 if constexpr (HasMidi<P>)
1256 {
1257 // Live MIDI rides the process() queues; a main-thread write only
1258 // keeps the proxy shadow coherent for host round-trips.
1259 if (isMidiProxyId(id))
1260 {
1261 p->midiProxyShadow[static_cast<size_t>(midiProxySlot(id))]
1262 .store(value, std::memory_order_relaxed);
1263 return Steinberg_kResultOk;
1264 }
1265 }
1266 const int idx = indexOfParamId(id);
1267 if (idx < 0) return Steinberg_kInvalidArgument;
1268 p->applyNormalized(idx, value); // user setters are atomic by contract
1269 p->refreshLatency();
1270 return Steinberg_kResultOk;
1271 }
1272
1273 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetComponentHandler(void* self_,
1274 Steinberg_Vst_IComponentHandler* handler)
1275 {
1276 auto* p = fromLens(self_, 2);
1277 auto* old = p->handler.load(std::memory_order_relaxed);
1278 if (old != nullptr)
1279 reinterpret_cast<Steinberg_FUnknown*>(old)->lpVtbl->release(old);
1280 p->handler.store(handler, std::memory_order_relaxed);
1281 if (handler != nullptr)
1282 reinterpret_cast<Steinberg_FUnknown*>(handler)->lpVtbl->addRef(handler);
1283 return Steinberg_kResultOk;
1284 }
1285
1286 static Steinberg_IPlugView* SMTG_STDMETHODCALLTYPE sCreateView(void* self_,
1287 Steinberg_FIDString name);
1288
1299
1300 // ==========================================================================
1301 // Lens 3 - IProcessContextRequirements
1302 // ==========================================================================
1303
1304 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetProcessContextRequirements(void*)
1305 {
1306 if constexpr (HasTransport<P>)
1307 return Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTempo
1308 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTransportState
1309 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedProjectTimeMusic
1310 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedBarPositionMusic
1311 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedTimeSignature
1312 | Steinberg_Vst_IProcessContextRequirements_Flags_kNeedCycleMusic;
1313 else
1314 return 0;
1315 }
1316
1317 inline static const Steinberg_Vst_IProcessContextRequirementsVtbl kContextReqVtbl = {
1318 &sQuery<3>, &sAddRef<3>, &sRelease<3>,
1320 };
1321
1322 // ==========================================================================
1323 // Lens 4 - IUnitInfo (factory presets as a program list; surfaced by
1324 // queryInterface only when the class declares factoryPresets)
1325 // ==========================================================================
1326
1327 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetUnitCount(void*) { return 1; }
1328
1329 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitInfo(void*,
1330 Steinberg_int32 unitIndex, Steinberg_Vst_UnitInfo* info)
1331 {
1332 if (unitIndex != 0 || info == nullptr) return Steinberg_kInvalidArgument;
1333 std::memset(info, 0, sizeof(*info));
1334 info->id = Steinberg_Vst_kRootUnitId;
1335 info->parentUnitId = Steinberg_Vst_kNoParentUnitId;
1336 info->programListId = kNumPresets > 0 ? kPresetProgramListId
1337 : Steinberg_Vst_kNoProgramListId;
1338 asciiToString128("Root", info->name);
1339 return Steinberg_kResultOk;
1340 }
1341
1342 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetProgramListCount(void*)
1343 {
1344 return kNumPresets > 0 ? 1 : 0;
1345 }
1346
1347 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramListInfo(void*,
1348 Steinberg_int32 listIndex, Steinberg_Vst_ProgramListInfo* info)
1349 {
1350 if (listIndex != 0 || info == nullptr || kNumPresets == 0)
1351 return Steinberg_kInvalidArgument;
1352 std::memset(info, 0, sizeof(*info));
1353 info->id = kPresetProgramListId;
1354 info->programCount = kNumPresets;
1355 asciiToString128("Factory Presets", info->name);
1356 return Steinberg_kResultOk;
1357 }
1358
1359 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramName(void*,
1360 Steinberg_Vst_ProgramListID listId, Steinberg_int32 programIndex,
1361 Steinberg_Vst_String128 name)
1362 {
1363 if constexpr (HasFactoryPresets<P>)
1364 {
1365 if (listId != kPresetProgramListId || name == nullptr
1366 || programIndex < 0 || programIndex >= kNumPresets)
1367 return Steinberg_kInvalidArgument;
1369 P::factoryPresets[static_cast<size_t>(programIndex)].name, name);
1370 return Steinberg_kResultOk;
1371 }
1372 else
1373 {
1374 (void) listId;
1375 (void) programIndex;
1376 (void) name;
1377 return Steinberg_kInvalidArgument;
1378 }
1379 }
1380
1381 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramInfo(void*,
1382 Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_Vst_CString,
1383 Steinberg_Vst_String128)
1384 { return Steinberg_kResultFalse; }
1385
1386 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sHasProgramPitchNames(void*,
1387 Steinberg_Vst_ProgramListID, Steinberg_int32)
1388 { return Steinberg_kResultFalse; }
1389
1390 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetProgramPitchName(void*,
1391 Steinberg_Vst_ProgramListID, Steinberg_int32, Steinberg_int16,
1392 Steinberg_Vst_String128)
1393 { return Steinberg_kResultFalse; }
1394
1395 static Steinberg_Vst_UnitID SMTG_STDMETHODCALLTYPE sGetSelectedUnit(void*)
1396 { return Steinberg_Vst_kRootUnitId; }
1397
1398 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSelectUnit(void*,
1399 Steinberg_Vst_UnitID unitId)
1400 { return unitId == Steinberg_Vst_kRootUnitId ? Steinberg_kResultOk
1401 : Steinberg_kInvalidArgument; }
1402
1403 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitByBus(void*,
1404 Steinberg_Vst_MediaType, Steinberg_Vst_BusDirection, Steinberg_int32,
1405 Steinberg_int32, Steinberg_Vst_UnitID* unitId)
1406 {
1407 if (unitId == nullptr) return Steinberg_kInvalidArgument;
1408 *unitId = Steinberg_Vst_kRootUnitId;
1409 return Steinberg_kResultOk;
1410 }
1411
1412 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetUnitProgramData(void*,
1413 Steinberg_int32, Steinberg_int32, struct Steinberg_IBStream*)
1414 { return Steinberg_kNotImplemented; }
1415
1416 inline static const Steinberg_Vst_IUnitInfoVtbl kUnitVtbl = {
1417 &sQuery<4>, &sAddRef<4>, &sRelease<4>,
1422 };
1423
1424 // ==========================================================================
1425 // Lens 5 - IMidiMapping (pitch bend / mod / sustain / channel pressure
1426 // arrive as proxy-parameter automation; surfaced only for HasMidi)
1427 // ==========================================================================
1428
1429 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetMidiControllerAssignment(void*,
1430 Steinberg_int32 busIndex, Steinberg_int16 channel,
1431 Steinberg_Vst_CtrlNumber midiControllerNumber, Steinberg_Vst_ParamID* id)
1432 {
1433 if (!HasMidi<P> || id == nullptr || busIndex != 0
1434 || channel < 0 || channel >= 16)
1435 return Steinberg_kResultFalse;
1436 const int ctrl = midiControllerNumber;
1437 if (ctrl != 1 && ctrl != 64 && ctrl != 128 && ctrl != 129)
1438 return Steinberg_kResultFalse;
1439 *id = midiProxyId(channel, ctrl);
1440 return Steinberg_kResultTrue;
1441 }
1442
1443 inline static const Steinberg_Vst_IMidiMappingVtbl kMidiMapVtbl = {
1444 &sQuery<5>, &sAddRef<5>, &sRelease<5>,
1446 };
1447};
1448
1449// -- the editor view (WebView editor layer) ---------------------------------------
1450//
1451// Compiled only when plugin/webview/DSParkWebViewEditor.h was included before
1452// this header, and created only for plugin classes that declare an editor.
1453// Without either, createView answers nullptr and hosts show their generic UI.
1454
1455#if defined(DSPARK_PLUGIN_WEBVIEW)
1456
1462template <typename P>
1463struct View
1464{
1465 static constexpr size_t kNumParams = Plugin<P>::kNumParams;
1466
1467 // COM lenses, same layout trick as the plugin object. The timer lens is
1468 // Linux-only: it receives the host IRunLoop ticks that drive GTK.
1469 const Steinberg_IPlugViewVtbl* viewVtbl;
1470 const Steinberg_IPlugViewContentScaleSupportVtbl* scaleVtbl;
1471#if defined(__linux__)
1472 const Steinberg_Linux_ITimerHandlerVtbl* timerVtbl;
1473#endif
1474
1475 std::atomic<Steinberg_uint32> refs { 1 };
1476 Plugin<P>* owner;
1477 Steinberg_IPlugFrame* frame = nullptr;
1478 webview_ui::Editor<P> editor;
1479 int width; // current physical size (host units)
1480 int height;
1481 double scale = 1.0;
1482 bool correctingSize = false; // re-entrancy guard for resizeView round-trips
1483 bool editActive[kNumParams == 0 ? 1 : kNumParams] {};
1484#if defined(__linux__)
1485 Steinberg_Linux_IRunLoop* runLoop = nullptr;
1486 bool timerRegistered = false;
1487#endif
1488
1489 explicit View(Plugin<P>* plugin) noexcept : owner(plugin)
1490 {
1491 viewVtbl = &kViewVtbl;
1492 scaleVtbl = &kScaleVtbl;
1493#if defined(__linux__)
1494 timerVtbl = &kTimerVtbl;
1495#endif
1496 owner->addRefImpl();
1497 const EditorSize logical = editorSizeOf<P>();
1498 width = logical.width;
1499 height = logical.height;
1500 }
1501
1502 ~View()
1503 {
1504#if defined(__linux__)
1505 releaseRunLoop();
1506#endif
1507 editor.destroy();
1508 if (frame != nullptr)
1509 reinterpret_cast<Steinberg_FUnknown*>(frame)->lpVtbl->release(frame);
1510 owner->releaseImpl();
1511 }
1512
1513#if defined(__linux__)
1514
1515 // --- host run loop: GTK breathes only when pumped from here ---------------------
1516
1517 void acquireRunLoop() noexcept
1518 {
1519 if (frame == nullptr || runLoop != nullptr) return;
1520 void* loop = nullptr;
1521 if (reinterpret_cast<Steinberg_FUnknown*>(frame)->lpVtbl->queryInterface(
1522 frame, Steinberg_Linux_IRunLoop_iid, &loop) == Steinberg_kResultOk)
1523 runLoop = static_cast<Steinberg_Linux_IRunLoop*>(loop);
1524 }
1525
1526 void startTimer() noexcept
1527 {
1528 acquireRunLoop();
1529 if (runLoop != nullptr && !timerRegistered && editor.created())
1530 timerRegistered = runLoop->lpVtbl->registerTimer(runLoop,
1531 reinterpret_cast<Steinberg_Linux_ITimerHandler*>(lensPtr(2)),
1532 33) == Steinberg_kResultOk;
1533 }
1534
1535 void stopTimer() noexcept
1536 {
1537 if (runLoop != nullptr && timerRegistered)
1538 runLoop->lpVtbl->unregisterTimer(runLoop,
1539 reinterpret_cast<Steinberg_Linux_ITimerHandler*>(lensPtr(2)));
1540 timerRegistered = false;
1541 }
1542
1543 void releaseRunLoop() noexcept
1544 {
1545 stopTimer();
1546 if (runLoop != nullptr)
1547 {
1548 reinterpret_cast<Steinberg_FUnknown*>(runLoop)->lpVtbl->release(runLoop);
1549 runLoop = nullptr;
1550 }
1551 }
1552
1553 static void SMTG_STDMETHODCALLTYPE sOnTimer(void* self_)
1554 {
1555 fromLens(self_, 2)->editor.pump();
1556 }
1557
1558#endif // __linux__
1559
1560 static View* fromLens(void* iface, int lens) noexcept
1561 {
1562 return reinterpret_cast<View*>(static_cast<char*>(iface)
1563 - static_cast<ptrdiff_t>(lens) * sizeof(void*));
1564 }
1565
1566 void* lensPtr(int lens) noexcept
1567 {
1568 return reinterpret_cast<char*>(this) + static_cast<ptrdiff_t>(lens) * sizeof(void*);
1569 }
1570
1571 static const char* platformType() noexcept
1572 {
1573#if defined(_WIN32)
1574 return Steinberg_kPlatformTypeHWND;
1575#elif defined(__APPLE__)
1576 return Steinberg_kPlatformTypeNSView;
1577#else
1578 return Steinberg_kPlatformTypeX11EmbedWindowID;
1579#endif
1580 }
1581
1582 // --- editor -> host/DSP bridge ------------------------------------------------
1583
1584 static void cbSetParam(void* context, int index, double plain) noexcept
1585 {
1586 auto* view = static_cast<View*>(context);
1587 const Param& spec = P::parameters[static_cast<size_t>(index)];
1588 const double normalized = toNormalized(spec, plain);
1589 view->owner->applyNormalized(index, normalized);
1590 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1591 {
1592 const Steinberg_Vst_ParamID id = hash32(spec.id);
1593 if (view->editActive[static_cast<size_t>(index)])
1594 handler->lpVtbl->performEdit(handler, id, normalized);
1595 else
1596 {
1597 // Edits outside an explicit gesture still need one for host undo.
1598 handler->lpVtbl->beginEdit(handler, id);
1599 handler->lpVtbl->performEdit(handler, id, normalized);
1600 handler->lpVtbl->endEdit(handler, id);
1601 }
1602 }
1603 }
1604
1605 static void cbBeginEdit(void* context, int index) noexcept
1606 {
1607 auto* view = static_cast<View*>(context);
1608 view->editActive[static_cast<size_t>(index)] = true;
1609 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1610 handler->lpVtbl->beginEdit(handler,
1611 hash32(P::parameters[static_cast<size_t>(index)].id));
1612 }
1613
1614 static void cbEndEdit(void* context, int index) noexcept
1615 {
1616 auto* view = static_cast<View*>(context);
1617 view->editActive[static_cast<size_t>(index)] = false;
1618 if (auto* handler = view->owner->handler.load(std::memory_order_relaxed))
1619 handler->lpVtbl->endEdit(handler,
1620 hash32(P::parameters[static_cast<size_t>(index)].id));
1621 }
1622
1623 // --- FUnknown -------------------------------------------------------------------
1624
1625 Steinberg_tresult query(const Steinberg_TUID iid, void** obj) noexcept
1626 {
1627 if (obj == nullptr) return Steinberg_kInvalidArgument;
1628 *obj = nullptr;
1629 if (tuidEqual(iid, Steinberg_FUnknown_iid)
1630 || tuidEqual(iid, Steinberg_IPlugView_iid))
1631 *obj = lensPtr(0);
1632 else if (tuidEqual(iid, Steinberg_IPlugViewContentScaleSupport_iid))
1633 *obj = lensPtr(1);
1634#if defined(__linux__)
1635 else if (tuidEqual(iid, Steinberg_Linux_ITimerHandler_iid))
1636 *obj = lensPtr(2);
1637#endif
1638 if (*obj == nullptr) return Steinberg_kNoInterface;
1639 refs.fetch_add(1, std::memory_order_relaxed);
1640 return Steinberg_kResultOk;
1641 }
1642
1643 template <int Lens>
1644 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
1645 const Steinberg_TUID iid,
1646 void** obj)
1647 { return fromLens(self_, Lens)->query(iid, obj); }
1648
1649 template <int Lens>
1650 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void* self_)
1651 { return fromLens(self_, Lens)->refs.fetch_add(1, std::memory_order_relaxed) + 1; }
1652
1653 template <int Lens>
1654 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void* self_)
1655 {
1656 auto* view = fromLens(self_, Lens);
1657 const Steinberg_uint32 left = view->refs.fetch_sub(1, std::memory_order_acq_rel) - 1;
1658 if (left == 0) delete view;
1659 return left;
1660 }
1661
1662 // --- IPlugView --------------------------------------------------------------------
1663
1664 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sIsPlatformTypeSupported(void*,
1665 Steinberg_FIDString type)
1666 {
1667 return (webview_ui::Editor<P>::available() && type != nullptr
1668 && std::strcmp(type, platformType()) == 0)
1669 ? Steinberg_kResultTrue : Steinberg_kResultFalse;
1670 }
1671
1672 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sAttached(void* self_, void* parent,
1673 Steinberg_FIDString type)
1674 {
1675 auto* view = fromLens(self_, 0);
1676 webview_ui::debugLog("vst3 attached(parent=%p type=%s) negotiated=%dx%d scale=%.2f",
1677 parent, type != nullptr ? type : "?",
1678 view->width, view->height, view->scale);
1679 if (sIsPlatformTypeSupported(self_, type) != Steinberg_kResultTrue
1680 || parent == nullptr)
1681 return Steinberg_kResultFalse;
1682 const webview_ui::HostCallbacks callbacks {
1683 view, &cbSetParam, &cbBeginEdit, &cbEndEdit
1684 };
1685 if (!view->editor.create(parent, view->owner->shadow, callbacks))
1686 return Steinberg_kResultFalse;
1687 // Fill the box the host ACTUALLY built - hosts differ in whether
1688 // getSize/setContentScaleFactor/attached arrive in spec order, so the
1689 // parent's real client size wins over the negotiated one.
1690 int parentW = 0, parentH = 0;
1691 if (view->editor.queryParentSize(parentW, parentH))
1692 {
1693 view->width = parentW;
1694 view->height = parentH;
1695 }
1696 view->editor.setBounds(view->width, view->height);
1697 view->editor.setVisible(true);
1698#if defined(__linux__)
1699 // GTK breathes only when pumped from the host's run loop; without a
1700 // usable IRunLoop the page would freeze, so fall back to generic UI.
1701 view->startTimer();
1702 if (!view->timerRegistered)
1703 {
1704 webview_ui::debugLog("vst3 attached: no usable IRunLoop -> no editor");
1705 view->editor.destroy();
1706 return Steinberg_kResultFalse;
1707 }
1708#endif
1709 return Steinberg_kResultOk;
1710 }
1711
1712 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sRemoved(void* self_)
1713 {
1714 auto* view = fromLens(self_, 0);
1715#if defined(__linux__)
1716 view->stopTimer();
1717#endif
1718 view->editor.destroy();
1719 return Steinberg_kResultOk;
1720 }
1721
1722 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnWheel(void*, float)
1723 { return Steinberg_kResultFalse; }
1724
1725 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnKeyDown(void*, Steinberg_char16,
1726 Steinberg_int16, Steinberg_int16)
1727 { return Steinberg_kResultFalse; }
1728
1729 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnKeyUp(void*, Steinberg_char16,
1730 Steinberg_int16, Steinberg_int16)
1731 { return Steinberg_kResultFalse; }
1732
1733 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetSize(void* self_,
1734 Steinberg_ViewRect* size)
1735 {
1736 auto* view = fromLens(self_, 0);
1737 if (size == nullptr) return Steinberg_kInvalidArgument;
1738 size->left = 0;
1739 size->top = 0;
1740 size->right = view->width;
1741 size->bottom = view->height;
1742 webview_ui::debugLog("vst3 getSize -> %dx%d", view->width, view->height);
1743 return Steinberg_kResultOk;
1744 }
1745
1746 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnSize(void* self_,
1747 Steinberg_ViewRect* newSize)
1748 {
1749 auto* view = fromLens(self_, 0);
1750 if (newSize == nullptr) return Steinberg_kInvalidArgument;
1751 const double proposedW = newSize->right - newSize->left;
1752 const double proposedH = newSize->bottom - newSize->top;
1753 double w = proposedW;
1754 double h = proposedH;
1755 // Enforce the resize policy HERE, not only in checkSizeConstraint:
1756 // some hosts drag-resize freely and only report the result. When the
1757 // host overshot, apply the constrained size and ask it to follow.
1758 if (!view->correctingSize)
1759 constrainEditorSize<P>(w, h, view->scale);
1760 webview_ui::debugLog("vst3 onSize %.0fx%.0f -> %.0fx%.0f%s",
1761 proposedW, proposedH, w, h,
1762 view->correctingSize ? " (correction round-trip)" : "");
1763 view->width = static_cast<int>(w + 0.5);
1764 view->height = static_cast<int>(h + 0.5);
1765 view->editor.setBounds(view->width, view->height);
1766 if (!view->correctingSize && view->frame != nullptr
1767 && (view->width - proposedW > 1.0 || proposedW - view->width > 1.0
1768 || view->height - proposedH > 1.0 || proposedH - view->height > 1.0))
1769 {
1770 view->correctingSize = true;
1771 Steinberg_ViewRect corrected { 0, 0, view->width, view->height };
1772 view->frame->lpVtbl->resizeView(view->frame,
1773 reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0)), &corrected);
1774 view->correctingSize = false;
1775 }
1776 return Steinberg_kResultOk;
1777 }
1778
1779 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sOnFocus(void*, Steinberg_TBool)
1780 { return Steinberg_kResultOk; }
1781
1782 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetFrame(void* self_,
1783 Steinberg_IPlugFrame* frame)
1784 {
1785 auto* view = fromLens(self_, 0);
1786#if defined(__linux__)
1787 view->releaseRunLoop(); // the run loop belongs to the old frame
1788#endif
1789 if (view->frame != nullptr)
1790 reinterpret_cast<Steinberg_FUnknown*>(view->frame)
1791 ->lpVtbl->release(view->frame);
1792 view->frame = frame;
1793 if (view->frame != nullptr)
1794 reinterpret_cast<Steinberg_FUnknown*>(view->frame)
1795 ->lpVtbl->addRef(view->frame);
1796#if defined(__linux__)
1797 if (view->editor.created())
1798 view->startTimer(); // frame arrived after attach (rare order)
1799#endif
1800 return Steinberg_kResultOk;
1801 }
1802
1803 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanResize(void*)
1804 {
1805 return editorResizeOf<P>() != EditorResize::Fixed ? Steinberg_kResultTrue
1806 : Steinberg_kResultFalse;
1807 }
1808
1809 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCheckSizeConstraint(void* self_,
1810 Steinberg_ViewRect* rect)
1811 {
1812 auto* view = fromLens(self_, 0);
1813 if (rect == nullptr) return Steinberg_kInvalidArgument;
1814 const double proposedW = rect->right - rect->left;
1815 const double proposedH = rect->bottom - rect->top;
1816 double w = proposedW;
1817 double h = proposedH;
1818 constrainEditorSize<P>(w, h, view->scale);
1819 rect->right = rect->left + static_cast<Steinberg_int32>(w + 0.5);
1820 rect->bottom = rect->top + static_cast<Steinberg_int32>(h + 0.5);
1821 webview_ui::debugLog("vst3 checkSizeConstraint %.0fx%.0f -> %.0fx%.0f",
1822 proposedW, proposedH, w, h);
1823 return Steinberg_kResultTrue;
1824 }
1825
1826 inline static const Steinberg_IPlugViewVtbl kViewVtbl = {
1827 &sQuery<0>, &sAddRef<0>, &sRelease<0>,
1828 &sIsPlatformTypeSupported, &sAttached, &sRemoved,
1829 &sOnWheel, &sOnKeyDown, &sOnKeyUp,
1830 &sGetSize, &sOnSize, &sOnFocus,
1831 &sSetFrame, &sCanResize, &sCheckSizeConstraint
1832 };
1833
1834 // --- IPlugViewContentScaleSupport ----------------------------------------------
1835
1836 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetContentScaleFactor(void* self_,
1837 Steinberg_IPlugViewContentScaleSupport_ScaleFactor factor)
1838 {
1839 auto* view = fromLens(self_, 1);
1840 if (!(factor > 0.0f)) return Steinberg_kInvalidArgument; // NaN included
1841 const double previous = view->scale;
1842 view->scale = factor;
1843 webview_ui::debugLog("vst3 setContentScaleFactor %.2f (was %.2f)",
1844 view->scale, previous);
1845 // Rescale the negotiated physical size (the page itself never zooms:
1846 // the web engine applies the window DPI to CSS pixels on its own).
1847 view->width = static_cast<int>(view->width * (view->scale / previous) + 0.5);
1848 view->height = static_cast<int>(view->height * (view->scale / previous) + 0.5);
1849 if (view->editor.created() && view->frame != nullptr)
1850 {
1851 // Already on screen (e.g. dragged to another monitor): ask the
1852 // host to rebuild the window at the new physical size.
1853 Steinberg_ViewRect rect { 0, 0, view->width, view->height };
1854 view->frame->lpVtbl->resizeView(view->frame,
1855 reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0)), &rect);
1856 }
1857 return Steinberg_kResultOk;
1858 }
1859
1860 inline static const Steinberg_IPlugViewContentScaleSupportVtbl kScaleVtbl = {
1861 &sQuery<1>, &sAddRef<1>, &sRelease<1>,
1862 &sSetContentScaleFactor
1863 };
1864
1865#if defined(__linux__)
1866 // Defined after the FUnknown thunks: a static member INITIALIZER has no
1867 // complete-class context, so every name it uses must be declared above.
1868 inline static const Steinberg_Linux_ITimerHandlerVtbl kTimerVtbl = {
1869 &sQuery<2>, &sAddRef<2>, &sRelease<2>, &sOnTimer
1870 };
1871#endif
1872};
1873
1874#endif // DSPARK_PLUGIN_WEBVIEW
1875
1876template <typename P>
1877Steinberg_IPlugView* SMTG_STDMETHODCALLTYPE Plugin<P>::sCreateView(void* self_,
1878 Steinberg_FIDString name)
1879{
1880 (void) self_;
1881 (void) name;
1882#if defined(DSPARK_PLUGIN_WEBVIEW)
1883 if constexpr (HasEditor<P>)
1884 {
1885 if (webview_ui::Editor<P>::available() && name != nullptr
1886 && std::strcmp(name, "editor") == 0)
1887 {
1888 auto* view = new (std::nothrow) View<P>(fromLens(self_, 2));
1889 if (view != nullptr)
1890 return reinterpret_cast<Steinberg_IPlugView*>(view->lensPtr(0));
1891 }
1892 }
1893#endif
1894 return nullptr; // no editor: the host shows its generic parameter UI
1895}
1896
1897// -- factory --------------------------------------------------------------------
1898
1899template <typename P>
1901{
1902 const Steinberg_IPluginFactory3Vtbl* vtbl;
1903
1904 Factory() noexcept { vtbl = &kVtbl; }
1905
1906 static std::array<uint8_t, 16> classUid() noexcept
1907 {
1908 return makeUid(P::descriptor.productId, 0x56535433ull); // 'VST3'
1909 }
1910
1911 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void* self_,
1912 const Steinberg_TUID iid, void** obj)
1913 {
1914 if (obj == nullptr) return Steinberg_kInvalidArgument;
1915 if (tuidEqual(iid, Steinberg_FUnknown_iid)
1916 || tuidEqual(iid, Steinberg_IPluginFactory_iid)
1917 || tuidEqual(iid, Steinberg_IPluginFactory2_iid)
1918 || tuidEqual(iid, Steinberg_IPluginFactory3_iid))
1919 {
1920 *obj = self_;
1921 return Steinberg_kResultOk;
1922 }
1923 *obj = nullptr;
1924 return Steinberg_kNoInterface;
1925 }
1926
1927 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void*) { return 100; }
1928 static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void*) { return 100; }
1929
1930 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetFactoryInfo(void*,
1931 Steinberg_PFactoryInfo* info)
1932 {
1933 if (info == nullptr) return Steinberg_kInvalidArgument;
1934 std::memset(info, 0, sizeof(*info));
1935 copyAscii(P::descriptor.vendor, info->vendor, sizeof(info->vendor));
1936 copyAscii(P::descriptor.url, info->url, sizeof(info->url));
1937 copyAscii(P::descriptor.email, info->email, sizeof(info->email));
1938 info->flags = Steinberg_PFactoryInfo_FactoryFlags_kUnicode;
1939 return Steinberg_kResultOk;
1940 }
1941
1942 static Steinberg_int32 SMTG_STDMETHODCALLTYPE sCountClasses(void*) { return 1; }
1943
1944 static void fillClassCommon(Steinberg_TUID cid) noexcept
1945 {
1946 const auto uid = classUid();
1947 std::memcpy(cid, uid.data(), 16);
1948 }
1949
1950 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo(void*,
1951 Steinberg_int32 index, Steinberg_PClassInfo* info)
1952 {
1953 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
1954 std::memset(info, 0, sizeof(*info));
1955 fillClassCommon(info->cid);
1956 info->cardinality = 0x7FFFFFFF; // kManyInstances
1957 copyAscii("Audio Module Class", info->category, sizeof(info->category));
1958 copyAscii(P::descriptor.name, info->name, sizeof(info->name));
1959 return Steinberg_kResultOk;
1960 }
1961
1962 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfo2(void*,
1963 Steinberg_int32 index, Steinberg_PClassInfo2* info)
1964 {
1965 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
1966 std::memset(info, 0, sizeof(*info));
1967 fillClassCommon(info->cid);
1968 info->cardinality = 0x7FFFFFFF;
1969 copyAscii("Audio Module Class", info->category, sizeof(info->category));
1970 copyAscii(P::descriptor.name, info->name, sizeof(info->name));
1971 info->classFlags = 0;
1972 copyAscii(P::descriptor.category == Category::Instrument ? "Instrument" : "Fx",
1973 info->subCategories, sizeof(info->subCategories));
1974 copyAscii(P::descriptor.vendor, info->vendor, sizeof(info->vendor));
1975 copyAscii(P::descriptor.version, info->version, sizeof(info->version));
1976 copyAscii("VST 3.7.9", info->sdkVersion, sizeof(info->sdkVersion));
1977 return Steinberg_kResultOk;
1978 }
1979
1980 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetClassInfoUnicode(void*,
1981 Steinberg_int32 index, Steinberg_PClassInfoW* info)
1982 {
1983 if (index != 0 || info == nullptr) return Steinberg_kInvalidArgument;
1984 std::memset(info, 0, sizeof(*info));
1985 fillClassCommon(info->cid);
1986 info->cardinality = 0x7FFFFFFF;
1987 copyAscii("Audio Module Class", info->category, sizeof(info->category));
1988 asciiToString128(P::descriptor.name, info->name);
1989 info->classFlags = 0;
1990 copyAscii(P::descriptor.category == Category::Instrument ? "Instrument" : "Fx",
1991 info->subCategories, sizeof(info->subCategories));
1992 asciiToString128(P::descriptor.vendor, info->vendor);
1993 asciiToString128(P::descriptor.version, info->version);
1994 asciiToString128("VST 3.7.9", info->sdkVersion);
1995 return Steinberg_kResultOk;
1996 }
1997
1998 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCreateInstance(void*,
1999 Steinberg_FIDString cid, Steinberg_FIDString iid, void** obj)
2000 {
2001 if (cid == nullptr || iid == nullptr || obj == nullptr)
2002 return Steinberg_kInvalidArgument;
2003 *obj = nullptr;
2004 const auto uid = classUid();
2005 if (std::memcmp(cid, uid.data(), 16) != 0)
2006 return Steinberg_kNoInterface;
2007
2008 auto* plugin = new (std::nothrow) Plugin<P>();
2009 if (plugin == nullptr) return Steinberg_kResultFalse;
2010
2011 Steinberg_TUID requested;
2012 std::memcpy(requested, iid, sizeof(requested));
2013 const Steinberg_tresult r = plugin->query(requested, obj);
2014 plugin->releaseImpl(); // drop the construction reference
2015 return r == Steinberg_kResultOk ? Steinberg_kResultOk : Steinberg_kNoInterface;
2016 }
2017
2018 static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetHostContext(void*, Steinberg_FUnknown*)
2019 { return Steinberg_kResultOk; }
2020
2021 inline static const Steinberg_IPluginFactory3Vtbl kVtbl = {
2022 &sQuery, &sAddRef, &sRelease,
2026 };
2027};
2028
2029} // namespace dspark::plugin::vst3
2030
2031// -- module entry ----------------------------------------------------------------
2032
2033#if defined(_WIN32)
2034#define DSPARK_VST3_EXPORT __declspec(dllexport)
2035#else
2036#define DSPARK_VST3_EXPORT __attribute__((visibility("default")))
2037#endif
2038
2043#define DSPARK_VST3_PLUGIN(PluginClass) \
2044 static dspark::plugin::vst3::Factory<PluginClass> gDsparkVst3Factory; \
2045 extern "C" { \
2046 DSPARK_VST3_EXPORT Steinberg_IPluginFactory* SMTG_STDMETHODCALLTYPE \
2047 GetPluginFactory() \
2048 { \
2049 return reinterpret_cast<Steinberg_IPluginFactory*>(&gDsparkVst3Factory); \
2050 } \
2051 DSPARK_VST3_EXPORT bool InitDll() { return true; } \
2052 DSPARK_VST3_EXPORT bool ExitDll() { return true; } \
2053 DSPARK_VST3_EXPORT bool ModuleEntry(void*) { return true; } \
2054 DSPARK_VST3_EXPORT bool ModuleExit() { return true; } \
2055 DSPARK_VST3_EXPORT bool bundleEntry(void*) { return true; } \
2056 DSPARK_VST3_EXPORT bool bundleExit() { return true; } \
2057 }
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
One embedded WebView editor instance for plugin class P. Owned by the format backends (the VST3 IPlug...
Custom-editor switch. With hasEditor = false (or absent) hosts show their generic parameter UI....
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:149
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:97
constexpr uint32_t kBypassParamId
Definition DSParkVst3.h:128
constexpr uint32_t kMidiProxyBase
Definition DSParkVst3.h:136
constexpr int kNumMidiProxies
Definition DSParkVst3.h:138
bool tuidEqual(const Steinberg_TUID a, const Steinberg_TUID b) noexcept
Definition DSParkVst3.h:77
bool streamWrite(Steinberg_IBStream *s, const void *src, Steinberg_int32 bytes) noexcept
Definition DSParkVst3.h:111
constexpr Steinberg_int32 kPresetProgramListId
Definition DSParkVst3.h:131
void asciiToString128(const char *src, Steinberg_Vst_String128 dst) noexcept
Definition DSParkVst3.h:82
constexpr int kMidiProxyControllers[4]
Definition DSParkVst3.h:137
constexpr bool isMidiProxyId(uint32_t id) noexcept
Definition DSParkVst3.h:140
MidiEvent midiProxyToEvent(uint32_t id, double normalized, int offset) noexcept
Proxy parameter (normalized) -> the framework-neutral MidiEvent.
Definition DSParkVst3.h:156
constexpr uint32_t kProgramParamId
Definition DSParkVst3.h:129
constexpr int kBypassRampSamples
Definition DSParkVst3.h:130
void copyAscii(const char *src, char *dst, size_t cap) noexcept
Definition DSParkVst3.h:91
@ 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 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", "440 Hz").
void sortBlockEvents(BlockEvent *events, int count) noexcept
Stable insertion sort by offset (tiny N, allocation-free - audio-thread safe; equal offsets keep arri...
bool applyState(P &user, const uint8_t *data, size_t size, double *normalized, int *programIndex=nullptr, int *bypassState=nullptr)
Parses a state blob; fills normalized (defaults pre-loaded by the caller), reports a persisted progra...
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
One timestamped in-block event, normalised across formats: a parameter point, a bypass point,...
int32_t offset
Frame position within the block.
One incoming MIDI-ish event, normalised across formats.
@ NoteOn
note = key, value = velocity 0..1.
@ PolyPressure
note = key, value = pressure 0..1.
@ NoteOff
note = key, value = release velocity 0..1.
@ ControlChange
note = controller number, value = 0..1.
@ ChannelPressure
value = pressure 0..1 (note unused).
@ PitchBend
value = bend -1..+1 (note unused).
int sampleOffset
Frames into the NEXT processBlock call.
One automatable parameter. Plain values run [min, max]; hosts see the normalized [0,...
int steps
0 continuous, 1 toggle, N discrete.
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_tresult SMTG_STDMETHODCALLTYPE sTerminate(void *)
Definition DSParkVst3.h:550
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetLatencySamples(void *self_)
Definition DSParkVst3.h:751
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentSetState(void *self_, Steinberg_IBStream *state)
Definition DSParkVst3.h:646
static Steinberg_Vst_ParamValue SMTG_STDMETHODCALLTYPE sGetParamNormalized(void *self_, Steinberg_Vst_ParamID id)
std::atomic< double > shadow[kNumParams==0 ? 1 :kNumParams]
Definition DSParkVst3.h:217
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetupProcessing(void *self_, Steinberg_Vst_ProcessSetup *setup)
Definition DSParkVst3.h:756
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:202
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlInitialize(void *, Steinberg_FUnknown *)
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sRelease(void *self_)
Definition DSParkVst3.h:305
const Steinberg_Vst_IProcessContextRequirementsVtbl * contextReqVtbl
Definition DSParkVst3.h:205
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetRoutingInfo(void *, Steinberg_Vst_RoutingInfo *, Steinberg_Vst_RoutingInfo *)
Definition DSParkVst3.h:611
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 sCtrlTerminate(void *)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sComponentGetState(void *self_, Steinberg_IBStream *state)
Definition DSParkVst3.h:681
std::atomic< double > midiProxyShadow[HasMidi< P > ? kNumMidiProxies :1]
Definition DSParkVst3.h:218
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCtrlGetState(void *, Steinberg_IBStream *)
static constexpr Steinberg_int32 kBypassIndex
const Steinberg_Vst_IMidiMappingVtbl * midiMapVtbl
Definition DSParkVst3.h:207
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sHasProgramPitchNames(void *, Steinberg_Vst_ProgramListID, Steinberg_int32)
static constexpr size_t kNumParams
Definition DSParkVst3.h:184
static const Steinberg_Vst_IUnitInfoVtbl kUnitVtbl
void applyAllShadows() noexcept
Definition DSParkVst3.h:327
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sCanProcessSampleSize(void *, Steinberg_int32 size)
Definition DSParkVst3.h:744
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetIoMode(void *, Steinberg_Vst_IoMode)
Definition DSParkVst3.h:556
void applyFactoryPresetIdx(int idx) noexcept
Definition DSParkVst3.h:336
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sQuery(void *self_, const Steinberg_TUID iid, void **obj)
Definition DSParkVst3.h:295
static int indexOfParamId(Steinberg_Vst_ParamID id) noexcept
Definition DSParkVst3.h:310
static Steinberg_int32 numInputBuses() noexcept
Definition DSParkVst3.h:561
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)
static const Steinberg_Vst_IComponentVtbl kComponentVtbl
Definition DSParkVst3.h:697
void refreshLatency(Steinberg_int32 extraFlags=0) noexcept
Definition DSParkVst3.h:353
bool applyBlockEvent(const BlockEvent &ev, int blockStart, bool &programChanged) noexcept
Definition DSParkVst3.h:482
std::atomic< Steinberg_uint32 > refs
Definition DSParkVst3.h:209
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetActive(void *self_, Steinberg_TBool state)
Definition DSParkVst3.h:620
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetParameterCount(void *)
Steinberg_tresult query(const Steinberg_TUID iid, void **obj) noexcept
Definition DSParkVst3.h:259
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSelectUnit(void *, Steinberg_Vst_UnitID unitId)
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sAddRef(void *self_)
Definition DSParkVst3.h:301
const Steinberg_Vst_IUnitInfoVtbl * unitVtbl
Definition DSParkVst3.h:206
Steinberg_uint32 addRefImpl() noexcept
Definition DSParkVst3.h:282
static const Steinberg_Vst_IProcessContextRequirementsVtbl kContextReqVtbl
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetUnitCount(void *)
void forwardTransport(const Steinberg_Vst_ProcessContext *ctx) noexcept
Definition DSParkVst3.h:768
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)
Definition DSParkVst3.h:731
std::vector< float > silence
Definition DSParkVst3.h:223
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetUnitInfo(void *, Steinberg_int32 unitIndex, Steinberg_Vst_UnitInfo *info)
std::atomic< int > currentProgram
Definition DSParkVst3.h:220
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:204
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:615
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:187
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:576
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:222
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetUnitProgramData(void *, Steinberg_int32, Steinberg_int32, struct Steinberg_IBStream *)
Steinberg_Vst_ProcessSetup setup
Definition DSParkVst3.h:212
static Plugin * fromLens(void *iface, int lens) noexcept
Definition DSParkVst3.h:246
std::vector< float > dryL
Definition DSParkVst3.h:222
void collectParameterChanges(Steinberg_Vst_IParameterChanges *changes, Steinberg_int32 numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkVst3.h:378
void collectInputEvents(Steinberg_Vst_IEventList *list, Steinberg_int32 numSamples, BlockEvent *events, int &count) noexcept
Definition DSParkVst3.h:415
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sProcess(void *self_, Steinberg_Vst_ProcessData *data)
Definition DSParkVst3.h:808
std::atomic< bool > bypass
Definition DSParkVst3.h:219
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetTailSamples(void *self_)
Definition DSParkVst3.h:985
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetParamNormalized(void *self_, Steinberg_Vst_ParamID id, Steinberg_Vst_ParamValue value)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sInitialize(void *, Steinberg_FUnknown *)
Definition DSParkVst3.h:547
static Steinberg_Vst_UnitID SMTG_STDMETHODCALLTYPE sGetSelectedUnit(void *)
Steinberg_uint32 releaseImpl() noexcept
Definition DSParkVst3.h:287
void * lensPtr(int lens) noexcept
Definition DSParkVst3.h:252
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)
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sGetControllerClassId(void *, Steinberg_TUID)
Definition DSParkVst3.h:553
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetProcessing(void *, Steinberg_TBool)
Definition DSParkVst3.h:764
static constexpr bool kIsInstrument
Definition DSParkVst3.h:185
static Steinberg_uint32 SMTG_STDMETHODCALLTYPE sGetProcessContextRequirements(void *)
const Steinberg_Vst_IAudioProcessorVtbl * processorVtbl
Definition DSParkVst3.h:203
std::atomic< Steinberg_Vst_IComponentHandler * > handler
Definition DSParkVst3.h:229
static Steinberg_int32 SMTG_STDMETHODCALLTYPE sGetBusCount(void *, Steinberg_Vst_MediaType type, Steinberg_Vst_BusDirection dir)
Definition DSParkVst3.h:567
void applyNormalized(int index, double normalized) noexcept
Definition DSParkVst3.h:317
static Steinberg_tresult SMTG_STDMETHODCALLTYPE sSetBusArrangements(void *self_, Steinberg_Vst_SpeakerArrangement *inputs, Steinberg_int32 numIns, Steinberg_Vst_SpeakerArrangement *outputs, Steinberg_int32 numOuts)
Definition DSParkVst3.h:711
static int midiProxySlot(uint32_t id) noexcept
Definition DSParkVst3.h:535