Loading...
Searching...
No Matches
Protocol.hpp
1#pragma once
2#include <algorithm>
3#include <bit>
4#include <cstddef>
5#include <cstdint>
6#include <cstring>
7#include <memory>
8#include <optional>
9#include <string>
10#include <string_view>
11#include <vector>
12
13// Wire format of the glTF live link: a DCC (the Blender add-on,
14// https://github.com/ossia/score-blender-livelink) connects over TCP and
15// streams frames.
16// That repository's PROTOCOL.md is the specification for writing a sender.
17//
18// Every integer and float is little-endian. A frame is
19//
20// u32 length byte count of what follows (type + body)
21// u8 type
22// u8 body[length - 1]
23//
24// A `str` below is a u16 byte count followed by that many UTF-8 bytes.
25//
26// hello (1) UTF-8 JSON, see "Negotiation" below
27// scene (2) u64 seq, then a complete binary glTF (.glb)
28//
29// Every other type is u64 seq, u32 count, then count records:
30//
31// transforms (3) str node, f32 translation[3], f32 rotation[4] (x, y, z, w),
32// f32 scale[3]: the node's local TRS
33// joints (4) str armature node, u32 bone count, then per bone a
34// transforms record named after the bone: bone names are
35// only unique within their armature
36// weights (5) str node, u16 n, f32 weights[n]: the morph target weights
37// of the node's mesh
38// visibility (6) str node, u8 visible
39// lights (7) str node, u8 type (0 directional, 1 point, 2 spot),
40// f32 color[3], f32 intensity, f32 range (0: infinite),
41// f32 inner cone, f32 outer cone (radians)
42// cameras (8) str node, u8 projection (0 perspective, 1 orthographic),
43// f32 yfov (radians), f32 aspect (0: unset), f32 znear,
44// f32 zfar, f32 xmag, f32 ymag
45// materials (9) str material, u32 fields, f32 base color[4],
46// f32 metallic, f32 roughness, f32 emissive[3],
47// f32 emissive strength; `fields` says which are meant
48// remove (10) str node: removes the node, and its subtree, from the
49// scene
50// environment (12) str world, f32 ambient[3], f32 strength: the light
51// the world sends from every direction (glTF has none)
52// active camera (13) str node: the camera node to render from, of the
53// several a scene may hold (glTF has no notion of it)
54//
55// and, laid out like a scene frame:
56//
57// patch (11) u64 seq, then a binary glTF of a few objects, whose root
58// nodes are added to the scene's roots
59//
60// Patches and removals change the scene without sending all of it: an object
61// added, deleted, or re-exported alone (a removal and a patch of the same
62// node). They apply in seq order, after the scene frame they follow.
63//
64// All values are what the glTF of the last scene frame would hold for them:
65// node names are glTF node names, units are glTF units, axes are glTF axes.
66// A record overrides that value in the last scene, so a scene is sent only
67// when geometry, materials' structure or the hierarchy change.
68//
69// `seq` is a single counter shared by every frame type, increasing on the
70// sender. Comparing seqs is how the receiver drops overrides that a later
71// scene already contains.
72//
73// Negotiation
74// -----------
75// The sender opens with a hello, {"protocol": 1, "app": ..., "features":
76// [...]}, and the receiver answers with its own: "protocol" and "accepts",
77// the names of the frame types it handles (accepted_frames below). A sender
78// uses only frame types the receiver accepts. One that gets no answer takes
79// the receiver for one that does not negotiate, and sends it scene frames
80// only. glTF is the baseline every sender can fall back to: an exporter only
81// has to produce a .glb to drive the receiver; every other frame type is an
82// optimisation over re-sending it.
83//
84// Native frames
85// -------------
86// What a glTF export would carry, without the export: the sender's own
87// evaluated meshes and its images' original files, and a JSON scene that
88// refers to them by key. They apply in seq order with patches and removals.
89//
90// mesh (14) u64 seq, str key, u16 primitive count, then per primitive
91// u32 vertex count v, u32 index count i (0: not indexed),
92// u8 attributes (1 normal, 2 texcoord0, 4 texcoord1,
93// 8 color0, 16 joints0 + weights0), u16 morph target count
94// t, f32 position[3v], then the attributes present in that
95// order (normal[3v], texcoord[2v], texcoord1[2v],
96// color[4v], u16 joints[4v] then f32 weights[4v]), then
97// u32 indices[i], then per morph target u8 (1 position, 2
98// normal deltas) and those f32 deltas[3v]. glTF axes and
99// UV convention; one primitive per material slot. Joints
100// index the "joints" of the skin of the node using it.
101// image (15) u64 seq, str key, str MIME type, then the encoded image
102// file (PNG, JPEG: whatever the receiver can decode)
103// native_scene (16) u64 seq, u8 reset, then UTF-8 JSON:
104// {"nodes": [{"name", "parent" (index in "nodes", -1 for
105// none: names repeat, bones), "t": [3], "r": [4] (xyzw),
106// "s": [3], "visible", "mesh": key, "materials": [name
107// per primitive], "light": [type, r, g, b, intensity,
108// range, inner, outer], "camera": [projection, yfov,
109// aspect, znear, zfar, xmag, ymag], "weights": [morph
110// target weights], "skin": {"armature": node index, "joints":
111// [bone node names, under the armature node],
112// "inverse_bind": [16 per joint, column-major]}}, ...]
113// (parents before children),
114// "materials": {key: {"name", "base_color": [4], "metallic",
115// "roughness", "emissive": [3], "emissive_strength",
116// "alpha": "opaque" | "mask" | "blend", "alpha_cutoff",
117// "double_sided", "unlit", and the KHR_materials_*
118// factors: "ior", "transmission", "specular",
119// "specular_color": [3], "clearcoat",
120// "clearcoat_roughness", "sheen_color": [3],
121// "sheen_roughness", "anisotropy", "anisotropy_rotation",
122// "iridescence", "iridescence_ior",
123// "iridescence_thickness": [min, max];
124// "textures": {slot: {"image": key, "uv": set,
125// "wrap": "repeat" | "clamp" | "mirror",
126// "filter": "linear" | "nearest", "scale", "strength",
127// "transform": {"offset": [2], "rotation", "scale": [2]}
128// (KHR_texture_transform)}}}}}
129// ("name" is the material's, for streamed values; nodes
130// name materials by key), with slots as glTF's: base_color,
131// transmission (R), specular (A), specular_color,
132// clearcoat (R), clearcoat_roughness (G), clearcoat_normal,
133// sheen_color, sheen_roughness (A), metallic_roughness (G roughness,
134// B metallic), normal, occlusion (R), emissive. With reset,
135// the nodes replace the whole scene, as a scene frame;
136// without, they are added to it, as a patch. Meshes and
137// images are kept by key until a reset no longer uses them.
138namespace Threedim::LiveLink
139{
140
141constexpr uint16_t default_port = 9877;
142
143// A glTF beyond this is refused: the receiver buffers a whole frame in memory
144// before it can parse it, and a corrupted length must not allocate gigabytes.
145constexpr uint32_t max_frame_size = 512u * 1024u * 1024u;
146
147enum class frame_type : uint8_t
148{
149 hello = 1,
150 scene = 2,
151 transforms = 3,
152 joints = 4,
153 weights = 5,
154 visibility = 6,
155 lights = 7,
156 cameras = 8,
157 materials = 9,
158 remove = 10,
159 patch = 11,
160 environment = 12,
161 active_camera = 13,
162 mesh = 14,
163 image = 15,
164 native_scene = 16,
165};
166
167constexpr int protocol_version = 1;
168
169// The hello's "accepts": every frame type this receiver handles.
170inline constexpr std::string_view accepted_frames[] = {
171 "scene", "patch", "remove", "transforms", "joints",
172 "weights", "visibility", "lights", "cameras", "materials",
173 "environment", "active_camera", "mesh", "image", "native_scene"};
174
175namespace detail
176{
177// memcpy keeps the reads alignment-safe; every platform score ships on is
178// little-endian, which the static_assert pins.
179static_assert(
180 std::endian::native == std::endian::little,
181 "the live link wire format is read in place as little-endian");
182
183struct reader
184{
185 std::string_view in;
186
187 template <typename T>
188 bool operator()(T& out) noexcept
189 {
190 if(in.size() < sizeof(T))
191 return false;
192 std::memcpy(&out, in.data(), sizeof(T));
193 in.remove_prefix(sizeof(T));
194 return true;
195 }
196
197 template <typename T, std::size_t N>
198 bool operator()(T (&out)[N]) noexcept
199 {
200 for(auto& v : out)
201 if(!(*this)(v))
202 return false;
203 return true;
204 }
205
206 bool operator()(std::string& out)
207 {
208 uint16_t len{};
209 if(!(*this)(len) || in.size() < len)
210 return false;
211 out.assign(in.data(), len);
212 in.remove_prefix(len);
213 return true;
214 }
215};
216
217struct writer
218{
219 std::string& out;
220
221 template <typename T>
222 void operator()(const T& v)
223 {
224 char buf[sizeof(T)];
225 std::memcpy(buf, &v, sizeof(T));
226 out.append(buf, sizeof(T));
227 }
228
229 template <typename T, std::size_t N>
230 void operator()(const T (&v)[N])
231 {
232 for(const auto& x : v)
233 (*this)(x);
234 }
235
236 void operator()(const std::string& s)
237 {
238 const auto len = uint16_t(std::min<std::size_t>(s.size(), 0xFFFF));
239 (*this)(len);
240 out.append(s.data(), len);
241 }
242};
243}
244
246{
247 std::string name;
248 float translation[3]{0.f, 0.f, 0.f};
249 float rotation[4]{0.f, 0.f, 0.f, 1.f};
250 float scale[3]{1.f, 1.f, 1.f};
251
252 static constexpr std::size_t min_size = 2 + 10 * sizeof(float);
253 bool read(detail::reader& r)
254 {
255 return r(name) && r(translation) && r(rotation) && r(scale);
256 }
257 void write(detail::writer& w) const
258 {
259 w(name), w(translation), w(rotation), w(scale);
260 }
261};
262
264{
265 std::string armature;
266 std::vector<transform_record> bones;
267
268 static constexpr std::size_t min_size = 2 + 4;
269 bool read(detail::reader& r)
270 {
271 uint32_t count{};
272 if(!r(armature) || !r(count) || count > r.in.size() / transform_record::min_size)
273 return false;
274 bones.resize(count);
275 for(auto& b : bones)
276 if(!b.read(r))
277 return false;
278 return true;
279 }
280 void write(detail::writer& w) const
281 {
282 w(armature), w(uint32_t(bones.size()));
283 for(const auto& b : bones)
284 b.write(w);
285 }
286};
287
289{
290 std::string name;
291 std::vector<float> weights;
292
293 static constexpr std::size_t min_size = 2 + 2;
294 bool read(detail::reader& r)
295 {
296 uint16_t n{};
297 if(!r(name) || !r(n) || r.in.size() < n * sizeof(float))
298 return false;
299 weights.resize(n);
300 for(float& f : weights)
301 r(f);
302 return true;
303 }
304 void write(detail::writer& w) const
305 {
306 w(name), w(uint16_t(weights.size()));
307 for(float f : weights)
308 w(f);
309 }
310};
311
313{
314 std::string name;
315 uint8_t visible{1};
316
317 static constexpr std::size_t min_size = 2 + 1;
318 bool read(detail::reader& r) { return r(name) && r(visible); }
319 void write(detail::writer& w) const { w(name), w(visible); }
320};
321
323{
324 enum : uint8_t { directional = 0, point = 1, spot = 2 };
325
326 std::string name;
327 uint8_t type{point};
328 float color[3]{1.f, 1.f, 1.f};
329 float intensity{1.f};
330 float range{0.f};
331 float inner_cone{0.f};
332 float outer_cone{0.7853981f};
333
334 static constexpr std::size_t min_size = 2 + 1 + 7 * sizeof(float);
335 bool read(detail::reader& r)
336 {
337 return r(name) && r(type) && r(color) && r(intensity) && r(range)
338 && r(inner_cone) && r(outer_cone);
339 }
340 void write(detail::writer& w) const
341 {
342 w(name), w(type), w(color), w(intensity), w(range), w(inner_cone), w(outer_cone);
343 }
344};
345
347{
348 enum : uint8_t { perspective = 0, orthographic = 1 };
349
350 std::string name;
351 uint8_t projection{perspective};
352 float yfov{0.7853981f};
353 float aspect{0.f};
354 float znear{0.1f};
355 float zfar{1000.f};
356 float xmag{1.f};
357 float ymag{1.f};
358
359 static constexpr std::size_t min_size = 2 + 1 + 6 * sizeof(float);
360 bool read(detail::reader& r)
361 {
362 return r(name) && r(projection) && r(yfov) && r(aspect) && r(znear) && r(zfar)
363 && r(xmag) && r(ymag);
364 }
365 void write(detail::writer& w) const
366 {
367 w(name), w(projection), w(yfov), w(aspect), w(znear), w(zfar), w(xmag), w(ymag);
368 }
369};
370
372{
373 // Which of the values below are meant. A Blender input wired to a node
374 // graph has no single value to stream; its field is left out.
375 enum : uint32_t
376 {
377 base_color_rgb = 1u << 0,
378 alpha = 1u << 1,
379 metallic_field = 1u << 2,
380 roughness_field = 1u << 3,
381 emissive = 1u << 4, // emissive factor and strength together
382 };
383
384 std::string name;
385 uint32_t fields{};
386 float base_color[4]{1.f, 1.f, 1.f, 1.f};
387 float metallic{1.f};
388 float roughness{1.f};
389 float emissive_factor[3]{0.f, 0.f, 0.f};
390 float emissive_strength{1.f};
391
392 static constexpr std::size_t min_size = 2 + 4 + 10 * sizeof(float);
393 bool read(detail::reader& r)
394 {
395 return r(name) && r(fields) && r(base_color) && r(metallic) && r(roughness)
396 && r(emissive_factor) && r(emissive_strength);
397 }
398 void write(detail::writer& w) const
399 {
400 w(name), w(fields), w(base_color), w(metallic), w(roughness), w(emissive_factor),
401 w(emissive_strength);
402 }
403};
404
405// A node name: what to remove, or which camera to render from.
407{
408 std::string name;
409
410 static constexpr std::size_t min_size = 2;
411 bool read(detail::reader& r) { return r(name); }
412 void write(detail::writer& w) const { w(name); }
413};
414
416{
417 std::string name;
418 float ambient[3]{0.f, 0.f, 0.f};
419 float strength{1.f};
420
421 static constexpr std::size_t min_size = 2 + 4 * sizeof(float);
422 bool read(detail::reader& r) { return r(name) && r(ambient) && r(strength); }
423 void write(detail::writer& w) const { w(name), w(ambient), w(strength); }
424};
425
427{
428 uint64_t seq{};
429 std::string glb;
430};
431
432template <typename Record>
434{
435 uint64_t seq{};
436 std::vector<Record> records;
437};
439
440// One primitive of a mesh frame. Absent attributes are null.
442{
443 uint32_t vertex_count{};
444 std::shared_ptr<std::vector<float>> positions, normals, texcoords, texcoords1, colors;
445 std::shared_ptr<std::vector<uint16_t>> joints; // 4 per vertex
446 std::shared_ptr<std::vector<float>> weights; // 4 per vertex
447 std::shared_ptr<std::vector<uint32_t>> indices;
448 struct morph
449 {
450 std::shared_ptr<std::vector<float>> positions, normals; // deltas
451 };
452 std::vector<morph> targets;
453};
454
456{
457 uint64_t seq{};
458 std::string key;
459 std::vector<native_primitive> primitives;
460};
461
463{
464 uint64_t seq{};
465 std::string key;
466 std::string mime_type;
467 std::shared_ptr<std::vector<uint8_t>> bytes;
468};
469
471{
472 uint64_t seq{};
473 bool reset{};
474 std::string json;
475};
476
477// Body of a scene or patch frame (without the length and type bytes).
478inline std::optional<scene_message> parse_scene(std::string_view body)
479{
481 detail::reader r{body};
482 if(!r(m.seq))
483 return std::nullopt;
484 // A .glb starts with the "glTF" magic; anything else cannot be a binary
485 // glTF and would only fail later, on a worker, with a vaguer message.
486 if(r.in.size() < 12 || r.in.substr(0, 4) != "glTF")
487 return std::nullopt;
488 m.glb.assign(r.in.data(), r.in.size());
489 return m;
490}
491
492namespace detail
493{
494template <typename T>
495bool read_array(reader& r, std::size_t count, std::shared_ptr<std::vector<T>>& out)
496{
497 // Bounded by what the body holds before anything is allocated.
498 if(count > r.in.size() / sizeof(T))
499 return false;
500 out = std::make_shared<std::vector<T>>(count);
501 std::memcpy(out->data(), r.in.data(), count * sizeof(T));
502 r.in.remove_prefix(count * sizeof(T));
503 return true;
504}
505}
506
507enum mesh_attribute : uint8_t
508{
509 mesh_normal = 1,
510 mesh_texcoord0 = 2,
511 mesh_texcoord1 = 4,
512 mesh_color0 = 8,
513 mesh_skin = 16,
514};
515
516inline std::optional<mesh_message> parse_mesh(std::string_view body)
517{
518 mesh_message m;
519 detail::reader r{body};
520 uint16_t count{};
521 if(!r(m.seq) || !r(m.key) || !r(count))
522 return std::nullopt;
523 m.primitives.resize(count);
524 for(auto& p : m.primitives)
525 {
526 uint32_t indices{};
527 uint8_t attrs{};
528 uint16_t targets{};
529 if(!r(p.vertex_count) || !r(indices) || !r(attrs) || !r(targets))
530 return std::nullopt;
531 const std::size_t v = p.vertex_count;
532 if(!detail::read_array(r, 3 * v, p.positions))
533 return std::nullopt;
534 if((attrs & mesh_normal) && !detail::read_array(r, 3 * v, p.normals))
535 return std::nullopt;
536 if((attrs & mesh_texcoord0) && !detail::read_array(r, 2 * v, p.texcoords))
537 return std::nullopt;
538 if((attrs & mesh_texcoord1) && !detail::read_array(r, 2 * v, p.texcoords1))
539 return std::nullopt;
540 if((attrs & mesh_color0) && !detail::read_array(r, 4 * v, p.colors))
541 return std::nullopt;
542 if((attrs & mesh_skin)
543 && (!detail::read_array(r, 4 * v, p.joints) || !detail::read_array(r, 4 * v, p.weights)))
544 return std::nullopt;
545 if(indices > 0)
546 {
547 if(!detail::read_array(r, indices, p.indices))
548 return std::nullopt;
549 for(uint32_t i : *p.indices)
550 if(i >= p.vertex_count)
551 return std::nullopt;
552 }
553 else if(v % 3 != 0)
554 {
555 return std::nullopt; // not indexed: a triangle list
556 }
557 p.targets.resize(targets);
558 for(auto& t : p.targets)
559 {
560 uint8_t which{};
561 if(!r(which))
562 return std::nullopt;
563 if((which & 1) && !detail::read_array(r, 3 * v, t.positions))
564 return std::nullopt;
565 if((which & 2) && !detail::read_array(r, 3 * v, t.normals))
566 return std::nullopt;
567 }
568 }
569 if(!r.in.empty())
570 return std::nullopt;
571 return m;
572}
573
574inline std::optional<image_message> parse_image(std::string_view body)
575{
576 image_message m;
577 detail::reader r{body};
578 if(!r(m.seq) || !r(m.key) || !r(m.mime_type) || r.in.empty())
579 return std::nullopt;
580 m.bytes = std::make_shared<std::vector<uint8_t>>(
581 (const uint8_t*)r.in.data(), (const uint8_t*)r.in.data() + r.in.size());
582 return m;
583}
584
585inline std::optional<native_scene_message> parse_native_scene(std::string_view body)
586{
587 native_scene_message m;
588 detail::reader r{body};
589 uint8_t reset{};
590 if(!r(m.seq) || !r(reset))
591 return std::nullopt;
592 m.reset = reset != 0;
593 m.json.assign(r.in.data(), r.in.size());
594 return m;
595}
596
597// Body of any records frame. All-or-nothing: a truncated or inconsistent
598// record rejects the frame rather than applying part of it.
599template <typename Record>
600std::optional<records_message<Record>> parse_records(std::string_view body)
601{
602 records_message<Record> m;
603 detail::reader r{body};
604 uint32_t count{};
605 if(!r(m.seq) || !r(count))
606 return std::nullopt;
607
608 // Bounds count before the resize below can be asked for an absurd size.
609 if(count > r.in.size() / Record::min_size)
610 return std::nullopt;
611
612 m.records.resize(count);
613 for(auto& rec : m.records)
614 if(!rec.read(r))
615 return std::nullopt;
616 if(!r.in.empty())
617 return std::nullopt;
618 return m;
619}
620
621inline std::optional<transforms_message> parse_transforms(std::string_view body)
622{
623 return parse_records<transform_record>(body);
624}
625
626// Encoders, for the tests and for any C++ sender.
627inline std::string encode_frame(frame_type t, std::string_view body)
628{
629 std::string out;
630 out.reserve(5 + body.size());
631 detail::writer{out}(uint32_t(body.size() + 1));
632 out.push_back(char(t));
633 out.append(body);
634 return out;
635}
636
637inline std::string
638encode_scene(uint64_t seq, std::string_view glb, frame_type t = frame_type::scene)
639{
640 std::string body;
641 detail::writer{body}(seq);
642 body.append(glb);
643 return encode_frame(t, body);
644}
645
646inline std::string encode_mesh(
647 uint64_t seq, const std::string& key, const std::vector<native_primitive>& prims)
648{
649 std::string body;
650 detail::writer w{body};
651 w(seq), w(key), w(uint16_t(prims.size()));
652 auto put = [&](const auto& v) {
653 if(v)
654 body.append((const char*)v->data(), v->size() * sizeof((*v)[0]));
655 };
656 for(const auto& p : prims)
657 {
658 const uint8_t attrs = (p.normals ? mesh_normal : 0) | (p.texcoords ? mesh_texcoord0 : 0)
659 | (p.texcoords1 ? mesh_texcoord1 : 0) | (p.colors ? mesh_color0 : 0)
660 | (p.joints && p.weights ? mesh_skin : 0);
661 w(p.vertex_count), w(uint32_t(p.indices ? p.indices->size() : 0)), w(attrs),
662 w(uint16_t(p.targets.size()));
663 put(p.positions), put(p.normals), put(p.texcoords), put(p.texcoords1), put(p.colors);
664 if(p.joints && p.weights)
665 put(p.joints), put(p.weights);
666 put(p.indices);
667 for(const auto& t : p.targets)
668 {
669 w(uint8_t((t.positions ? 1 : 0) | (t.normals ? 2 : 0)));
670 put(t.positions), put(t.normals);
671 }
672 }
673 return encode_frame(frame_type::mesh, body);
674}
675
676inline std::string encode_image(
677 uint64_t seq, const std::string& key, const std::string& mime, std::string_view bytes)
678{
679 std::string body;
680 detail::writer w{body};
681 w(seq), w(key), w(mime);
682 body.append(bytes);
683 return encode_frame(frame_type::image, body);
684}
685
686inline std::string encode_native_scene(uint64_t seq, bool reset, std::string_view json)
687{
688 std::string body;
689 detail::writer w{body};
690 w(seq), w(uint8_t(reset ? 1 : 0));
691 body.append(json);
692 return encode_frame(frame_type::native_scene, body);
693}
694
695template <typename Record>
696std::string encode_records(frame_type t, uint64_t seq, const std::vector<Record>& records)
697{
698 std::string body;
699 detail::writer w{body};
700 w(seq), w(uint32_t(records.size()));
701 for(const auto& rec : records)
702 rec.write(w);
703 return encode_frame(t, body);
704}
705
706inline std::string
707encode_transforms(uint64_t seq, const std::vector<transform_record>& records)
708{
709 return encode_records(frame_type::transforms, seq, records);
710}
711
712// Incremental frame splitter for a byte stream: feed() whatever the socket
713// delivered, get called back once per complete frame. Once it has failed (a
714// zero or oversized length), the stream cannot be resynchronised and the
715// connection should be dropped.
717{
718public:
719 template <typename F>
720 bool feed(std::string_view bytes, F&& on_frame)
721 {
722 if(m_failed)
723 return false;
724 m_buffer.append(bytes);
725
726 std::size_t pos = 0;
727 while(m_buffer.size() - pos >= 4)
728 {
729 uint32_t len{};
730 std::memcpy(&len, m_buffer.data() + pos, 4);
731 if(len == 0 || len > max_frame_size)
732 {
733 m_failed = true;
734 m_buffer.clear();
735 return false;
736 }
737 if(m_buffer.size() - pos - 4 < len)
738 break;
739
740 const auto type = frame_type(uint8_t(m_buffer[pos + 4]));
741 on_frame(type, std::string_view{m_buffer.data() + pos + 5, len - 1});
742 pos += 4 + len;
743 }
744 m_buffer.erase(0, pos);
745 return true;
746 }
747
748 bool failed() const noexcept { return m_failed; }
749 std::size_t buffered() const noexcept { return m_buffer.size(); }
750
751private:
752 std::string m_buffer;
753 bool m_failed{false};
754};
755
756}
STL namespace.
Definition MIDISync.hpp:149