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