Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
mermaid.hpp
Go to the documentation of this file.
1// sbl/dsp/diagram/mermaid.hpp — Render a Graph as a Mermaid flowchart (AP-037)
2//
3// Host-only: uses snprintf. Depth::Full draws every node the components
4// described — the paper's figure. Depth::Components folds each nested group
5// into one box and keeps the edges between boxes, one per pair.
6//
7// Usage:
8// char text[8192];
9// sbl::dsp::diagram::render_mermaid(graph, text, sizeof(text), Depth::Full);
10
11#ifndef SBL_DSP_DIAGRAM_MERMAID_HPP_
12#define SBL_DSP_DIAGRAM_MERMAID_HPP_
13
14#include <cstddef>
15#include <cstdint>
16#include <cstdio>
17
19
20namespace sbl::dsp::diagram {
21
22enum class Depth : uint8_t {
23 Full, ///< every node, as the components described them
24 Components, ///< nested groups folded to one box each
25};
26
27namespace detail {
28
29struct Out {
30 char* buf;
31 size_t cap;
32 size_t pos = 0;
33
34 // The format string is a literal at every call site but arrives through
35 // a template parameter pack, which -Wformat-security cannot see through.
36 template<typename... Args>
37 void line(const char* fmt, Args... args) {
38 if (pos >= cap) return;
39#if defined(__GNUC__)
40#pragma GCC diagnostic push
41#pragma GCC diagnostic ignored "-Wformat-security"
42#endif
43 const int n = snprintf(buf + pos, cap - pos, fmt, args...);
44#if defined(__GNUC__)
45#pragma GCC diagnostic pop
46#endif
47 if (n < 0) return;
48 pos += static_cast<size_t>(n) < cap - pos ? static_cast<size_t>(n) : cap - pos - 1;
49 if (pos < cap - 1) buf[pos++] = '\n';
50 buf[pos] = '\0';
51 }
52};
53
54/// Label with the paper symbol and the cheap live parameter.
55inline void node_label(const Node& n, char* out, size_t cap) {
56 switch (n.kind) {
57 case Kind::DelayLine: snprintf(out, cap, "%s<br/>z^-%.1f", n.name, static_cast<double>(n.value)); break;
58 case Kind::Loss: snprintf(out, cap, "%s<br/>LP %.0f Hz", n.name, static_cast<double>(n.value)); break;
59 case Kind::Allpass: snprintf(out, cap, "%s<br/>AP %.2f", n.name, static_cast<double>(n.value)); break;
60 case Kind::DcBlock: snprintf(out, cap, "%s<br/>HP %.0f Hz", n.name, static_cast<double>(n.value)); break;
61 case Kind::Reflection: snprintf(out, cap, "%s<br/>r = %+.2g", n.name, static_cast<double>(n.value)); break;
62 case Kind::Load:
63 case Kind::Resonator: snprintf(out, cap, "%s<br/>%.0f Hz, Q %.1f", n.name, static_cast<double>(n.value), static_cast<double>(n.value2)); break;
64 case Kind::Bandpass: snprintf(out, cap, "%s<br/>BP %.0f Hz, Q %.1f", n.name, static_cast<double>(n.value), static_cast<double>(n.value2)); break;
65 case Kind::Gain: snprintf(out, cap, "%s<br/>× %.2f", n.name, static_cast<double>(n.value)); break;
66 default: snprintf(out, cap, "%s", n.name); break;
67 }
68}
69
70/// Shape by kind: the reader should tell a delay from a filter at a glance.
71inline void node_decl(const Node& n, uint8_t id, char* out, size_t cap) {
72 char label[96];
73 node_label(n, label, sizeof(label));
74 switch (n.kind) {
75 case Kind::DelayLine: snprintf(out, cap, "n%u[\"%s\"]", id, label); break;
76 case Kind::Loss:
77 case Kind::Allpass:
78 case Kind::DcBlock: snprintf(out, cap, "n%u(\"%s\")", id, label); break;
79 case Kind::Reflection: snprintf(out, cap, "n%u{{\"%s\"}}", id, label); break;
80 case Kind::Junction: snprintf(out, cap, "n%u((\"%s\"))", id, label); break;
81 case Kind::Source: snprintf(out, cap, "n%u>\"%s\"]", id, label); break;
82 case Kind::Load:
83 case Kind::Resonator: snprintf(out, cap, "n%u[(\"%s\")]", id, label); break;
84 case Kind::Bandpass:
85 case Kind::Gain: snprintf(out, cap, "n%u(\"%s\")", id, label); break;
86 case Kind::Sum: snprintf(out, cap, "n%u((\"%s\"))", id, label); break;
87 case Kind::Tap: snprintf(out, cap, "n%u[/\"%s\"/]", id, label); break;
88 case Kind::Component: snprintf(out, cap, "n%u[[\"%s\"]]", id, label); break;
89 }
90}
91
92inline const char* wave_name(Wave w) {
93 switch (w) {
94 case Wave::Velocity: return "v";
95 case Wave::Force: return "F";
96 case Wave::Signal: return "";
97 }
98 return "";
99}
100
101inline void edge_decl(uint8_t from, uint8_t to, Wave w, char* out, size_t cap) {
102 const char* label = wave_name(w);
103 if (label[0]) snprintf(out, cap, "n%u -->|%s| n%u", from, label, to);
104 else snprintf(out, cap, "n%u --> n%u", from, to);
105}
106
107/// The group a node renders in at Components depth: nested groups fold to their id.
108inline uint8_t fold_target(const Graph& g, uint8_t group) {
109 // A group with a parent folds; a top-level group stays a subgraph.
110 return (group != NO_GROUP && g.group(group).parent != NO_GROUP) ? group : NO_GROUP;
111}
112
113inline void emit_group(const Graph& g, uint8_t gid, Depth depth, Out& o, int indent) {
114 const char* pad = indent ? " " : "";
115 o.line("%ssubgraph g%u[\"%s\"]", pad, gid, g.group(gid).name);
116 // Child groups
117 for (uint8_t c = 0; c < g.group_count(); ++c) {
118 if (g.group(c).parent != gid) continue;
119 if (depth == Depth::Components) {
120 o.line("%s n%u[[\"%s\"]]", pad, 200 + c, g.group(c).name);
121 } else {
122 emit_group(g, c, depth, o, indent + 1);
123 }
124 }
125 // Own nodes
126 for (uint8_t i = 0; i < g.node_count(); ++i) {
127 if (g.node(i).group != gid) continue;
128 char decl[160];
129 node_decl(g.node(i), i, decl, sizeof(decl));
130 o.line("%s %s", pad, decl);
131 }
132 o.line("%send", pad);
133}
134
135} // namespace detail
136
137/**
138 * @brief Render the graph as a Mermaid flowchart into a buffer
139 * @return characters written (excluding the terminator)
140 */
141inline size_t render_mermaid(const Graph& g, char* buf, size_t cap, Depth depth = Depth::Full) {
142 if (cap == 0) return 0;
143 detail::Out o{buf, cap};
144 buf[0] = '\0';
145 o.line("flowchart LR");
146
147 for (uint8_t gid = 0; gid < g.group_count(); ++gid) {
148 if (g.group(gid).parent == NO_GROUP) detail::emit_group(g, gid, depth, o, 0);
149 }
150 // Ungrouped nodes
151 for (uint8_t i = 0; i < g.node_count(); ++i) {
152 if (g.node(i).group != NO_GROUP) continue;
153 char decl[160];
154 detail::node_decl(g.node(i), i, decl, sizeof(decl));
155 o.line(" %s", decl);
156 }
157
158 // Edges. At Components depth an edge whose end sits in a folded group is
159 // redirected to that group's box, and duplicates between boxes dropped.
160 uint8_t seen_from[Graph::MAX_EDGES];
161 uint8_t seen_to[Graph::MAX_EDGES];
162 uint8_t seen = 0;
163 for (uint8_t i = 0; i < g.edge_count(); ++i) {
164 const Edge& e = g.edge(i);
165 uint8_t from = e.from, to = e.to;
166 if (depth == Depth::Components) {
167 const uint8_t gf = detail::fold_target(g, g.node(e.from).group);
168 const uint8_t gt = detail::fold_target(g, g.node(e.to).group);
169 if (gf != NO_GROUP) from = static_cast<uint8_t>(200 + gf);
170 if (gt != NO_GROUP) to = static_cast<uint8_t>(200 + gt);
171 if (from == to) continue; // internal to a folded box
172 bool dup = false;
173 for (uint8_t k = 0; k < seen; ++k) {
174 if (seen_from[k] == from && seen_to[k] == to) { dup = true; break; }
175 }
176 if (dup) continue;
177 if (seen < Graph::MAX_EDGES) { seen_from[seen] = from; seen_to[seen] = to; ++seen; }
178 }
179 char decl[64];
180 detail::edge_decl(from, to, e.wave, decl, sizeof(decl));
181 o.line(" %s", decl);
182 }
183 return o.pos;
184}
185
186} // namespace sbl::dsp::diagram
187
188#endif // SBL_DSP_DIAGRAM_MERMAID_HPP_
uint8_t node_count() const
Definition graph.hpp:93
static constexpr uint8_t MAX_EDGES
Definition graph.hpp:71
uint8_t group_count() const
Definition graph.hpp:95
const Group & group(uint8_t i) const
Definition graph.hpp:98
const Edge & edge(uint8_t i) const
Definition graph.hpp:97
uint8_t edge_count() const
Definition graph.hpp:94
const Node & node(uint8_t i) const
Definition graph.hpp:96
A model's wiring, as data (AP-037)
uint8_t fold_target(const Graph &g, uint8_t group)
The group a node renders in at Components depth: nested groups fold to their id.
Definition mermaid.hpp:108
void edge_decl(uint8_t from, uint8_t to, Wave w, char *out, size_t cap)
Definition mermaid.hpp:101
void emit_group(const Graph &g, uint8_t gid, Depth depth, Out &o, int indent)
Definition mermaid.hpp:113
void node_label(const Node &n, char *out, size_t cap)
Label with the paper symbol and the cheap live parameter.
Definition mermaid.hpp:55
const char * wave_name(Wave w)
Definition mermaid.hpp:92
void node_decl(const Node &n, uint8_t id, char *out, size_t cap)
Shape by kind: the reader should tell a delay from a filter at a glance.
Definition mermaid.hpp:71
A model's wiring as data, for pictures.
Definition graph.hpp:23
Wave
What travels along an edge.
Definition graph.hpp:44
size_t render_mermaid(const Graph &g, char *buf, size_t cap, Depth depth=Depth::Full)
Render the graph as a Mermaid flowchart into a buffer.
Definition mermaid.hpp:141
@ Loss
value: cutoff in Hz
@ Bandpass
a banded-waveguide band; value: Hz, value2: Q
@ Component
a folded group, made by the renderer
@ Gain
value: the factor
@ DcBlock
value: corner in Hz
@ Junction
where waves scatter or a source meets the string
@ Resonator
a modal source; value: Hz, value2: Q
@ Sum
where signals add
@ Reflection
value: coefficient (−1 wall, +1 free end)
@ DelayLine
value: length in samples
@ Load
a lumped body; value: Hz, value2: Q
@ Source
a driving signal (bow velocity, pluck)
@ Allpass
value: delay in samples
@ Components
nested groups folded to one box each
@ Full
every node, as the components described them
constexpr uint8_t NO_GROUP
Definition graph.hpp:46
const char * name
Definition graph.hpp:64
const char * name
short label; string literals only, the graph does not own it
Definition graph.hpp:51
void line(const char *fmt, Args... args)
Definition mermaid.hpp:37