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