DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
DSParkPlugin.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
58#include "../Core/AudioSpec.h"
59#include "../Core/AudioBuffer.h"
60#include "../Core/DenormalGuard.h"
61
62#include <algorithm>
63#include <array>
64#include <concepts>
65#include <cstdint>
66#include <cstdio>
67#include <cstdlib>
68#include <cstring>
69#include <limits>
70#include <type_traits>
71#include <vector>
72
73namespace dspark::plugin {
74
75// -- Descriptor ---------------------------------------------------------------
76
86enum class Category
87{
88 Fx,
90};
91
101{
102 const char* name = "DSPark Plugin";
103 const char* vendor = "DSPark";
104 const char* url = "";
105 const char* email = "";
106 const char* productId = "com.dspark.plugin";
107 const char* version = "1.0.0";
109};
110
111// -- Parameters ---------------------------------------------------------------
112
121struct Param
122{
123 const char* id = "";
124 const char* name = "";
125 float minValue = 0.0f;
126 float maxValue = 1.0f;
127 float defValue = 0.0f;
128 const char* unit = "";
129 int steps = 0;
130 const char* const* labels = nullptr;
131};
132
134constexpr Param param(const char* id, const char* name,
135 float minValue, float maxValue, float defValue,
136 const char* unit) noexcept
137{
138 return Param { id, name, minValue, maxValue, defValue, unit, 0 };
139}
140
142constexpr Param toggle(const char* id, const char* name, bool defaultOn) noexcept
143{
144 return Param { id, name, 0.0f, 1.0f, defaultOn ? 1.0f : 0.0f, "", 1 };
145}
146
152constexpr Param stepped(const char* id, const char* name,
153 float minValue, float maxValue, float defValue,
154 int steps, const char* unit = "") noexcept
155{
156 return Param { id, name, minValue, maxValue, defValue, unit,
157 steps > 1 ? steps : 1, nullptr };
158}
159
170template <size_t N>
171constexpr Param choice(const char* id, const char* name,
172 const char* const (&labels)[N], int defaultIndex = 0) noexcept
173{
174 static_assert(N >= 2, "a choice needs at least two labels");
175 const int def = defaultIndex < 0 ? 0
176 : (defaultIndex > static_cast<int>(N) - 1 ? static_cast<int>(N) - 1
177 : defaultIndex);
178 return Param { id, name, 0.0f, static_cast<float>(N - 1), static_cast<float>(def),
179 "", static_cast<int>(N - 1), labels };
180}
181
183constexpr bool isToggle(const Param& p) noexcept
184{
185 return p.steps == 1 && p.labels == nullptr;
186}
187
189template <typename... Ps>
190constexpr std::array<Param, sizeof...(Ps)> params(Ps... ps) noexcept
191{
192 return { ps... };
193}
194
195// -- Normalisation ------------------------------------------------------------
196
198constexpr double toNormalized(const Param& p, double plain) noexcept
199{
200 const double range = static_cast<double>(p.maxValue) - p.minValue;
201 if (range <= 0.0) return 0.0;
202 const double n = (plain - p.minValue) / range;
203 // Written so a NaN (a host's text-to-value on garbage, say) lands on 0
204 // rather than passing through: NaN compares false against both bounds.
205 return !(n > 0.0) ? 0.0 : (n > 1.0 ? 1.0 : n);
206}
207
209constexpr double toPlain(const Param& p, double normalized) noexcept
210{
211 // Written so a NaN lands on 0 rather than passing through: NaN compares
212 // false against both bounds, and the snap below would then convert it.
213 double n = !(normalized > 0.0) ? 0.0 : (normalized > 1.0 ? 1.0 : normalized);
214 if (p.steps > 0)
215 {
216 // Snap to the nearest of steps+1 positions. n is bounded above, so the
217 // conversion is in range by construction.
218 const double scaled = n * p.steps + 0.5;
219 const double idx = static_cast<double>(static_cast<long long>(scaled));
220 n = idx / p.steps;
221 n = n < 0.0 ? 0.0 : (n > 1.0 ? 1.0 : n);
222 }
223 return p.minValue + n * (static_cast<double>(p.maxValue) - p.minValue);
224}
225
230inline int parseToggleText(const char* text) noexcept
231{
232 auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? static_cast<char>(c + 32) : c; };
233 if (text == nullptr) return -1;
234 if (lower(text[0]) == 'o' && lower(text[1]) == 'n' && text[2] == '\0') return 1;
235 if (lower(text[0]) == 'o' && lower(text[1]) == 'f' && lower(text[2]) == 'f'
236 && text[3] == '\0') return 0;
237 return -1;
238}
239
241constexpr int stepIndex(const Param& p, double plain) noexcept
242{
243 if (p.steps <= 0) return 0;
244 const double scaled = toNormalized(p, plain) * p.steps + 0.5;
245 const int idx = static_cast<int>(scaled);
246 return idx < 0 ? 0 : (idx > p.steps ? p.steps : idx);
247}
248
250inline void formatValue(const Param& p, double plain, char* out, int outSize) noexcept
251{
252 if (out == nullptr || outSize <= 0) return;
253 const size_t cap = static_cast<size_t>(outSize);
254 if (p.labels != nullptr && p.steps > 0)
255 {
256 const char* label = p.labels[stepIndex(p, plain)];
257 std::snprintf(out, cap, "%s", label != nullptr ? label : "");
258 return;
259 }
260 if (isToggle(p))
261 {
262 std::snprintf(out, cap, "%s",
263 plain >= 0.5 * (p.minValue + p.maxValue) ? "On" : "Off");
264 return;
265 }
266 // Whole-number positions (a stepped 1..8, say) display without decimals.
267 bool whole = false;
268 if (p.steps > 1)
269 {
270 const double step = (static_cast<double>(p.maxValue) - p.minValue) / p.steps;
271 auto isWhole = [](double v) {
272 const double r = static_cast<double>(static_cast<long long>(v < 0.0 ? v - 0.5
273 : v + 0.5));
274 return v - r < 1e-9 && r - v < 1e-9;
275 };
276 whole = isWhole(step) && isWhole(p.minValue);
277 }
278 char number[48];
279 if (whole)
280 std::snprintf(number, sizeof(number), "%.0f", plain);
281 else
282 std::snprintf(number, sizeof(number), "%.2f", plain);
283 // "<number> <unit>", cut to the host's buffer (always terminated).
284 size_t n = 0;
285 auto put = [&](const char* text) noexcept {
286 for (; text != nullptr && *text != '\0' && n + 1 < cap; ++text) out[n++] = *text;
287 };
288 put(number);
289 if (p.unit != nullptr && p.unit[0] != '\0')
290 {
291 put(" ");
292 put(p.unit);
293 }
294 out[n] = '\0';
295}
296
304inline bool parseValue(const Param& p, const char* text, double& plain) noexcept
305{
306 if (text == nullptr) return false;
307 while (*text == ' ' || *text == '\t') ++text;
308 if (p.labels != nullptr && p.steps > 0)
309 {
310 auto lower = [](char c) {
311 return c >= 'A' && c <= 'Z' ? static_cast<char>(c + 32) : c;
312 };
313 for (int i = 0; i <= p.steps; ++i)
314 {
315 const char* label = p.labels[i];
316 if (label == nullptr) continue;
317 size_t k = 0;
318 while (label[k] != '\0' && lower(label[k]) == lower(text[k])) ++k;
319 if (label[k] == '\0' && text[k] == '\0')
320 {
321 plain = toPlain(p, static_cast<double>(i) / p.steps);
322 return true;
323 }
324 }
325 }
326 if (isToggle(p))
327 {
328 const int toggle = parseToggleText(text);
329 if (toggle >= 0)
330 {
331 plain = toggle != 0 ? p.maxValue : p.minValue;
332 return true;
333 }
334 }
335 char* end = nullptr;
336 const double v = std::strtod(text, &end);
337 if (end == text || v != v) return false; // not a number, or NaN
338 plain = toPlain(p, toNormalized(p, v)); // clamp + snap
339 return true;
340}
341
342// -- Stable hashing (parameter ids, class UIDs) -------------------------------
343
345constexpr uint32_t hash32(const char* s) noexcept
346{
347 uint32_t h = 2166136261u;
348 while (*s != '\0')
349 {
350 h ^= static_cast<uint8_t>(*s++);
351 h *= 16777619u;
352 }
353 return h;
354}
355
357constexpr uint64_t hash64(const char* s, uint64_t salt) noexcept
358{
359 uint64_t h = 14695981039346656037ull ^ salt;
360 while (*s != '\0')
361 {
362 h ^= static_cast<uint8_t>(*s++);
363 h *= 1099511628211ull;
364 }
365 return h;
366}
367
374template <typename P>
375constexpr bool paramIdsUnique() noexcept
376{
377 constexpr size_t n = P::parameters.size();
378 for (size_t i = 0; i < n; ++i)
379 {
380 const uint32_t h = hash32(P::parameters[i].id);
381 if (h == 0x5052474Du || h == 0x42595053u) return false; // 'PRGM'/'BYPS'
382 for (size_t j = i + 1; j < n; ++j)
383 if (hash32(P::parameters[j].id) == h) return false;
384 }
385 return true;
386}
387
392constexpr std::array<uint8_t, 16> makeUid(const char* productId, uint64_t salt) noexcept
393{
394 const uint64_t a = hash64(productId, salt);
395 const uint64_t b = hash64(productId, salt ^ 0x9E3779B97F4A7C15ull);
396 std::array<uint8_t, 16> uid {};
397 for (int i = 0; i < 8; ++i)
398 {
399 uid[static_cast<size_t>(i)] = static_cast<uint8_t>(a >> (8 * i));
400 uid[static_cast<size_t>(i + 8)] = static_cast<uint8_t>(b >> (8 * i));
401 }
402 return uid;
403}
404
405// -- Capability detection -----------------------------------------------------
406
407template <typename P>
408concept HasReset = requires(P p) { p.reset(); };
409
410template <typename P>
411concept HasLatency = requires(const P p) { { p.getLatency() } -> std::convertible_to<int>; };
412
413template <typename P>
414concept HasTail = requires(const P p) { { p.getTailSeconds() } -> std::convertible_to<double>; };
415
416template <typename P>
417concept HasGetState = requires(const P p) {
418 { p.getState() } -> std::convertible_to<std::vector<uint8_t>>;
419};
420
421template <typename P>
422concept HasSetState = requires(P p, const uint8_t* d, size_t n) {
423 { p.setState(d, n) } -> std::convertible_to<bool>;
424};
425
430template <typename P>
431concept HasEditor = requires { P::hasEditor; } && P::hasEditor;
432
451template <typename P>
452concept HasSidechain = requires(P p, AudioBufferView<float> io,
453 AudioBufferView<float> sc) {
454 p.processBlock(io, sc);
455};
456
457// -- Transport (host tempo & timeline) ------------------------------------------
458
470{
471 double tempoBpm = 120.0;
472 double ppqPosition = 0.0;
473 double barStartPpq = 0.0;
476 double loopStartPpq = 0.0;
477 double loopEndPpq = 0.0;
478 bool playing = false;
479 bool recording = false;
480 bool looping = false;
481 bool tempoValid = false;
482 bool positionValid = false;
483 bool timeSigValid = false;
484 bool loopValid = false;
485
487 [[nodiscard]] double secondsPerBeat() const noexcept
488 {
489 return 60.0 / (tempoBpm > 1.0 ? tempoBpm : 120.0);
490 }
491
493 [[nodiscard]] double samplesPerBeat(double sampleRate) const noexcept
494 {
495 return secondsPerBeat() * sampleRate;
496 }
497};
498
505template <typename P>
506concept HasTransport = requires(P p, const TransportInfo& t) {
507 p.setTransport(t);
508};
509
510// -- MIDI ------------------------------------------------------------------------
511
527{
528 enum class Type : uint8_t
529 {
530 NoteOn,
531 NoteOff,
532 PitchBend,
536 };
537
539 uint8_t channel = 0;
540 uint8_t note = 0;
541 float value = 0.0f;
542 int sampleOffset = 0;
543};
544
552template <typename P>
553concept HasMidi = requires(P p, const MidiEvent& e) {
554 p.handleMidiEvent(e);
555};
556
557// -- Offline rendering ------------------------------------------------------------
558
567template <typename P>
568concept HasOfflineMode = requires(P p, bool offline) {
569 p.setOfflineRendering(offline);
570};
571
572// -- Channel support ----------------------------------------------------------------
573
584{
586 StereoOnly,
587 MonoOnly
588};
589
590template <typename P>
591concept HasChannelSupport = requires {
592 { P::channels } -> std::convertible_to<ChannelSupport>;
593};
594
596template <typename P>
598{
599 if constexpr (HasChannelSupport<P>)
600 return P::channels;
601 else
603}
604
606template <typename P>
607constexpr bool supportsChannelCount(int numChannels) noexcept
608{
609 constexpr ChannelSupport s = channelSupportOf<P>();
610 if (numChannels == 1) return s != ChannelSupport::StereoOnly;
611 if (numChannels == 2) return s != ChannelSupport::MonoOnly;
612 return false;
613}
614
616template <typename P>
617constexpr int defaultChannelCount() noexcept
618{
619 return channelSupportOf<P>() == ChannelSupport::MonoOnly ? 1 : 2;
620}
621
622// -- Factory presets ----------------------------------------------------------------
623
629template <size_t NumValues>
631{
632 const char* name = "";
633 std::array<float, NumValues> values {};
634};
635
638template <typename... Vs>
639constexpr PresetDef<sizeof...(Vs)> preset(const char* name, Vs... values) noexcept
640{
641 return PresetDef<sizeof...(Vs)> { name, { static_cast<float>(values)... } };
642}
643
647template <typename First, typename... Rest>
648constexpr std::array<First, 1 + sizeof...(Rest)> presets(First first, Rest... rest) noexcept
649{
650 static_assert((std::is_same_v<First, Rest> && ...),
651 "every preset must list one value per parameter");
652 return { first, rest... };
653}
654
662template <typename P>
663concept HasFactoryPresets = requires {
664 { P::factoryPresets.size() } -> std::convertible_to<size_t>;
665 { P::factoryPresets[0].name } -> std::convertible_to<const char*>;
666 { P::factoryPresets[0].values.size() } -> std::convertible_to<size_t>;
667};
668
670template <typename P>
671constexpr int factoryPresetCountOf() noexcept
672{
673 if constexpr (HasFactoryPresets<P>)
674 {
675 static_assert(P::factoryPresets[0].values.size() == P::parameters.size(),
676 "factoryPresets must list one PLAIN value per parameter, "
677 "in parameter-table order");
678 return static_cast<int>(P::factoryPresets.size());
679 }
680 else
681 return 0;
682}
683
686template <typename P>
687constexpr double presetNormalized(int presetIndex, size_t paramIndex) noexcept
688{
689 if constexpr (HasFactoryPresets<P>)
690 return toNormalized(P::parameters[paramIndex],
691 P::factoryPresets[static_cast<size_t>(presetIndex)]
692 .values[paramIndex]);
693 else
694 {
695 (void) presetIndex;
696 (void) paramIndex;
697 return 0.0;
698 }
699}
700
701// -- Sample-accurate automation -------------------------------------------------------
702
711template <typename P>
712concept HasSampleAccurateOptOut = requires {
713 { P::sampleAccurateAutomation } -> std::convertible_to<bool>;
714};
715
716template <typename P>
717constexpr bool sampleAccurateOf() noexcept
718{
719 if constexpr (HasSampleAccurateOptOut<P>)
720 return P::sampleAccurateAutomation;
721 else
722 return true;
723}
724
727inline constexpr int kAutomationQuantum = 32;
728
731inline constexpr int kMaxBlockEvents = 256;
732
740{
741 enum class Kind : uint8_t { Param, Bypass, Midi };
742 int32_t offset = 0;
744 uint32_t paramId = 0;
745 double value = 0.0;
747};
748
751inline void sortBlockEvents(BlockEvent* events, int count) noexcept
752{
753 for (int i = 1; i < count; ++i)
754 {
755 BlockEvent key = events[i];
756 int j = i - 1;
757 while (j >= 0 && events[j].offset > key.offset)
758 {
759 events[j + 1] = events[j];
760 --j;
761 }
762 events[j + 1] = key;
763 }
764}
765
766// -- Bypass dry path -------------------------------------------------------------
767
781{
782public:
785 void prepare(int maxLatency, int maxBlock)
786 {
787 const int need = (maxLatency > 0 ? maxLatency : 0) + (maxBlock > 0 ? maxBlock : 0) + 1;
788 size_ = 1;
789 while (size_ < need) size_ <<= 1;
790 for (auto& b : buf_) b.assign(static_cast<size_t>(size_), 0.0f);
791 write_ = 0;
792 }
793
795 void reset() noexcept
796 {
797 for (auto& b : buf_) std::fill(b.begin(), b.end(), 0.0f);
798 write_ = 0;
799 }
800
802 [[nodiscard]] int capacity(int n) const noexcept { return size_ - 1 - n; }
803
809 void process(float* const* dry, int numChannels, int n, int latency) noexcept
810 {
811 if (size_ == 0 || n <= 0) return;
812 const int mask = size_ - 1;
813 const int cap = capacity(n);
814 const int d = latency < 0 ? 0 : (latency > cap ? (cap > 0 ? cap : 0) : latency);
815 for (int ch = 0; ch < numChannels && ch < 2; ++ch)
816 {
817 float* ring = buf_[static_cast<size_t>(ch)].data();
818 float* x = dry[ch];
819 for (int i = 0; i < n; ++i)
820 {
821 const int w = (write_ + i) & mask;
822 ring[w] = x[i];
823 x[i] = ring[(w - d) & mask];
824 }
825 }
826 write_ = (write_ + n) & mask;
827 }
828
829private:
830 std::array<std::vector<float>, 2> buf_;
831 int size_ = 0;
832 int write_ = 0;
833};
834
838inline constexpr int kDryDelayMinCapacity = 8192;
839
840// -- Editor contract (used by plugin/webview/DSParkWebViewEditor.h) -------------
841
844{
845 int width = 480;
846 int height = 320;
847};
848
855enum class EditorResize
856{
857 Fixed,
858 Free,
860};
861
864template <typename P>
865concept HasEditorHtml = requires {
866 { P::editorHtml() } -> std::convertible_to<const char*>;
867};
868
870template <typename P>
871concept HasEditorSize = requires {
872 { P::editorSize.width } -> std::convertible_to<int>;
873 { P::editorSize.height } -> std::convertible_to<int>;
874};
875
877template <typename P>
878constexpr EditorSize editorSizeOf() noexcept
879{
880 if constexpr (HasEditorSize<P>)
881 return EditorSize { static_cast<int>(P::editorSize.width),
882 static_cast<int>(P::editorSize.height) };
883 else
884 return EditorSize {};
885}
886
889template <typename P>
890concept HasEditorResizable = requires { P::editorResizable; } && P::editorResizable;
891
894template <typename P>
895concept HasEditorResize = requires {
896 { P::editorResize } -> std::convertible_to<EditorResize>;
897};
898
900template <typename P>
901constexpr EditorResize editorResizeOf() noexcept
902{
903 if constexpr (HasEditorResize<P>)
904 return P::editorResize;
905 else if constexpr (HasEditorResizable<P>)
906 return EditorResize::Free;
907 else
908 return EditorResize::Fixed;
909}
910
912inline constexpr double kEditorMinSizeFactor = 0.5;
913inline constexpr double kEditorMaxSizeFactor = 3.0;
914
924inline constexpr double kEditorMaxPixels = 32767.0;
925
940constexpr int toBoundedInt(double value, int lo, int hi) noexcept
941{
942 if (!(value > static_cast<double>(lo))) return lo; // false for NaN
943 if (value >= static_cast<double>(hi)) return hi;
944 return static_cast<int>(value);
945}
946
955template <typename P>
956constexpr void constrainEditorSize(double& width, double& height, double scale) noexcept
957{
958 const EditorSize logical = editorSizeOf<P>();
959 constexpr EditorResize mode = editorResizeOf<P>();
960 if constexpr (mode == EditorResize::Fixed)
961 {
962 width = logical.width * scale;
963 height = logical.height * scale;
964 }
965 else
966 {
967 const double minW = logical.width * scale * kEditorMinSizeFactor;
968 const double maxW = logical.width * scale * kEditorMaxSizeFactor;
969 const double minH = logical.height * scale * kEditorMinSizeFactor;
970 const double maxH = logical.height * scale * kEditorMaxSizeFactor;
971 width = width < minW ? minW : (width > maxW ? maxW : width);
972 height = height < minH ? minH : (height > maxH ? maxH : height);
973 if constexpr (mode == EditorResize::KeepAspect)
974 {
975 const double ratio = static_cast<double>(logical.width) / logical.height;
976 double fitW = width;
977 if (height * ratio < fitW) fitW = height * ratio; // inside-fit
978 fitW = fitW < minW ? minW : (fitW > maxW ? maxW : fitW);
979 width = fitW;
980 height = fitW / ratio;
981 }
982 }
983 // The policy above is expressed as multiples of a host-chosen scale, so
984 // its own bounds move with that scale. Every caller converts these to an
985 // integer; bound them in pixels first, and write the test so a NaN cannot
986 // pass through it.
987 width = !(width > 1.0) ? 1.0
988 : (width > kEditorMaxPixels ? kEditorMaxPixels : width);
989 height = !(height > 1.0) ? 1.0
990 : (height > kEditorMaxPixels ? kEditorMaxPixels : height);
991}
992
995template <typename P>
996concept HasEditorDebug = requires { P::editorDebug; } && P::editorDebug;
997
1002template <typename P>
1003concept HasEditorDevFile = requires {
1004 { P::editorDevFile() } -> std::convertible_to<const char*>;
1005};
1006
1007// -- PluginBase: the whole contract, visible in one place ----------------------
1008
1042template <typename Derived>
1044{
1051 void reset() noexcept {}
1052
1061 [[nodiscard]] int getLatency() const noexcept { return 0; }
1062
1069 [[nodiscard]] double getTailSeconds() const noexcept { return 0.0; }
1070
1078 [[nodiscard]] std::vector<uint8_t> getState() const { return {}; }
1079
1081 bool setState(const uint8_t*, size_t) { return false; }
1082
1091 void setTransport(const TransportInfo&) noexcept {}
1092
1101 void setOfflineRendering(bool) noexcept {}
1102
1109 static constexpr bool hasEditor = false;
1110};
1111
1112// -- Wrapper-side state serialisation ------------------------------------------
1113//
1114// Every backend stores plugin state in ONE container format so presets stay
1115// portable across formats and tolerant across versions (same philosophy as
1116// Core/StateBlob): a magic/version header, the parameter table keyed by
1117// hash32(id) - unknown ids are skipped, missing ids keep defaults - and an
1118// optional opaque user-state section appended verbatim.
1119
1120inline constexpr uint32_t kStateMagic = 0x4453504Bu; // "DSPK"
1121inline constexpr uint32_t kStateVersion = 1u;
1122
1125inline constexpr uint32_t kProgramStateId = 0x5052474Du;
1126
1131inline constexpr uint32_t kBypassStateId = 0x42595053u;
1132
1133template <typename P>
1134inline std::vector<uint8_t> buildState(const P& user, const double* normalized,
1135 size_t numParams, int programIndex = -1,
1136 int bypassState = -1)
1137{
1138 auto push32 = [](std::vector<uint8_t>& v, uint32_t x) {
1139 v.push_back(static_cast<uint8_t>(x));
1140 v.push_back(static_cast<uint8_t>(x >> 8));
1141 v.push_back(static_cast<uint8_t>(x >> 16));
1142 v.push_back(static_cast<uint8_t>(x >> 24));
1143 };
1144 auto pushValue = [&push32](std::vector<uint8_t>& v, double value) {
1145 uint64_t bits = 0;
1146 static_assert(sizeof(bits) == sizeof(value));
1147 std::memcpy(&bits, &value, sizeof(bits));
1148 push32(v, static_cast<uint32_t>(bits));
1149 push32(v, static_cast<uint32_t>(bits >> 32));
1150 };
1151 std::vector<uint8_t> blob;
1152 push32(blob, kStateMagic);
1153 push32(blob, kStateVersion);
1154 push32(blob, static_cast<uint32_t>(numParams) + (programIndex >= 0 ? 1u : 0u)
1155 + (bypassState >= 0 ? 1u : 0u));
1156 for (size_t i = 0; i < numParams; ++i)
1157 {
1158 push32(blob, hash32(P::parameters[i].id));
1159 pushValue(blob, normalized[i]);
1160 }
1161 if (programIndex >= 0)
1162 {
1163 // The active factory preset rides along as one more entry so hosts
1164 // restore the program selection with the session.
1165 push32(blob, kProgramStateId);
1166 pushValue(blob, static_cast<double>(programIndex));
1167 }
1168 if (bypassState >= 0)
1169 {
1170 push32(blob, kBypassStateId);
1171 pushValue(blob, bypassState > 0 ? 1.0 : 0.0);
1172 }
1173 if constexpr (HasGetState<P>)
1174 {
1175 const std::vector<uint8_t> userBlob = user.getState();
1176 push32(blob, static_cast<uint32_t>(userBlob.size()));
1177 blob.insert(blob.end(), userBlob.begin(), userBlob.end());
1178 }
1179 else
1180 push32(blob, 0u);
1181 return blob;
1182}
1183
1188template <typename P>
1189inline bool applyState(P& user, const uint8_t* data, size_t size, double* normalized,
1190 int* programIndex = nullptr, int* bypassState = nullptr)
1191{
1192 auto read32 = [&](size_t& pos, uint32_t& out) {
1193 if (pos + 4 > size) return false;
1194 out = static_cast<uint32_t>(data[pos])
1195 | static_cast<uint32_t>(data[pos + 1]) << 8
1196 | static_cast<uint32_t>(data[pos + 2]) << 16
1197 | static_cast<uint32_t>(data[pos + 3]) << 24;
1198 pos += 4;
1199 return true;
1200 };
1201 size_t pos = 0;
1202 uint32_t magic = 0, version = 0, count = 0;
1203 if (!read32(pos, magic) || magic != kStateMagic) return false;
1204 if (!read32(pos, version) || version == 0) return false;
1205 if (!read32(pos, count)) return false;
1206
1207 constexpr size_t numParams = P::parameters.size();
1208 for (uint32_t i = 0; i < count; ++i)
1209 {
1210 uint32_t id = 0, lo = 0, hi = 0;
1211 if (!read32(pos, id) || !read32(pos, lo) || !read32(pos, hi)) return false;
1212 const uint64_t bits = static_cast<uint64_t>(lo) | (static_cast<uint64_t>(hi) << 32);
1213 double v = 0.0;
1214 std::memcpy(&v, &bits, sizeof(v));
1215 // A corrupt blob can carry any bit pattern: a NaN would survive both
1216 // range checks below (every comparison is false) and poison the
1217 // parameter shadow, so non-finite entries keep the default instead.
1218 if (v != v) continue;
1219 if (id == kProgramStateId)
1220 {
1221 // Converting a double that does not fit an int is undefined, and a
1222 // blob out of a project file can carry any value at all, so the
1223 // range is settled BEFORE the conversion rather than after it. An
1224 // index past the end of the preset list is refused by the caller's
1225 // own bounds check, exactly as an index written by a build with a
1226 // longer list always was.
1227 if (programIndex != nullptr && v >= 0.0)
1228 {
1229 const double rounded = v + 0.5;
1230 constexpr double intLimit =
1231 static_cast<double>(std::numeric_limits<int>::max());
1232 *programIndex = rounded < intLimit
1233 ? static_cast<int>(rounded)
1234 : std::numeric_limits<int>::max();
1235 }
1236 continue;
1237 }
1238 if (id == kBypassStateId)
1239 {
1240 if (bypassState != nullptr)
1241 *bypassState = v >= 0.5 ? 1 : 0;
1242 continue;
1243 }
1244 for (size_t k = 0; k < numParams; ++k)
1245 if (hash32(P::parameters[k].id) == id)
1246 {
1247 normalized[k] = v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v);
1248 break;
1249 }
1250 // Unknown ids are skipped: forward-compatible by construction.
1251 }
1252
1253 uint32_t userSize = 0;
1254 // Subtraction form: pos <= size is guaranteed after read32, and on
1255 // 32-bit size_t targets (WASM) the addition form `pos + userSize` could
1256 // wrap around and admit an out-of-bounds user section.
1257 if (read32(pos, userSize) && userSize > 0 && userSize <= size - pos)
1258 {
1259 if constexpr (HasSetState<P>)
1260 user.setState(data + pos, userSize);
1261 }
1262 return true;
1263}
1264
1265} // namespace dspark::plugin
The dry signal of the wrappers' soft bypass, delayed by the plugin's latency.
int capacity(int n) const noexcept
void prepare(int maxLatency, int maxBlock)
void process(float *const *dry, int numChannels, int n, int latency) noexcept
Optional static constexpr bool editorDebug = true; - enables the browser DevTools in the editor (WebV...
Optional static const char* editorDevFile() - absolute path of an HTML file to load INSTEAD of editor...
static const char* editorHtml() - the editor page (HTML/CSS/JS), usually a raw string literal....
Optional static constexpr bool editorResizable = true; - shorthand for editorResize = EditorResize::F...
Optional static constexpr EditorResize editorResize = ...; - full resize policy; takes precedence ove...
Optional static constexpr EditorSize editorSize { w, h };.
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...
Sample-accurate opt-out: static constexpr bool sampleAccurateAutomation = false;. By default the wrap...
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 bool supportsChannelCount(int numChannels) noexcept
True when P accepts a bus width of numChannels (1 or 2).
constexpr std::array< Param, sizeof...(Ps)> params(Ps... ps) noexcept
Builds the parameter table (use inside static constexpr auto).
constexpr double kEditorMinSizeFactor
Resize bounds: hosts may shrink to half and grow to 3x the declared size.
EditorResize
How the host may resize the editor window. Declare static constexpr EditorResize editorResize = ....
@ Fixed
The window is exactly editorSize; hosts cannot drag-resize it.
@ KeepAspect
Drag-resizable, locked to the declared width:height ratio.
@ Free
Drag-resizable between 0.5x and 3x the declared size.
constexpr bool paramIdsUnique() noexcept
Compile-time guarantee that every parameter id hashes uniquely and collides with neither reserved sta...
constexpr ChannelSupport channelSupportOf() noexcept
The declared channel support of P (default: mono+stereo).
constexpr uint32_t kProgramStateId
Reserved entry id persisting the active factory-preset index ('PRGM'); never a user parameter....
constexpr uint32_t kStateVersion
Category
Plugin category (reflected into each format's class metadata).
@ Instrument
Audio generator (MIDI in -> audio out, no audio input).
@ Fx
Audio effect (audio in -> audio out).
constexpr double kEditorMaxPixels
Pixel ceiling for a negotiated editor size.
constexpr Param stepped(const char *id, const char *name, float minValue, float maxValue, float defValue, int steps, const char *unit="") noexcept
Discrete parameter helper: steps + 1 evenly spaced positions from minValue to maxValue,...
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 stepIndex(const Param &p, double plain) noexcept
Position index (0..steps) of a plain value of a discrete parameter.
constexpr int kDryDelayMinCapacity
Ring headroom of the bypass dry path: latencies up to this (or twice the latency at activation,...
constexpr bool sampleAccurateOf() noexcept
constexpr uint64_t hash64(const char *s, uint64_t salt) noexcept
FNV-1a 64-bit with a salt (two runs build a 128-bit class UID).
constexpr Param param(const char *id, const char *name, float minValue, float maxValue, float defValue, const char *unit) noexcept
Continuous 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 double presetNormalized(int presetIndex, size_t paramIndex) noexcept
Normalized value of parameter paramIndex in factory preset presetIndex (bounds already validated by t...
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 Param choice(const char *id, const char *name, const char *const (&labels)[N], int defaultIndex=0) noexcept
Named-choice helper: one position per label, plain values 0..N-1 (the label index),...
constexpr EditorResize editorResizeOf() noexcept
The effective resize policy for plugin class P.
constexpr uint32_t kStateMagic
constexpr void constrainEditorSize(double &width, double &height, double scale) noexcept
Applies the resize policy of P to a size the host proposes: Fixed pins it, Free clamps each axis to t...
constexpr double kEditorMaxSizeFactor
constexpr int kMaxBlockEvents
Hard cap of timestamped events handled per block; the (very unlikely) excess applies at the position ...
constexpr bool isToggle(const Param &p) noexcept
True for an on/off parameter (two unnamed positions).
constexpr PresetDef< sizeof...(Vs)> preset(const char *name, Vs... values) noexcept
Builds one factory preset: preset("Warm", 6.0f, 0.8f, ...) - one PLAIN value per parameter,...
constexpr EditorSize editorSizeOf() noexcept
The declared (logical) editor size, or the 480x320 default.
constexpr int factoryPresetCountOf() noexcept
Number of factory presets of P (0 when none declared).
constexpr int defaultChannelCount() noexcept
The width every backend starts in before the host negotiates.
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...
ChannelSupport
Bus widths a plugin offers to the host. The DSPark contract is channel-agnostic by construction (prep...
@ MonoOnly
Strictly 1->1 (rare; metering/utility plugins).
@ StereoOnly
Strictly 2->2 (inherently stereo DSP).
@ MonoAndStereo
Negotiates 1->1 or 2->2 with the host (default).
constexpr uint32_t kBypassStateId
Reserved entry id persisting the host bypass ('BYPS' - the same value every backend uses as the bypas...
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.
constexpr std::array< First, 1+sizeof...(Rest)> presets(First first, Rest... rest) noexcept
Builds the factory preset table (all presets must cover the same parameter count - enforced here; mat...
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),...
One timestamped in-block event, normalised across formats: a parameter point, a bypass point,...
uint32_t paramId
hash32 id (Kind::Param only).
int32_t offset
Frame position within the block.
double value
Normalized value / bypass >= 0.5.
MidiEvent midi
Kind::Midi payload.
Static identity of a plugin. All fields are plain literals so the whole descriptor can live in a stat...
Editor window size in logical pixels (physical = logical x host scale).
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).
float value
Velocity / CC / pressure 0..1; bend -1..+1.
int sampleOffset
Frames into the NEXT processBlock call.
uint8_t note
Key 0..127, or controller number for CC.
One automatable parameter. Plain values run [min, max]; hosts see the normalized [0,...
const char * name
Display name.
const char *const * labels
steps+1 position names, or null.
const char * unit
Display unit ("dB", "Hz", "%", ...).
int steps
0 continuous, 1 toggle, N discrete.
Optional convenience base that makes EVERY contract method visible and overridable from one place - I...
void setOfflineRendering(bool) noexcept
void setTransport(const TransportInfo &) noexcept
static constexpr bool hasEditor
double getTailSeconds() const noexcept
std::vector< uint8_t > getState() const
bool setState(const uint8_t *, size_t)
- restore side. Return false on a foreign blob.
int getLatency() const noexcept
One factory preset: a name plus a PLAIN value for every parameter, in parameter-table order....
std::array< float, NumValues > values
Host transport snapshot, delivered once per audio block.
bool playing
Transport is rolling.
double secondsPerBeat() const noexcept
Seconds per quarter note at the current tempo.
double tempoBpm
Current tempo (when tempoValid).
double loopStartPpq
Cycle start (when loopValid).
double samplesPerBeat(double sampleRate) const noexcept
Samples per quarter note - the basis for tempo-synced delays/LFOs.
bool timeSigValid
Time signature came from the host.
bool tempoValid
tempoBpm came from the host.
bool positionValid
ppqPosition/barStartPpq came from the host.
double ppqPosition
Musical position of frame 0, in quarter notes.
double loopEndPpq
Cycle end (when loopValid).
double barStartPpq
Quarter-note position of the current bar start.
bool loopValid
Loop points came from the host.
int timeSigNumerator
e.g. 3 in 3/4 (when timeSigValid).
bool looping
Cycle/loop mode is engaged.
bool recording
Transport is recording.
int timeSigDenominator
e.g. 4 in 3/4 (when timeSigValid).