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