DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
StateBlob.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
35#include <algorithm>
36#include <cassert>
37#include <cmath>
38#include <cstdint>
39#include <cstdio>
40#include <cstring>
41#include <limits>
42#include <string>
43#include <vector>
44
45namespace dspark {
46
47// ============================================================================
48// StateWriter
49// ============================================================================
50
53{
54public:
59 StateWriter(uint32_t processorId, uint16_t processorVersion)
60 {
61 blob_.reserve(256);
62 append("DSPK", 4);
63 appendU16(kFormatVersion);
64 appendU32(processorId);
65 appendU16(processorVersion);
66 countPos_ = blob_.size();
67 appendU16(0); // patched by blob()
68 }
69
71 void write(const char* key, float value)
72 {
73 uint32_t bits = 0;
74 std::memcpy(&bits, &value, 4);
75 writeEntry(key, 0, bits);
76 }
77
79 void write(const char* key, int32_t value)
80 {
81 writeEntry(key, 1, static_cast<uint32_t>(value));
82 }
83
85 void write(const char* key, bool value)
86 {
87 writeEntry(key, 2, value ? 1u : 0u);
88 }
89
91 void write(const char* key, const std::vector<uint8_t>& nested)
92 {
93 const size_t len = std::min<size_t>(std::strlen(key), 255);
94 assert(len == std::strlen(key) && "state key longer than 255 chars is truncated");
95 assert(count_ < 0xFFFF && "state entry count overflows u16");
96 blob_.push_back(static_cast<uint8_t>(len));
97 append(key, len);
98 blob_.push_back(3);
99 appendU32(static_cast<uint32_t>(nested.size()));
100 blob_.insert(blob_.end(), nested.begin(), nested.end());
101 ++count_;
102 }
103
105 [[nodiscard]] std::vector<uint8_t> blob() const
106 {
107 std::vector<uint8_t> out = blob_;
108 out[countPos_] = static_cast<uint8_t>(count_ & 0xFF);
109 out[countPos_ + 1] = static_cast<uint8_t>((count_ >> 8) & 0xFF);
110 return out;
111 }
112
113private:
114 static constexpr uint16_t kFormatVersion = 1;
115
116 void writeEntry(const char* key, uint8_t type, uint32_t payload)
117 {
118 const size_t len = std::min<size_t>(std::strlen(key), 255);
119 assert(len == std::strlen(key) && "state key longer than 255 chars is truncated");
120 assert(count_ < 0xFFFF && "state entry count overflows u16");
121 blob_.push_back(static_cast<uint8_t>(len));
122 append(key, len);
123 blob_.push_back(type);
124 appendU32(payload);
125 ++count_;
126 }
127
128 void append(const char* data, size_t n)
129 {
130 blob_.insert(blob_.end(), data, data + n);
131 }
132 void appendU16(uint16_t v)
133 {
134 blob_.push_back(static_cast<uint8_t>(v & 0xFF));
135 blob_.push_back(static_cast<uint8_t>(v >> 8));
136 }
137 void appendU32(uint32_t v)
138 {
139 for (int i = 0; i < 4; ++i)
140 blob_.push_back(static_cast<uint8_t>((v >> (8 * i)) & 0xFF));
141 }
142
143 std::vector<uint8_t> blob_;
144 size_t countPos_ = 0;
145 uint16_t count_ = 0;
146};
147
148// ============================================================================
149// StateReader
150// ============================================================================
151
161{
162public:
163 StateReader(const uint8_t* data, size_t size)
164 {
165 if (data == nullptr || size < 14 || std::memcmp(data, "DSPK", 4) != 0)
166 return;
167 const uint16_t fmt = readU16(data + 4);
168 if (fmt != 1) return;
169 processorId_ = readU32(data + 6);
170 processorVersion_ = readU16(data + 10);
171 const uint16_t count = readU16(data + 12);
172
173 size_t pos = 14;
174 for (uint16_t i = 0; i < count; ++i)
175 {
176 if (pos + 1 > size) return;
177 const size_t keyLen = data[pos++];
178 if (pos + keyLen + 5 > size) return;
179 Entry e;
180 e.key.assign(reinterpret_cast<const char*>(data + pos), keyLen);
181 pos += keyLen;
182 e.type = data[pos++];
183 e.payload = readU32(data + pos);
184 pos += 4;
185 if (e.type == 3) // nested blob: payload is its byte length
186 {
187 // Subtraction form: `pos + e.payload > size` would wrap on
188 // 32-bit size_t (WASM) for a corrupt length near UINT32_MAX
189 // and pass the check into a giant out-of-bounds read.
190 if (e.payload > size - pos) return;
191 e.bytes.assign(data + pos, data + pos + e.payload);
192 pos += e.payload;
193 }
194 entries_.push_back(std::move(e));
195 }
196 valid_ = true;
197 }
198
199 [[nodiscard]] bool isValid() const noexcept { return valid_; }
200 [[nodiscard]] uint32_t processorId() const noexcept { return processorId_; }
201 [[nodiscard]] uint16_t processorVersion() const noexcept { return processorVersion_; }
202
204 [[nodiscard]] float read(const char* key, float defaultValue) const
205 {
206 if (const Entry* e = find(key, 0))
207 {
208 float v = 0.0f;
209 std::memcpy(&v, &e->payload, 4);
210 return v;
211 }
212 return defaultValue;
213 }
214
216 [[nodiscard]] int32_t read(const char* key, int32_t defaultValue) const
217 {
218 if (const Entry* e = find(key, 1))
219 return static_cast<int32_t>(e->payload);
220 return defaultValue;
221 }
222
224 [[nodiscard]] bool read(const char* key, bool defaultValue) const
225 {
226 if (const Entry* e = find(key, 2))
227 return e->payload != 0;
228 return defaultValue;
229 }
230
232 [[nodiscard]] std::vector<uint8_t> readBlob(const char* key) const
233 {
234 if (const Entry* e = find(key, 3))
235 return e->bytes;
236 return {};
237 }
238
240 struct Entry
241 {
242 std::string key;
243 uint8_t type = 0;
244 uint32_t payload = 0;
245 std::vector<uint8_t> bytes;
246 };
247 [[nodiscard]] const std::vector<Entry>& entries() const noexcept { return entries_; }
248
249private:
250 [[nodiscard]] const Entry* find(const char* key, uint8_t type) const
251 {
252 for (const auto& e : entries_)
253 if (e.type == type && e.key == key)
254 return &e;
255 return nullptr;
256 }
257
258 static uint16_t readU16(const uint8_t* p)
259 {
260 return static_cast<uint16_t>(p[0] | (p[1] << 8));
261 }
262 static uint32_t readU32(const uint8_t* p)
263 {
264 return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8)
265 | (static_cast<uint32_t>(p[2]) << 16) | (static_cast<uint32_t>(p[3]) << 24);
266 }
267
268 bool valid_ = false;
269 uint32_t processorId_ = 0;
270 uint16_t processorVersion_ = 0;
271 std::vector<Entry> entries_;
272};
273
274// ============================================================================
275// JSON helpers (the blob's flat-object subset, zero dependencies)
276// ============================================================================
277
278namespace detail {
279
282constexpr int kMaxStateJsonDepth = 32;
283
289inline void appendStateJsonString(std::string& out, const std::string& s)
290{
291 out += '"';
292 for (const unsigned char c : s)
293 {
294 switch (c)
295 {
296 case '"': out += "\\\""; break;
297 case '\\': out += "\\\\"; break;
298 case '\b': out += "\\b"; break;
299 case '\f': out += "\\f"; break;
300 case '\n': out += "\\n"; break;
301 case '\r': out += "\\r"; break;
302 case '\t': out += "\\t"; break;
303 default:
304 if (c < 0x20)
305 {
306 char esc[7];
307 std::snprintf(esc, sizeof(esc), "\\u%04x", static_cast<unsigned>(c));
308 out += esc;
309 }
310 else
311 {
312 out += static_cast<char>(c);
313 }
314 break;
315 }
316 }
317 out += '"';
318}
319
323inline void appendStateJsonNumber(std::string& out, float v)
324{
325 if (!std::isfinite(v)) { out += '0'; return; }
326 char num[48];
327 std::snprintf(num, sizeof(num), "%.9g", static_cast<double>(v));
328 for (char* c = num; *c != '\0'; ++c)
329 if (*c == ',') *c = '.'; // locale decimal comma -> JSON dot
330 out += num;
331}
332
334inline const char* parseStateJsonNumber(const char* s, const char* end, double& out) noexcept
335{
336 bool negative = false;
337 if (s < end && (*s == '-' || *s == '+'))
338 {
339 negative = (*s == '-');
340 ++s;
341 }
342 double v = 0.0;
343 bool any = false;
344 while (s < end && *s >= '0' && *s <= '9')
345 {
346 v = v * 10.0 + (*s - '0');
347 ++s;
348 any = true;
349 }
350 if (s < end && *s == '.')
351 {
352 ++s;
353 double f = 0.1;
354 while (s < end && *s >= '0' && *s <= '9')
355 {
356 v += (*s - '0') * f;
357 f *= 0.1;
358 ++s;
359 any = true;
360 }
361 }
362 if (!any) return nullptr;
363 if (s < end && (*s == 'e' || *s == 'E'))
364 {
365 ++s;
366 bool expNegative = false;
367 if (s < end && (*s == '-' || *s == '+'))
368 {
369 expNegative = (*s == '-');
370 ++s;
371 }
372 int e = 0;
373 bool eAny = false;
374 while (s < end && *s >= '0' && *s <= '9')
375 {
376 e = e * 10 + (*s - '0');
377 ++s;
378 eAny = true;
379 if (e > 308) { e = 309; } // saturate
380 }
381 if (!eAny) return nullptr;
382 double scale = 1.0;
383 for (int k = 0; k < (e > 309 ? 309 : e); ++k)
384 scale *= 10.0;
385 v = expNegative ? v / scale : v * scale;
386 }
387 out = negative ? -v : v;
388 return s;
389}
390
391inline std::string stateToJsonImpl(const std::vector<uint8_t>& blob, int depth)
392{
393 if (depth >= kMaxStateJsonDepth) return "{}";
394
395 StateReader r(blob.data(), blob.size());
396 if (!r.isValid()) return "{}";
397
398 std::string out = "{\"id\":" + std::to_string(r.processorId())
399 + ",\"version\":" + std::to_string(r.processorVersion())
400 + ",\"params\":{";
401 bool first = true;
402 for (const auto& e : r.entries())
403 {
404 if (!first) out += ',';
405 first = false;
406 appendStateJsonString(out, e.key);
407 out += ':';
408 if (e.type == 0)
409 {
410 float v = 0.0f;
411 std::memcpy(&v, &e.payload, 4);
412 appendStateJsonNumber(out, v);
413 }
414 else if (e.type == 1)
415 {
416 out += std::to_string(static_cast<int32_t>(e.payload));
417 }
418 else if (e.type == 3)
419 {
420 out += stateToJsonImpl(e.bytes, depth + 1); // nested state: recurse
421 }
422 else
423 {
424 out += (e.payload != 0) ? "true" : "false";
425 }
426 }
427 out += "}}";
428 return out;
429}
430
431inline std::vector<uint8_t> stateFromJsonImpl(const std::string& json, int depth)
432{
433 if (depth >= kMaxStateJsonDepth) return {};
434
435 auto skipWs = [&](size_t& p) { while (p < json.size() && (json[p] == ' ' || json[p] == '\n'
436 || json[p] == '\t' || json[p] == '\r')) ++p; };
437 auto expect = [&](size_t& p, char c) {
438 skipWs(p);
439 if (p >= json.size() || json[p] != c) return false;
440 ++p;
441 return true;
442 };
443 auto parseString = [&](size_t& p, std::string& out) {
444 skipWs(p);
445 if (p >= json.size() || json[p] != '"') return false;
446 ++p;
447 out.clear();
448 while (p < json.size() && json[p] != '"')
449 {
450 char c = json[p++];
451 if (c == '\\') // unescape the sequences appendStateJsonString emits
452 {
453 if (p >= json.size()) return false;
454 const char e = json[p++];
455 switch (e)
456 {
457 case '"': out += '"'; break;
458 case '\\': out += '\\'; break;
459 case '/': out += '/'; break;
460 case 'b': out += '\b'; break;
461 case 'f': out += '\f'; break;
462 case 'n': out += '\n'; break;
463 case 'r': out += '\r'; break;
464 case 't': out += '\t'; break;
465 case 'u':
466 {
467 if (p + 4 > json.size()) return false;
468 unsigned code = 0;
469 for (int k = 0; k < 4; ++k)
470 {
471 const char h = json[p++];
472 code <<= 4;
473 if (h >= '0' && h <= '9') code |= unsigned(h - '0');
474 else if (h >= 'a' && h <= 'f') code |= unsigned(h - 'a' + 10);
475 else if (h >= 'A' && h <= 'F') code |= unsigned(h - 'A' + 10);
476 else return false;
477 }
478 // Keys are ASCII by convention; emit the low byte (control
479 // chars round-trip, higher planes are out of contract).
480 out += static_cast<char>(code & 0xFF);
481 break;
482 }
483 default: return false; // unknown escape -> reject
484 }
485 }
486 else
487 {
488 out += c;
489 }
490 }
491 if (p >= json.size()) return false;
492 ++p;
493 return true;
494 };
495 auto parseNumber = [&](size_t& p, double& out) {
496 skipWs(p);
497 const char* start = json.c_str() + p;
498 const char* endOfJson = json.c_str() + json.size();
499 const char* stop = parseStateJsonNumber(start, endOfJson, out);
500 if (stop == nullptr) return false;
501 p = static_cast<size_t>(stop - json.c_str());
502 return true;
503 };
504
505 size_t p = 0;
506 std::string key;
507 double num = 0.0;
508 uint32_t id = 0;
509 uint16_t version = 0;
510
511 if (!expect(p, '{')) return {};
512 struct Param { std::string key; uint8_t type; uint32_t payload;
513 std::vector<uint8_t> bytes; };
514 std::vector<Param> params;
515
516 while (true)
517 {
518 if (!parseString(p, key) || !expect(p, ':')) return {};
519 if (key == "id" && parseNumber(p, num))
520 {
521 // Range-check before every float->int conversion: an out-of-range
522 // conversion is undefined behaviour, and this parser reads
523 // untrusted preset text.
524 if (!(num >= 0.0 && num <= 4294967295.0)) return {};
525 id = static_cast<uint32_t>(num);
526 }
527 else if (key == "version" && parseNumber(p, num))
528 {
529 if (!(num >= 0.0 && num <= 65535.0)) return {};
530 version = static_cast<uint16_t>(num);
531 }
532 else if (key == "params")
533 {
534 if (!expect(p, '{')) return {};
535 skipWs(p);
536 if (p < json.size() && json[p] == '}') { ++p; }
537 else
538 {
539 while (true)
540 {
541 std::string pk;
542 if (!parseString(p, pk) || !expect(p, ':')) return {};
543 skipWs(p);
544 Param prm { pk, 0, 0, {} };
545 if (p < json.size() && json[p] == '{')
546 {
547 // Nested state object: balance braces, recurse.
548 size_t braceDepth = 0, end = p;
549 for (; end < json.size(); ++end)
550 {
551 if (json[end] == '{') ++braceDepth;
552 else if (json[end] == '}' && --braceDepth == 0) { ++end; break; }
553 }
554 if (braceDepth != 0) return {};
555 prm.type = 3;
556 prm.bytes = stateFromJsonImpl(json.substr(p, end - p), depth + 1);
557 if (prm.bytes.empty()) return {};
558 p = end;
559 }
560 else if (json.compare(p, 4, "true") == 0)
561 {
562 prm.type = 2; prm.payload = 1; p += 4;
563 }
564 else if (json.compare(p, 5, "false") == 0)
565 {
566 prm.type = 2; prm.payload = 0; p += 5;
567 }
568 else if (parseNumber(p, num))
569 {
570 // Integers without fraction round-trip as int32 too;
571 // store as float (type 0) plus int mirror when exact.
572 // Beyond the float range the double->float conversion
573 // is undefined: reject such text as malformed.
574 if (!(std::abs(num) <= static_cast<double>(
575 std::numeric_limits<float>::max())))
576 return {};
577 const auto f = static_cast<float>(num);
578 std::memcpy(&prm.payload, &f, 4);
579 prm.type = 0;
580 if (num >= -2147483648.0 && num <= 2147483647.0
581 && num == static_cast<double>(static_cast<int32_t>(num)))
582 {
583 params.push_back({ pk, 1,
584 static_cast<uint32_t>(static_cast<int32_t>(num)), {} });
585 }
586 }
587 else return {};
588 params.push_back(std::move(prm));
589 skipWs(p);
590 if (p < json.size() && json[p] == ',') { ++p; continue; }
591 break;
592 }
593 if (!expect(p, '}')) return {};
594 }
595 }
596 else return {};
597 skipWs(p);
598 if (p < json.size() && json[p] == ',') { ++p; continue; }
599 break;
600 }
601 if (!expect(p, '}')) return {};
602
603 StateWriter w(id, version);
604 for (const auto& prm : params)
605 {
606 if (prm.type == 0)
607 {
608 float v = 0.0f;
609 std::memcpy(&v, &prm.payload, 4);
610 w.write(prm.key.c_str(), v);
611 }
612 else if (prm.type == 1)
613 {
614 w.write(prm.key.c_str(), static_cast<int32_t>(prm.payload));
615 }
616 else if (prm.type == 3)
617 {
618 w.write(prm.key.c_str(), prm.bytes);
619 }
620 else
621 {
622 w.write(prm.key.c_str(), prm.payload != 0);
623 }
624 }
625 return w.blob();
626}
627
628} // namespace detail
629
631[[nodiscard]] inline std::string stateToJson(const std::vector<uint8_t>& blob)
632{
633 return detail::stateToJsonImpl(blob, 0);
634}
635
645[[nodiscard]] inline std::vector<uint8_t> stateFromJson(const std::string& json)
646{
647 return detail::stateFromJsonImpl(json, 0);
648}
649
651[[nodiscard]] constexpr uint32_t stateId(const char (&tag)[5]) noexcept
652{
653 // Bytes, not chars: a (theoretical) non-ASCII tag char would sign-extend
654 // and smear across the other lanes.
655 return static_cast<uint32_t>(static_cast<uint8_t>(tag[0]))
656 | (static_cast<uint32_t>(static_cast<uint8_t>(tag[1])) << 8)
657 | (static_cast<uint32_t>(static_cast<uint8_t>(tag[2])) << 16)
658 | (static_cast<uint32_t>(static_cast<uint8_t>(tag[3])) << 24);
659}
660
661} // namespace dspark
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:161
std::vector< uint8_t > readBlob(const char *key) const
Reads a nested blob; empty when the key is absent.
Definition StateBlob.h:232
StateReader(const uint8_t *data, size_t size)
Definition StateBlob.h:163
int32_t read(const char *key, int32_t defaultValue) const
Reads an int32, or defaultValue when the key is absent.
Definition StateBlob.h:216
uint16_t processorVersion() const noexcept
Definition StateBlob.h:201
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:204
bool isValid() const noexcept
Definition StateBlob.h:199
uint32_t processorId() const noexcept
Definition StateBlob.h:200
const std::vector< Entry > & entries() const noexcept
Definition StateBlob.h:247
bool read(const char *key, bool defaultValue) const
Reads a bool, or defaultValue when the key is absent.
Definition StateBlob.h:224
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:53
void write(const char *key, const std::vector< uint8_t > &nested)
Writes a nested blob (composite processors: bands, slots...).
Definition StateBlob.h:91
StateWriter(uint32_t processorId, uint16_t processorVersion)
Definition StateBlob.h:59
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:105
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:71
void write(const char *key, bool value)
Writes a boolean parameter.
Definition StateBlob.h:85
void write(const char *key, int32_t value)
Writes an integer parameter (enums, counts, modes).
Definition StateBlob.h:79
const char * parseStateJsonNumber(const char *s, const char *end, double &out) noexcept
Locale-independent number parse over [s, end); nullptr on failure.
Definition StateBlob.h:334
std::string stateToJsonImpl(const std::vector< uint8_t > &blob, int depth)
Definition StateBlob.h:391
std::vector< uint8_t > stateFromJsonImpl(const std::string &json, int depth)
Definition StateBlob.h:431
void appendStateJsonNumber(std::string &out, float v)
Locale-independent float rendering ("%.9g" with a forced dot). Non-finite payloads (NaN/Inf – e....
Definition StateBlob.h:323
constexpr int kMaxStateJsonDepth
Definition StateBlob.h:282
void appendStateJsonString(std::string &out, const std::string &s)
Appends a JSON string literal, escaping the characters JSON forbids. Keys follow the [A-Za-z0-9....
Definition StateBlob.h:289
Main namespace for the DSPark framework.
std::string stateToJson(const std::vector< uint8_t > &blob)
Renders a state blob as a flat JSON object string.
Definition StateBlob.h:631
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
std::vector< uint8_t > stateFromJson(const std::string &json)
Parses the JSON produced by stateToJson() back into a blob.
Definition StateBlob.h:645
Access for generic tooling (JSON rendering, preset browsers).
Definition StateBlob.h:241
std::vector< uint8_t > bytes
Nested blob content (type 3).
Definition StateBlob.h:245