Loading...
Searching...
No Matches
SignalDisplay.hpp
1#pragma once
2#include <Process/Dataflow/NodeItem.hpp>
3
4#include <Scenario/Document/Interval/IntervalModel.hpp>
5
6#include <Effect/EffectLayer.hpp>
7#include <Effect/EffectLayout.hpp>
8
9#include <score/application/GUIApplicationContext.hpp>
10
11#include <ossia/detail/math.hpp>
12#include <ossia/detail/parse_strict.hpp>
13#include <ossia/math/safe_math.hpp>
14#include <ossia/network/value/format_value.hpp>
15#include <ossia/network/value/value_conversion.hpp>
16
17#include <QPainter>
18
19#include <halp/audio.hpp>
20#include <halp/callback.hpp>
21#include <halp/controls.hpp>
22#include <halp/meta.hpp>
23
24namespace Ui
25{
26using value_out = halp::callback<"out", ossia::value>;
27};
28namespace Ui::SignalDisplay
29{
30struct Node
31{
32 halp_meta(name, "Signal display")
33 halp_meta(c_name, "SignalDisplay")
34 halp_meta(category, "Monitoring")
35 halp_meta(author, "ossia score")
36 halp_meta(
37 manual_url,
38 "https://ossia.io/score-docs/common-practices/"
39 "4-audio.html#analysing-an-audio-signal")
40 halp_meta(description, "Visualize an input signal")
41 halp_meta(uuid, "9906e563-ddeb-4ecd-908c-952baee2a0a5")
42 halp_flag(fully_custom_item);
43 halp_flag(temporal);
44 halp_flag(loops_by_default);
45 halp_flag(ossia_show_ports_by_default);
46
47 using vec_type = boost::container::small_vector<float, 8>;
48 struct
49 {
50 struct : halp::val_port<"in", std::optional<ossia::value>>
51 {
52 enum widget
53 {
54 control
55 };
56 } port;
57 } inputs;
58
59 struct
60 {
61 struct : halp::val_port<"out", std::optional<vec_type>>
62 {
63 enum widget
64 {
65 control
66 };
67 } port;
68 } outputs;
69
71 {
72 vec_type& ret;
73 void operator()() const noexcept { }
74 void operator()(ossia::impulse) const noexcept { ret.push_back(1.f); }
75 void operator()(int v) const noexcept { ret.push_back(v); }
76 void operator()(float v) const noexcept { ret.push_back(v); }
77 void operator()(bool v) const noexcept { ret.push_back(v ? 1.f : 0.f); }
78 void operator()(std::string_view v) const noexcept
79 {
80 if(auto res = ossia::parse_strict<float>(v))
81 ret.push_back(*res);
82 }
83 template <std::size_t N>
84 void operator()(std::array<float, N> arr) const noexcept
85 {
86 ret.insert(ret.end(), arr.begin(), arr.end());
87 }
88 void operator()(const std::vector<ossia::value>& arr) const noexcept
89 {
90 ret.reserve(1 + arr.size());
91 for(auto& val : arr)
92 {
93 ret.push_back(ossia::convert<float>(val));
94 }
95 }
96 void operator()(const ossia::value_map_type& arr) const noexcept
97 {
98 ret.reserve(1 + arr.size());
99 for(auto& [k, v] : arr)
100 {
101 ret.push_back(ossia::convert<float>(v));
102 }
103 }
104 };
105
106 using tick = halp::tick_flicks;
107 void operator()(halp::tick_flicks tk)
108 {
109 if(auto& opt_v = inputs.port.value)
110 {
111 const auto& v = *opt_v;
112 const float val = ossia::convert<float>(v);
113 if(ossia::safe_isnan(val) || ossia::safe_isinf(val))
114 return;
115
116 vec_type ret;
117 ret.push_back(float(tk.relative_position));
118
119 v.apply(value_visitor{ret});
120
121 outputs.port.value = static_cast<vec_type&&>(ret);
122 }
123 }
125 {
126 static constexpr int timestamp_index = 0;
127 static constexpr int first_value_index = 1;
128
129 public:
130 Scenario::IntervalModel* m_interval{};
131
132 std::vector<vec_type> m_values;
133 vec_type min = {0.};
134 vec_type max = {1.};
135 int num_rows = 0;
136
137 Layer(
138 const Process::ProcessModel& process, const Process::Context& doc,
139 QGraphicsItem* parent)
141 , m_interval{Scenario::closestParentInterval(process.parent())}
142 {
143 setAcceptedMouseButtons({});
144 if(m_interval)
145 {
146 const Process::PortFactoryList& portFactory
148
149 auto inl = process.inlets().front();
150
151 // Signal Display has two dispaly mode, one time-line mode and one
152 if(parent->type() == Process::NodeItem::Type)
153 {
154 inl->hidden = true;
155 auto fact = portFactory.get(inl->concreteKey());
156 auto port = fact->makePortItem(*inl, doc, this, this);
157 port->setPos(0, 5);
158 }
159 else
160 {
161 inl->hidden = false;
162 }
163
164 connect(
165 m_interval, &Scenario::IntervalModel::executionEvent, this,
166 [this](Scenario::IntervalExecutionEvent ev) {
167 switch(ev)
168 {
169 case Scenario::IntervalExecutionEvent::Playing:
170 case Scenario::IntervalExecutionEvent::Stopped:
171 reset();
172 break;
173 default:
174 break;
175 }
176 });
177
178 auto outl = safe_cast<Process::ControlOutlet*>(process.outlets().front());
179 connect(
180 outl, &Process::ControlOutlet::valueChanged, this,
181 [this](const ossia::value& v) {
182 auto& val = *v.target<std::vector<ossia::value>>();
183 const int N = std::ssize(val);
184 if(N < 2)
185 return;
186 this->num_rows = std::max(this->num_rows, N - 1);
187 // [0] isn't useful as it is the timestamp row but better for code consistency - less branches.
188 if(min.size() < N)
189 min.resize(N, std::numeric_limits<float>::max());
190 if(max.size() < N)
191 max.resize(N, std::numeric_limits<float>::lowest());
192
193 vec_type vv;
194 vv.resize(N, boost::container::default_init);
195
196 int i = 0;
197 for(auto it = val.begin(); it != val.end(); ++it)
198 {
199 const float r = *it->target<float>();
200 vv[i] = r;
201 if(r < min[i])
202 min[i] = r;
203 if(r > max[i])
204 max[i] = r;
205
206 ++i;
207 }
208
209 // Handle looping: clear when we jump back in time
210 if(!m_values.empty() && !m_values.back().empty())
211 if(vv[timestamp_index] < m_values.back()[timestamp_index])
212 {
213 m_values.clear();
214 this->num_rows = 0;
215 }
216
217 m_values.push_back(std::move(vv));
218
219 update();
220 });
221 }
222 }
223
224 void reset()
225 {
226 min = {};
227 max = {};
228 this->num_rows = 0;
229 m_values.clear();
230 update();
231 }
232
233 void draw_row_simple(QPainter* p, qreal w, qreal h, int row_index, auto to_01) const
234 {
235 for(int start_idx = 0; start_idx < std::ssize(m_values) - 1; start_idx++)
236 {
237 int end_idx = start_idx + 1;
238
239 const auto* p0 = &m_values[start_idx];
240 const auto& v0 = *p0;
241 const auto N0 = std::ssize(*p0);
242 if(N0 <= row_index)
243 continue;
244 const auto* p1 = &m_values[end_idx];
245 const auto& v1 = *p1;
246 const auto N1 = std::ssize(*p1);
247 if(N1 <= row_index)
248 continue;
249 QPointF start = {v0[timestamp_index] * w, to_01(v0[row_index]) * h};
250 QPointF end = {v1[timestamp_index] * w, to_01(v1[row_index]) * h};
251 p->drawLine(start, end);
252 }
253 }
254
255 void draw_row(QPainter* p, qreal w, qreal h, int row_index, auto to_01) const
256 {
257 double quality = std::clamp(std::ceil(std::sqrt(0.1 + num_rows)), 1., 5.);
258 std::optional<int> last_idx;
259 for(int start_idx = 0; start_idx < std::ssize(m_values) - 1; start_idx++)
260 {
261 const auto* p0 = &m_values[start_idx];
262 const auto& v0 = *p0;
263 const auto N0 = std::ssize(*p0);
264 if(N0 <= row_index)
265 continue;
266
267 // To handle long jumps in values
268 if(last_idx && *last_idx < start_idx)
269 {
270 const auto& v0 = m_values[*last_idx];
271 const auto& v1 = v0;
272 QPointF p0 = {v0[timestamp_index] * w, to_01(v0[row_index]) * h};
273 QPointF p1 = {v1[timestamp_index] * w, to_01(v1[row_index]) * h};
274
275 if(QLineF l{p0, p1}; l.length() > quality && p0.x() < p1.x())
276 {
277 p->drawLine(l);
278 last_idx = start_idx;
279 }
280 }
281
282 // Find the next point at least "quality" px away
283 auto x0 = v0[timestamp_index] * w;
284 decltype(p0) p1 = nullptr;
285 std::optional<int> last_viable_end;
286 double x1 = x0;
287 for(int end_idx = start_idx + 1; end_idx < std::ssize(m_values); end_idx++)
288 {
289 auto pp1 = &m_values[end_idx];
290 const auto N1 = std::ssize(*pp1);
291 if(N1 <= row_index)
292 continue;
293
294 last_viable_end = end_idx;
295
296 const auto& v1 = *pp1;
297 x1 = v1[timestamp_index] * w;
298 if((x1 - x0) < quality)
299 {
300 continue;
301 }
302 else if(x1 - x0 > 10.)
303 {
304 break;
305 }
306 else
307 {
308 p1 = pp1;
309 break;
310 }
311 }
312
313 if(p1)
314 {
315 const auto& v1 = *p1;
316 QPointF p0 = {x0, to_01(v0[row_index]) * h};
317 QPointF p1 = {x1, to_01(v1[row_index]) * h};
318 p->drawLine(p0, p1);
319 last_idx = last_viable_end;
320 }
321 }
322 }
323
324 void draw_row_constant(QPainter* p, qreal w, qreal h, int row_index) const
325 {
326 for(auto& val : m_values)
327 {
328 if(std::ssize(val) > row_index)
329 {
330 QPointF p0 = {val[timestamp_index] * w, 0.5 * h};
331
332 p->drawPoint(p0);
333 }
334 }
335 }
336 void paint_impl(QPainter* p) const override
337 {
338 if(m_values.size() < 2 || this->num_rows == 0)
339 return;
340
341 p->save();
342 p->setRenderHint(QPainter::Antialiasing, true);
343 p->setPen(score::Skin::instance().Light.main.pen1_solid_flat_miter);
344
345 const auto w = m_defaultWidth;
346 const auto h = height() / num_rows;
347
348 for(int row = 0; row < num_rows; ++row)
349 {
350 const int row_index = row + 1;
351
352 const auto min = this->min[row_index];
353 const auto max = this->max[row_index];
354 if(min != max)
355 {
356 draw_row_simple(p, w, h, row_index, [min, max](float v) {
357 return 1.f - (v - min) / (max - min);
358 });
359 }
360 else
361 {
362 draw_row_constant(p, w, h, row_index);
363 }
364
365 p->translate(QPointF{0, h});
366 }
367
368 p->setRenderHint(QPainter::Antialiasing, false);
369 p->restore();
370 }
371 };
372};
373}
Definition EffectLayer.hpp:16
Definition PortFactory.hpp:74
The Process class.
Definition score-lib-process/Process/Process.hpp:61
Definition IntervalModel.hpp:50
FactoryType * get(const key_type &k) const noexcept
Get a particular factory from its ConcreteKey.
Definition InterfaceList.hpp:123
Definition ProcessContext.hpp:12
Definition SignalDisplay.hpp:125
Definition SignalDisplay.hpp:71
Definition SignalDisplay.hpp:31
const T & interfaces() const
Access to a specific interface list.
Definition ApplicationContext.hpp:67