Loading...
Searching...
No Matches
SignalDisplay.hpp
1#pragma once
2#include <Process/Dataflow/NodeItem.hpp>
3#include <Process/Dataflow/Port.hpp>
4#include <Process/Dataflow/PortFactory.hpp>
5#include <Process/Dataflow/PortItem.hpp>
6#include <Process/Monitoring/EventRing.hpp>
7#include <Process/Monitoring/MonitorLink.hpp>
8
9#include <Scenario/Document/Interval/IntervalModel.hpp>
10
11#include <Effect/EffectLayer.hpp>
12#include <Effect/EffectLayout.hpp>
13
14#include <score/application/GUIApplicationContext.hpp>
15
16#include <ossia/detail/math.hpp>
17#include <ossia/detail/parse_strict.hpp>
18#include <Process/Monitoring/Monitor.hpp>
19#include <ossia/math/safe_math.hpp>
20#include <ossia/network/value/format_value.hpp>
21#include <ossia/network/value/value_conversion.hpp>
22
23#include <QGraphicsSceneHoverEvent>
24#include <QPainter>
25
26#include <Ui/Monitor/LayerHelpers.hpp>
27#include <Ui/Monitor/Ring.hpp>
28#include <QGraphicsSceneMouseEvent>
29#include <Ui/Monitor/Paint.hpp>
30#include <halp/audio.hpp>
31#include <halp/callback.hpp>
32#include <halp/controls.hpp>
33#include <halp/meta.hpp>
34
35
36namespace Ui
37{
38using value_out = halp::callback<"out", ossia::value>;
39};
40namespace Ui::SignalDisplay
41{
42enum class Mode
43{
44 Timeline,
45 Trail,
46 XY,
47 Bars,
48 Array,
49 Waterfall,
50 Histogram
51};
52inline constexpr std::string_view mode_names[]{
53 "Timeline", "Trail", "XY", "Bars", "Array", "Waterfall", "Histogram"};
54
55static constexpr int max_components = 32;
56static constexpr int max_array = 2048;
57
59struct Sample
60{
62 float position{};
64 double time{};
65 int count{};
66 std::array<float, max_components> v{};
67};
68
70{
71 int count{};
72 std::array<float, max_array> v{};
73};
74
80
81struct Node
82{
83 halp_meta(name, "Signal display")
84 halp_meta(c_name, "SignalDisplay")
85 halp_meta(category, "Monitoring/Control")
86 halp_meta(author, "ossia score")
87 halp_meta(
88 manual_url,
89 "https://ossia.io/score-docs/common-practices/"
90 "4-audio.html#analysing-an-audio-signal")
91 halp_meta(
92 description,
93 "Plot a value stream: along the timeline, as a rolling trail, as XY, "
94 "bars, an array grid or waterfall, or a histogram")
95 halp_meta(uuid, "9906e563-ddeb-4ecd-908c-952baee2a0a5")
96 halp_flag(fully_custom_item);
97 halp_flag(temporal);
98 halp_flag(loops_by_default);
99 halp_flag(ossia_show_ports_by_default);
100
101 using vec_type = boost::container::small_vector<float, 8>;
102 struct
103 {
104 struct : halp::val_port<"in", std::optional<ossia::value>>
105 {
106 enum widget
107 {
108 control
109 };
110 } port;
112 halp::toggle<"Fixed range"> fixed;
113 halp::spinbox_f32<"Min", halp::range{-1e6, 1e6, 0.}> min;
114 halp::spinbox_f32<"Max", halp::range{-1e6, 1e6, 1.}> max;
115
116 struct : halp::combobox_t<"Mode", Mode>
117 {
118 struct range
119 {
120 std::string_view values[7]{mode_names[0], mode_names[1], mode_names[2], mode_names[3],
121 mode_names[4], mode_names[5], mode_names[6]};
122 Mode init{Mode::Timeline};
123 };
124 } mode;
125 struct : halp::spinbox_f32<"History", halp::range{0.1, 600., 10.}>
126 {
127 halp_meta(description, "Seconds shown by the trail, XY and histogram, and over which their range is fitted")
128 } history;
129 struct : halp::spinbox_i32<"Columns", halp::range{0, 512, 0}>
130 {
131 halp_meta(description, "Cells per row of the array grid; 0 picks one (32 for a DMX universe)")
132 } columns;
133 struct : halp::combobox_t<"Palette", Ui::Monitor::ColorMap>
134 {
135 halp_meta(description, "Colours of the array grid and the waterfall, from the lowest value to the highest")
136 struct range
137 {
138 std::string_view values[6]{"Inferno", "Viridis", "Grey", "Magma", "Turbo", "Blue to red"};
139 Ui::Monitor::ColorMap init{Ui::Monitor::ColorMap::Viridis};
140 };
141 } palette;
142 } inputs;
143
144 struct
145 {
146 struct : halp::val_port<"out", std::optional<vec_type>>
147 {
148 enum widget
149 {
150 control
151 };
152 } port;
153 } outputs;
154
156 {
157 vec_type& ret;
158 void operator()() const noexcept { }
159 void operator()(ossia::impulse) const noexcept { ret.push_back(1.f); }
160 void operator()(int v) const noexcept { ret.push_back(v); }
161 void operator()(float v) const noexcept { ret.push_back(v); }
162 void operator()(bool v) const noexcept { ret.push_back(v ? 1.f : 0.f); }
163 void operator()(std::string_view v) const noexcept
164 {
165 if(auto res = ossia::parse_strict<float>(v))
166 ret.push_back(*res);
167 }
168 template <std::size_t N>
169 void operator()(std::array<float, N> arr) const noexcept
170 {
171 ret.insert(ret.end(), arr.begin(), arr.end());
172 }
173 void operator()(const std::vector<ossia::value>& arr) const noexcept
174 {
175 ret.reserve(1 + arr.size());
176 for(auto& val : arr)
177 {
178 ret.push_back(ossia::convert<float>(val));
179 }
180 }
181 void operator()(const ossia::value_map_type& arr) const noexcept
182 {
183 ret.reserve(1 + arr.size());
184 for(auto& [k, v] : arr)
185 {
186 ret.push_back(ossia::convert<float>(v));
187 }
188 }
189 };
190
193 {
194 float* out;
195 int capacity;
196 int count{};
197 void push(float f) noexcept
198 {
199 if(count < capacity)
200 out[count++] = f;
201 }
202 void operator()() noexcept { }
203 void operator()(ossia::impulse) noexcept { push(1.f); }
204 void operator()(int v) noexcept { push(v); }
205 void operator()(float v) noexcept { push(v); }
206 void operator()(bool v) noexcept { push(v ? 1.f : 0.f); }
207 void operator()(std::string_view v) noexcept
208 {
209 if(auto res = ossia::parse_strict<float>(v))
210 push(*res);
211 }
212 template <std::size_t N>
213 void operator()(std::array<float, N> arr) noexcept
214 {
215 for(float f : arr)
216 push(f);
217 }
218 void operator()(const std::vector<ossia::value>& arr) noexcept
219 {
220 for(auto& val : arr)
221 push(ossia::convert<float>(val));
222 }
223 void operator()(const ossia::value_map_type& arr) noexcept
224 {
225 for(auto& [k, v] : arr)
226 push(ossia::convert<float>(v));
227 }
228 };
229
232 static std::pair<float, float> row_range(
233 bool fixed, float min, float max, float seen_min, float seen_max) noexcept
234 {
235 if(fixed)
236 return {min, max};
237 return {seen_min, seen_max};
238 }
239
240 std::shared_ptr<SignalMonitor> monitor = std::make_shared<SignalMonitor>();
241 double elapsed{};
242 ArrayFrame array;
243
244 using tick = halp::tick_flicks;
245 void operator()(halp::tick_flicks tk)
246 {
247 elapsed += (tk.end_in_flicks - tk.start_in_flicks) / 705600000.;
248 if(auto& opt_v = inputs.port.value)
249 {
250 const auto& v = *opt_v;
251 const float val = ossia::convert<float>(v);
252 if(ossia::safe_isnan(val) || ossia::safe_isinf(val))
253 return;
254
255 if(monitor->watched())
256 {
257 Sample s;
258 s.position = float(tk.relative_position);
259 s.time = elapsed;
260 fixed_visitor vis{s.v.data(), max_components};
261 v.apply(vis);
262 s.count = vis.count;
263 monitor->samples.push(s);
264
265 if(inputs.mode.value == Mode::Array || inputs.mode.value == Mode::Waterfall)
266 {
267 fixed_visitor av{array.v.data(), max_array};
268 v.apply(av);
269 array.count = av.count;
270 monitor->arrays.produce(array);
271 }
272 }
273
274 vec_type ret;
275 ret.push_back(float(tk.relative_position));
276 v.apply(value_visitor{ret});
277 outputs.port.value = static_cast<vec_type&&>(ret);
278 }
279 }
280
282 {
283 static constexpr int timestamp_index = 0;
284 static constexpr int first_value_index = 1;
285
286 public:
287 Scenario::IntervalModel* m_interval{};
288 // Absent from processes which have not been reloaded since they were added
289 const Process::ControlInlet* m_fixed{};
290 const Process::ControlInlet* m_min{};
291 const Process::ControlInlet* m_max{};
292 const Process::ControlInlet* m_mode{};
293 const Process::ControlInlet* m_history{};
294 const Process::ControlInlet* m_columns{};
295 const Process::ControlInlet* m_palette{};
296 const score::CommandStackFacade& m_stack;
297 Ui::Monitor::DoubleClick m_doubleClick;
298 Process::MonitorLink* m_link{};
299
300 // Timeline mode: [position, values...] per value received
301 std::vector<vec_type> m_values;
302 vec_type min = {0.};
303 vec_type max = {1.};
304 int num_rows = 0;
305
306 // The other modes: the samples of the last `history` seconds
307 boost::circular_buffer<Sample> m_recent;
308 std::array<float, max_components> m_seen_min{}, m_seen_max{};
309 int m_components{};
310 ArrayFrame m_array;
311 QImage m_waterfall;
312 int m_waterfall_row{};
313 uint64_t m_dropped{};
314 std::optional<QPointF> m_hover;
315
317 static constexpr int fade_steps = 16;
318 mutable QPolygonF m_polyline;
319 mutable std::array<QList<QLineF>, fade_steps> m_fade;
320
321 Layer(
322 const Process::ProcessModel& process, const Process::Context& doc,
323 QGraphicsItem* parent)
324 : Process::EffectLayerView{parent}
325 , m_interval{Scenario::closestParentInterval(process.parent())}
326 , m_stack{doc.commandStack}
327 {
328 // For the double click; a press goes on to the node
329 setAcceptedMouseButtons(Qt::LeftButton);
330 setAcceptHoverEvents(true);
331 auto inl = process.inlets().front();
332
333 // In the nodal view the layer shows its inlet; in the timeline the
334 // process header does
335 if(parent && parent->type() == Process::NodeItem::Type)
336 {
337 inl->displayHandledExplicitly = true;
338 Ui::Monitor::showInlet(process, 0, doc, this, this);
339 }
340 else
341 {
342 inl->displayHandledExplicitly = false;
343 }
344
345 if(m_interval)
346 {
347 connect(
348 m_interval, &Scenario::IntervalModel::executionEvent, this,
349 [this](Scenario::IntervalExecutionEvent ev) {
350 switch(ev)
351 {
352 case Scenario::IntervalExecutionEvent::Playing:
353 case Scenario::IntervalExecutionEvent::Stopped:
354 reset();
355 break;
356 default:
357 break;
358 }
359 });
360 }
361
362 using namespace Ui::Monitor;
363 m_fixed = controlInlet(process, 1);
364 m_min = controlInlet(process, 2);
365 m_max = controlInlet(process, 3);
366 m_mode = controlInlet(process, 4);
367 m_history = controlInlet(process, 5);
368 m_columns = controlInlet(process, 6);
369 m_palette = controlInlet(process, 7);
370 ModeSwitch::make(m_mode, doc, this, this);
371 repaintOn(this, m_fixed, m_min, m_max, m_mode, m_history, m_columns, m_palette);
372 // The range is only kept up to date for the modes that read it
373 if(m_mode)
374 connect(m_mode, &Process::ControlInlet::valueChanged, this, [this] { fitRanges(); });
375 // The waterfall keeps the colours its rows were drawn with
376 if(m_palette)
377 connect(m_palette, &Process::ControlInlet::valueChanged, this, [this] {
378 m_waterfall = {};
379 });
380
381 m_link = new Process::MonitorLink{
382 process, *this,
383 [this](Process::Monitor& m) { refresh(static_cast<SignalMonitor&>(m)); },
384 [this](Process::Monitor*) { reset(); }, this};
385 }
386
387 Mode mode() const { return Ui::Monitor::enumValue(m_mode, Mode::Timeline, Mode::Histogram); }
388 const std::array<QRgb, 256>& palette() const
389 {
390 using namespace Ui::Monitor;
391 return colorMap(enumValue(m_palette, ColorMap::Viridis, ColorMap::CoolWarm));
392 }
393 double history() const { return std::max(0.1f, Ui::Monitor::floatValue(m_history, 10.f)); }
394 bool fixedRange() const
395 {
396 return m_fixed && m_min && m_max && ossia::convert<bool>(m_fixed->value());
397 }
398
399 void reset()
400 {
401 min = {};
402 max = {};
403 this->num_rows = 0;
404 m_values.clear();
405 m_recent.clear();
406 m_components = 0;
407 fitRanges();
408 m_waterfall = {};
409 update();
410 }
411
415 void mousePressEvent(QGraphicsSceneMouseEvent* ev) override
416 {
417 if(!m_doubleClick(*ev))
418 {
419 ev->ignore();
420 return;
421 }
422 ev->accept();
423 if(mode() == Mode::Histogram)
424 {
425 reset();
426 return;
427 }
428 if(!fixedRange())
429 {
430 fitRanges();
431 m_waterfall = {};
432 update();
433 return;
434 }
435 float lo = std::numeric_limits<float>::max(), hi = std::numeric_limits<float>::lowest();
436 if(mode() == Mode::Array || mode() == Mode::Waterfall)
437 {
438 for(int i = 0; i < m_array.count; i++)
439 {
440 lo = std::min(lo, m_array.v[i]);
441 hi = std::max(hi, m_array.v[i]);
442 }
443 }
444 else
445 {
446 for(const auto& s : m_recent)
447 for(int i = 0; i < s.count; i++)
448 {
449 lo = std::min(lo, s.v[i]);
450 hi = std::max(hi, s.v[i]);
451 }
452 }
453 if(lo > hi)
454 return;
455 if(lo == hi)
456 {
457 lo -= 0.5f;
458 hi += 0.5f;
459 }
460 Ui::Monitor::setControls(m_stack, {{m_min, lo}, {m_max, hi}});
461 }
462
463 void hoverMoveEvent(QGraphicsSceneHoverEvent* ev) override
464 {
465 m_hover = ev->pos();
466 if(mode() == Mode::Array)
467 update();
468 }
469 void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override
470 {
471 m_hover.reset();
472 update();
473 }
474
475 void refresh(SignalMonitor& mon)
476 {
477 bool changed = false;
478 mon.samples.drain([&](const Sample& s) {
479 changed = true;
480 addTimeline(s);
481 addRecent(s);
482 });
483 m_dropped = mon.samples.dropped();
484 if(mon.arrays.consume(m_array))
485 {
486 changed = true;
487 if(mode() == Mode::Waterfall)
488 addWaterfallRow();
489 }
490 // Forget what scrolled out of the history
491 if(!m_recent.empty())
492 {
493 const double horizon = m_recent.back().time - history();
494 while(!m_recent.empty() && m_recent.front().time < horizon)
495 {
496 m_recent.pop_front();
497 changed = true;
498 }
499 }
500 if(changed)
501 {
502 if(usesHistoryRange())
503 fitRanges();
504 update();
505 }
506 }
507
508 void addTimeline(const Sample& s)
509 {
510 const int N = s.count + 1;
511 this->num_rows = std::max(this->num_rows, N - 1);
512 if(int(min.size()) < N)
513 min.resize(N, std::numeric_limits<float>::max());
514 if(int(max.size()) < N)
515 max.resize(N, std::numeric_limits<float>::lowest());
516
517 vec_type vv;
518 vv.resize(N, boost::container::default_init);
519 vv[timestamp_index] = s.position;
520 for(int i = 0; i < s.count; i++)
521 {
522 const float r = s.v[i];
523 vv[i + 1] = r;
524 min[i + 1] = std::min(min[i + 1], r);
525 max[i + 1] = std::max(max[i + 1], r);
526 }
527
528 // Handle looping: clear when we jump back in time
529 if(!m_values.empty() && !m_values.back().empty())
530 if(vv[timestamp_index] < m_values.back()[timestamp_index])
531 {
532 m_values.clear();
533 this->num_rows = 0;
534 }
535
536 m_values.push_back(std::move(vv));
537 }
538
539 void addRecent(const Sample& s)
540 {
541 m_components = std::max(m_components, s.count);
542 Ui::Monitor::pushRing(m_recent, s, 50000);
543 }
544
545 bool usesHistoryRange() const
546 {
547 switch(mode())
548 {
549 case Mode::Trail:
550 case Mode::XY:
551 case Mode::Bars:
552 case Mode::Histogram:
553 return true;
554 default:
555 return false;
556 }
557 }
558
561 {
562 m_seen_min.fill(std::numeric_limits<float>::max());
563 m_seen_max.fill(std::numeric_limits<float>::lowest());
564 for(const auto& s : m_recent)
565 for(int i = 0; i < s.count; i++)
566 {
567 m_seen_min[i] = std::min(m_seen_min[i], s.v[i]);
568 m_seen_max[i] = std::max(m_seen_max[i], s.v[i]);
569 }
570 for(int i = 0; i < max_components; i++)
571 if(m_seen_min[i] > m_seen_max[i])
572 {
573 m_seen_min[i] = 0.f;
574 m_seen_max[i] = 1.f;
575 }
576 }
577
579 std::pair<float, float> rangeOf(int i) const
580 {
581 const bool fixed = fixedRange();
582 auto r = Node::row_range(
583 fixed, fixed ? ossia::convert<float>(m_min->value()) : 0.f,
584 fixed ? ossia::convert<float>(m_max->value()) : 1.f,
585 i < m_components ? m_seen_min[i] : 0.f, i < m_components ? m_seen_max[i] : 1.f);
586 if(r.first == r.second)
587 {
588 r.first -= 0.5f;
589 r.second += 0.5f;
590 }
591 return r;
592 }
593
594 void addWaterfallRow()
595 {
596 const auto r = boundingRect().adjusted(28, 20, -2, -2);
597 const int w = std::max(8, int(r.width())), h = std::max(8, int(r.height()));
598 if(m_waterfall.width() != w || m_waterfall.height() != h)
599 {
600 m_waterfall = QImage(w, h, QImage::Format_RGB32);
601 m_waterfall.fill(palette()[0]);
602 m_waterfall_row = 0;
603 }
604 if(m_array.count == 0)
605 return;
606 const auto& lut = palette();
607 auto [lo, hi] = arrayRange();
608 // Rows go up the image as they come: from the newest down, the image
609 // reads in its own order, wrapping once
610 m_waterfall_row = (m_waterfall_row - 1 + h) % h;
611 auto* line = reinterpret_cast<QRgb*>(m_waterfall.scanLine(m_waterfall_row));
612 for(int x = 0; x < w; x++)
613 {
614 const int i = std::min(m_array.count - 1, x * m_array.count / w);
615 const int idx = std::clamp(int((m_array.v[i] - lo) / (hi - lo) * 255.f), 0, 255);
616 line[x] = lut[idx];
617 }
618 }
619
620 std::pair<float, float> arrayRange() const
621 {
622 if(fixedRange())
623 {
624 float lo = ossia::convert<float>(m_min->value()), hi = ossia::convert<float>(m_max->value());
625 if(lo == hi)
626 hi = lo + 1;
627 return {lo, hi};
628 }
629 float lo = 0, hi = 1;
630 for(int i = 0; i < m_array.count; i++)
631 {
632 lo = std::min(lo, m_array.v[i]);
633 hi = std::max(hi, m_array.v[i]);
634 }
635 // Values that look like DMX or 8-bit colour keep a 0 to 255 range
636 if(hi > 1 && hi <= 255 && lo >= 0)
637 hi = 255;
638 return {lo, hi};
639 }
640
641 void draw_row_simple(QPainter* p, qreal w, qreal h, int row_index, auto to_01) const
642 {
643 for(int start_idx = 0; start_idx < std::ssize(m_values) - 1; start_idx++)
644 {
645 int end_idx = start_idx + 1;
646
647 const auto* p0 = &m_values[start_idx];
648 const auto& v0 = *p0;
649 const auto N0 = std::ssize(*p0);
650 if(N0 <= row_index)
651 continue;
652 const auto* p1 = &m_values[end_idx];
653 const auto& v1 = *p1;
654 const auto N1 = std::ssize(*p1);
655 if(N1 <= row_index)
656 continue;
657 QPointF start = {v0[timestamp_index] * w, to_01(v0[row_index]) * h};
658 QPointF end = {v1[timestamp_index] * w, to_01(v1[row_index]) * h};
659 p->drawLine(start, end);
660 }
661 }
662
663 void draw_row_constant(QPainter* p, qreal w, qreal h, int row_index) const
664 {
665 for(auto& val : m_values)
666 {
667 if(std::ssize(val) > row_index)
668 {
669 QPointF p0 = {val[timestamp_index] * w, 0.5 * h};
670
671 p->drawPoint(p0);
672 }
673 }
674 }
675
676 void paint_impl(QPainter* p) const override
677 {
678 switch(mode())
679 {
680 case Mode::Timeline:
681 paintTimeline(p);
682 break;
683 case Mode::Trail:
684 paintTrail(p);
685 break;
686 case Mode::XY:
687 paintXY(p);
688 break;
689 case Mode::Bars:
690 paintBars(p);
691 break;
692 case Mode::Array:
693 paintArray(p);
694 break;
695 case Mode::Waterfall:
696 paintWaterfall(p);
697 break;
698 case Mode::Histogram:
699 paintHistogram(p);
700 break;
701 }
702 if(m_dropped > 0 && mode() != Mode::Timeline)
704 *p, boundingRect().bottomLeft() - QPointF{0, 16},
705 {QStringLiteral("%1 dropped").arg(m_dropped)});
706 }
707
708 void paintTimeline(QPainter* p) const
709 {
710 if(m_values.size() < 2 || this->num_rows == 0)
711 return;
712
713 p->save();
714 p->setRenderHint(QPainter::Antialiasing, true);
715 p->setPen(score::Skin::instance().Light.main.pen1_solid_flat_miter);
716
717 const auto w = m_defaultWidth > 0 ? m_defaultWidth : boundingRect().width();
718 // Below the strip of the mode switch, as the other modes
719 constexpr double top = 20.;
720 const auto h = std::max(0., height() - top) / num_rows;
721 p->translate(QPointF{0, top});
722
723 const bool fixed = fixedRange();
724 const float fixed_min = fixed ? ossia::convert<float>(m_min->value()) : 0.f;
725 const float fixed_max = fixed ? ossia::convert<float>(m_max->value()) : 1.f;
726
727 for(int row = 0; row < num_rows; ++row)
728 {
729 const int row_index = row + 1;
730
731 const auto [min, max] = Node::row_range(
732 fixed, fixed_min, fixed_max, this->min[row_index], this->max[row_index]);
733 // Values out of a fixed range stay in their row
734 if(fixed)
735 p->setClipRect(QRectF{0., 0., w, h});
736 if(min != max)
737 {
738 draw_row_simple(p, w, h, row_index, [min, max](float v) {
739 return 1.f - (v - min) / (max - min);
740 });
741 }
742 else
743 {
744 draw_row_constant(p, w, h, row_index);
745 }
746
747 p->translate(QPointF{0, h});
748 }
749
750 p->setRenderHint(QPainter::Antialiasing, false);
751 p->restore();
752 }
753
754 void paintTrail(QPainter* p) const
755 {
756 using namespace Ui::Monitor;
757 if(m_recent.empty() || m_components == 0)
758 return;
759 p->save();
760 const QRectF area = boundingRect().adjusted(28, 20, -2, -14);
761 const int rows = std::min(m_components, 16);
762 const double rh = area.height() / rows;
763 const double now = m_recent.back().time;
764 const double span = history();
765 for(int row = 0; row < rows; row++)
766 {
767 const QRectF r{area.left(), area.top() + row * rh, area.width(), rh};
768 const auto [lo, hi] = rangeOf(row);
769 p->setRenderHint(QPainter::Antialiasing, false);
770 p->setPen(gridColor());
771 p->drawLine(r.bottomLeft(), r.bottomRight());
772 // The top of each row, and its bottom when there is room between the
773 // two and the next row's top
774 if(rh >= 16)
775 drawLabel(*p, QRectF(0, r.top(), area.left() - 2, 10), Qt::AlignRight | Qt::AlignTop, QString::number(hi, 'g', 3));
776 if(rh >= 40)
777 drawLabel(*p, QRectF(0, r.bottom() - 12, area.left() - 2, 10), Qt::AlignRight | Qt::AlignBottom, QString::number(lo, 'g', 3));
778 auto& line = m_polyline;
779 line.resize(0);
780 for(const auto& s : m_recent)
781 {
782 if(s.count <= row)
783 continue;
784 const double x = r.right() - (now - s.time) / span * r.width();
785 const double y = r.bottom() - std::clamp((s.v[row] - lo) / (hi - lo), 0.f, 1.f) * r.height();
786 // Held values are steps: a value stays until the next one comes
787 if(!line.isEmpty())
788 line << QPointF{x, line.back().y()};
789 line << QPointF{x, y};
790 }
791 if(!line.isEmpty())
792 line << QPointF{r.right(), line.back().y()};
793 p->setRenderHint(QPainter::Antialiasing, true);
794 p->setPen(QPen(channelColor(row), 1.2));
795 p->drawPolyline(line);
796 }
797 auto times = linearTicks(0., span, area.width(), 50., " s");
798 for(auto& t : times)
799 t.label = t.value == 0 ? QStringLiteral("now") : "-" + t.label;
800 drawXAxisLabels(*p, area, times, [&](double v) { return area.right() - v / span * area.width(); });
801 p->restore();
802 }
803
804 void paintXY(QPainter* p) const
805 {
806 using namespace Ui::Monitor;
807 if(m_recent.empty() || m_components < 2)
808 {
809 drawLabel(*p, boundingRect(), Qt::AlignCenter, QStringLiteral("XY needs two components"));
810 return;
811 }
812 p->save();
813 p->setRenderHint(QPainter::Antialiasing, true);
814 const QRectF b = boundingRect().adjusted(4, 20, -4, -4);
815 const double side = std::min(b.width(), b.height());
816 const QRectF r{b.center().x() - side / 2, b.center().y() - side / 2, side, side};
817 const bool three = m_components >= 3;
818 const auto rx = rangeOf(0), ry = rangeOf(1), rz = three ? rangeOf(2) : std::pair{0.f, 1.f};
819 auto norm = [](float v, std::pair<float, float> rr) {
820 return std::clamp((v - rr.first) / (rr.second - rr.first), 0.f, 1.f) * 2.f - 1.f;
821 };
822 // Three components: an isometric view of the unit cube
823 auto project = [&](float x, float y, float z) {
824 if(!three)
825 return QPointF{r.center().x() + x * side / 2, r.center().y() - y * side / 2};
826 const double s = side * 0.32;
827 return QPointF{
828 r.center().x() + (x - z) * 0.866 * s, r.center().y() - (y + (x + z) * 0.5) * s * 0.9};
829 };
830 p->setPen(gridColor());
831 if(three)
832 {
833 for(float a : {-1.f, 1.f})
834 for(float c : {-1.f, 1.f})
835 {
836 p->drawLine(project(-1, a, c), project(1, a, c));
837 p->drawLine(project(a, -1, c), project(a, 1, c));
838 p->drawLine(project(a, c, -1), project(a, c, 1));
839 }
840 }
841 else
842 {
843 p->drawRect(r);
844 p->drawLine(QPointF{r.center().x(), r.top()}, QPointF{r.center().x(), r.bottom()});
845 p->drawLine(QPointF{r.left(), r.center().y()}, QPointF{r.right(), r.center().y()});
846 }
847 // Older segments fade, by steps: one pen and one call per step
848 const double now = m_recent.back().time, span = history();
849 for(auto& b : m_fade)
850 b.resize(0);
851 QPointF prev;
852 bool has_prev = false;
853 for(const auto& s : m_recent)
854 {
855 if(s.count < 2)
856 continue;
857 const auto pt = project(norm(s.v[0], rx), norm(s.v[1], ry), three ? norm(s.v[2], rz) : 0.f);
858 if(has_prev)
859 {
860 const double alpha = std::clamp(1. - (now - s.time) / span, 0.05, 1.);
861 m_fade[std::min(int(alpha * fade_steps), fade_steps - 1)].append(QLineF{prev, pt});
862 }
863 prev = pt;
864 has_prev = true;
865 }
866 for(int i = 0; i < fade_steps; i++)
867 {
868 if(m_fade[i].isEmpty())
869 continue;
870 QColor c = traceColor();
871 c.setAlphaF((i + 1.) / fade_steps);
872 p->setPen(QPen(c, 1.5));
873 p->drawLines(m_fade[i]);
874 }
875 p->setPen(Qt::NoPen);
876 p->setBrush(warnColor());
877 p->drawEllipse(prev, 4, 4);
878 const auto& last = m_recent.back();
879 QStringList info{QStringLiteral("x %1").arg(last.v[0], 0, 'g', 4), QStringLiteral("y %1").arg(last.v[1], 0, 'g', 4)};
880 if(three)
881 info << QStringLiteral("z %1").arg(last.v[2], 0, 'g', 4);
882 if(isUnderMouse())
883 drawReadout(*p, b.topLeft(), info);
884 p->restore();
885 }
886
887 void paintBars(QPainter* p) const
888 {
889 using namespace Ui::Monitor;
890 if(m_recent.empty())
891 return;
892 const auto& s = m_recent.back();
893 if(s.count == 0)
894 return;
895 p->save();
896 const QRectF r = boundingRect().adjusted(4, 20, -4, -14);
897 const double bw = r.width() / s.count;
898 for(int i = 0; i < s.count; i++)
899 {
900 const auto [lo, hi] = rangeOf(i);
901 const double t = std::clamp((s.v[i] - lo) / (hi - lo), 0.f, 1.f);
902 // From zero when the range holds it, else from the bottom
903 const double zero = (lo < 0 && hi > 0) ? (-lo) / (hi - lo) : 0.;
904 const double y0 = r.bottom() - zero * r.height(), y1 = r.bottom() - t * r.height();
905 const QRectF bar{r.left() + i * bw + 1, std::min(y0, y1), std::max(1., bw - 2), std::abs(y1 - y0)};
906 p->fillRect(QRectF(r.left() + i * bw + 1, r.top(), std::max(1., bw - 2), r.height()), QColor(30, 30, 32));
907 p->fillRect(bar, channelColor(i));
908 if(bw >= 28)
909 drawLabel(*p, QRectF(r.left() + i * bw, r.bottom() + 1, bw, 12), Qt::AlignHCenter | Qt::AlignTop,
910 QString::number(s.v[i], 'g', 3));
911 }
912 p->restore();
913 }
914
915 struct Grid
916 {
917 int cols{}, rows{};
918 double cell{};
919 QRectF area;
920 };
921 Grid arrayGrid() const
922 {
923 Grid g;
924 g.area = boundingRect().adjusted(2, 20, -2, -2);
925 const int n = std::max(1, m_array.count);
926 int cols = m_columns ? ossia::convert<int>(m_columns->value()) : 0;
927 if(cols <= 0)
928 cols = n == 512 ? 32 : std::max(1, int(std::ceil(std::sqrt(n * g.area.width() / std::max(1., g.area.height())))));
929 g.cols = cols;
930 g.rows = (n + cols - 1) / cols;
931 g.cell = std::max(1., std::min(g.area.width() / g.cols, g.area.height() / g.rows));
932 return g;
933 }
934
935 void paintArray(QPainter* p) const
936 {
937 using namespace Ui::Monitor;
938 if(m_array.count == 0)
939 return;
940 p->save();
941 const auto g = arrayGrid();
942 const auto [lo, hi] = arrayRange();
943 const auto& lut = palette();
944 const double gap = g.cell >= 8 ? 1 : 0;
945 int hovered = -1;
946 QFont f = smallFont();
947 f.setPixelSize(int(std::clamp(g.cell * 0.4, 6., 10.)));
948 p->setFont(f);
949 for(int i = 0; i < m_array.count; i++)
950 {
951 const QRectF cell{g.area.left() + (i % g.cols) * g.cell, g.area.top() + (i / g.cols) * g.cell, g.cell - gap, g.cell - gap};
952 const int idx = std::clamp(int((m_array.v[i] - lo) / (hi - lo) * 255.f), 0, 255);
953 p->fillRect(cell, QColor::fromRgb(lut[idx]));
954 if(g.cell >= 22)
955 {
956 p->setPen(qGray(lut[idx]) > 140 ? Qt::black : Qt::white);
957 p->drawText(cell, Qt::AlignCenter, QString::number(m_array.v[i], 'g', 3));
958 }
959 if(m_hover && cell.contains(*m_hover))
960 hovered = i;
961 }
962 QString head = QStringLiteral("%1 values, %2 to %3").arg(m_array.count).arg(lo).arg(hi);
963 if(hovered >= 0)
964 head = QStringLiteral("[%1] = %2").arg(hovered).arg(m_array.v[hovered], 0, 'g', 6);
965 if(isUnderMouse())
966 drawLabel(*p, QRectF(2, 0, boundingRect().width() - 6, 13), Qt::AlignRight | Qt::AlignVCenter, head);
967 p->restore();
968 }
969
970 void paintWaterfall(QPainter* p) const
971 {
972 using namespace Ui::Monitor;
973 if(m_waterfall.isNull())
974 return;
975 const QRectF r = boundingRect().adjusted(28, 20, -2, -2);
976 // Newest row at the top: from it to the end of the image, then the
977 // rows from the start of the image, which are the oldest
978 const int h = m_waterfall.height(), w = m_waterfall.width();
979 const int newest = m_waterfall_row, upper = h - newest;
980 p->drawImage(QRectF(r.left(), r.top(), w, upper), m_waterfall, QRectF(0, newest, w, upper));
981 if(newest > 0)
982 p->drawImage(
983 QRectF(r.left(), r.top() + upper, w, newest), m_waterfall, QRectF(0, 0, w, newest));
984 }
985
986 void paintHistogram(QPainter* p) const
987 {
988 using namespace Ui::Monitor;
989 if(m_recent.empty())
990 return;
991 p->save();
992 const QRectF r = boundingRect().adjusted(4, 20, -4, -14);
993 const auto [lo, hi] = rangeOf(0);
994 std::array<int, 64> bins{};
995 double sum = 0, sum2 = 0;
996 int n = 0;
997 for(const auto& s : m_recent)
998 {
999 if(s.count == 0)
1000 continue;
1001 const float v = s.v[0];
1002 bins[std::clamp(int((v - lo) / (hi - lo) * bins.size()), 0, int(bins.size()) - 1)]++;
1003 sum += v;
1004 sum2 += v * v;
1005 n++;
1006 }
1007 const int peak = std::max(1, *std::max_element(bins.begin(), bins.end()));
1008 const double bw = r.width() / bins.size();
1009 for(std::size_t i = 0; i < bins.size(); i++)
1010 {
1011 const double h = double(bins[i]) / peak * r.height();
1012 p->fillRect(QRectF(r.left() + i * bw, r.bottom() - h, std::max(1., bw - 1), h), traceColor());
1013 }
1014 drawLabel(*p, QRectF(r.left(), r.bottom() + 1, 80, 12), Qt::AlignLeft | Qt::AlignTop, QString::number(lo, 'g', 3));
1015 drawLabel(*p, QRectF(r.right() - 80, r.bottom() + 1, 80, 12), Qt::AlignRight | Qt::AlignTop, QString::number(hi, 'g', 3));
1016 const double mean = n ? sum / n : 0, sd = n ? std::sqrt(std::max(0., sum2 / n - mean * mean)) : 0;
1017 if(isUnderMouse())
1018 drawReadout(*p, r.topLeft(),
1019 {QStringLiteral("n %1").arg(n), QStringLiteral("mean %1").arg(mean, 0, 'g', 4),
1020 QStringLiteral("sd %1").arg(sd, 0, 'g', 4),
1021 QStringLiteral("min %1 max %2").arg(m_seen_min[0], 0, 'g', 4).arg(m_seen_max[0], 0, 'g', 4)});
1022 p->restore();
1023 }
1024 };
1025};
1026}
Definition Port.hpp:242
Definition EffectLayer.hpp:17
Definition EventRing.hpp:27
Definition Monitor.hpp:59
The Process class.
Definition score-lib-process/Process/Process.hpp:82
Definition IntervalModel.hpp:50
Definition LayerHelpers.hpp:94
A small abstraction layer over the score::CommandStack.
Definition CommandStackFacade.hpp:21
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Main plug-in of score.
Definition score-plugin-dataflow/Dataflow/PortItem.hpp:13
Definition BufferDecode.hpp:16
QColor channelColor(int i)
Colours for the n-th channel or trace, distinct on a dark background.
Definition Paint.hpp:69
void pushRing(boost::circular_buffer< T > &ring, V &&v, std::size_t limit=std::numeric_limits< std::size_t >::max())
Definition Ring.hpp:17
ColorMap
Definition Paint.hpp:122
std::vector< Tick > linearTicks(double lo, double hi, double pixels, double min_spacing=18., QString unit={})
Definition Paint.hpp:222
void drawXAxisLabels(QPainter &p, const QRectF &plot, const std::vector< Tick > &ticks, XOf x_of)
Definition Paint.hpp:388
void drawLabel(QPainter &p, const QRectF &r, int flags, const QString &text, QColor c=textColor())
Draws text in the small monitor font, flags as for QPainter::drawText.
Definition Paint.hpp:370
void drawReadout(QPainter &p, QPointF topLeft, const QStringList &lines)
A readout box in a corner: semi-opaque so that it stays legible over traces.
Definition Paint.hpp:440
void showInlet(const Process::ProcessModel &p, std::size_t index, const Process::Context &doc, QGraphicsItem *parent, QObject *owner, double y=5.)
Definition LayerHelpers.hpp:63
void setControls(const score::CommandStackFacade &stack, std::initializer_list< std::pair< const Process::ControlInlet *, ossia::value > > values)
Sets several controls as one undoable edit.
Definition LayerHelpers.hpp:120
STL namespace.
Definition ProcessContext.hpp:12
Definition Monitor.hpp:27
Definition SignalDisplay.hpp:70
Definition SignalDisplay.hpp:916
Definition SignalDisplay.hpp:282
void mousePressEvent(QGraphicsSceneMouseEvent *ev) override
Definition SignalDisplay.hpp:415
void fitRanges()
What each component spans over the history shown.
Definition SignalDisplay.hpp:560
std::pair< float, float > rangeOf(int i) const
Range of component i: the fixed one, or what the history spans.
Definition SignalDisplay.hpp:579
static constexpr int fade_steps
Reused by the painting, which would otherwise allocate on every paint.
Definition SignalDisplay.hpp:317
The components of a value, without allocating: at most out.size().
Definition SignalDisplay.hpp:193
Definition SignalDisplay.hpp:156
Definition SignalDisplay.hpp:82
static std::pair< float, float > row_range(bool fixed, float min, float max, float seen_min, float seen_max) noexcept
Definition SignalDisplay.hpp:232
halp::toggle<"Fixed range"> fixed
Off: each row spans the lowest to the highest value it received.
Definition SignalDisplay.hpp:112
One value as received: when, and its components.
Definition SignalDisplay.hpp:60
float position
Position in the parent interval, 0 to 1.
Definition SignalDisplay.hpp:62
double time
Seconds since execution started.
Definition SignalDisplay.hpp:64
Definition SignalDisplay.hpp:76
Definition MIDISync.hpp:149