Loading...
Searching...
No Matches
ExecutorPortSetup.hpp
1#pragma once
2#include <Process/Process.hpp>
3
4#include <Crousti/File.hpp>
5#include <Crousti/ProcessModel.hpp>
6
7#include <ossia/detail/flat_map.hpp>
8
9#include <avnd/binding/ossia/node.hpp>
10
11namespace oscr
12{
13template <typename T>
17
22template <oscr::has_dynamic_ports T>
24{
25 Process::Inlets m_oldInlets;
26 Process::Outlets m_oldOutlets;
27 ossia::flat_map<Process::Inlet*, std::pair<int, QMetaObject::Connection>>
28 m_connectedControls;
29};
30
31// A halp::folder_port: a std::string control whose widget is a directory picker
32// (`enum widget { folder };`). Like the file ports, its value is a path and must
33// go through score::locateFilePath so that <LIBRARY>:, <PROJECT>: and
34// document-relative paths are resolved before the object reads it.
35template <typename T>
36concept folder_control_port = requires { T::folder; } && requires(T t) {
37 { t.value } -> std::convertible_to<std::string_view>;
38};
39
40template <typename Node, typename Field, std::size_t NPred, std::size_t NField>
42{
43 using ExecNode = safe_node<Node>;
44 const Execution::Context& ctx;
45 std::weak_ptr<ExecNode> weak_node;
46 Field& field;
47
48 // NPred is the index of the port in whichever list it was dispatched from in
49 // Executor::connect_controls -- the controls, but also e.g. the curve ports.
50 // control_updated_from_ui indexes the control list, so the two only coincide
51 // when the port happens to be at the same position in both. Recompute it from
52 // the field index, which is unambiguous.
53 static constexpr std::size_t control_index
54 = avnd::control_input_introspection<Node>::field_index_to_index(
55 avnd::field_index<NField>{});
56
57 void operator()(const ossia::value& val)
58 {
59 using control_value_type = std::decay_t<decltype(Field::value)>;
60
61 if(auto node = weak_node.lock())
62 {
63 control_value_type v;
64 node->from_ossia_value(field, val, v, avnd::field_index<NField>{});
65 ctx.executionQueue.enqueue([weak_node = weak_node, v = std::move(v)]() mutable {
66 if(auto n = weak_node.lock())
67 {
68 n->template control_updated_from_ui<control_value_type, control_index>(
69 std::move(v));
70 }
71 });
72 }
73 }
74};
75
79template <typename Node, typename Field, std::size_t NPred, std::size_t NField>
81{
82 using ExecNode = safe_node<Node>;
83 const Execution::Context& ctx;
84 std::weak_ptr<ExecNode> weak_node;
85 int port_index;
86
87 void operator()(const ossia::value& val)
88 {
89 using control_type = avnd::dynamic_port_type<Field>;
90 using control_value_type = std::decay_t<decltype(control_type::value)>;
91
92 if(auto node = weak_node.lock())
93 {
94 // The group's port vector belongs to the execution thread, which resizes
95 // it: it must not be read from here, not even to look up the port. The
96 // conversion only depends on the port type, so it is done against a
97 // local instance; the bounds check happens in the execution thread,
98 // in control_updated_from_ui.
99 control_type witness{};
100 control_value_type v;
101 node->from_ossia_value(witness, val, v, avnd::field_index<NField>{});
102 ctx.executionQueue.enqueue([weak_node = weak_node, port_index = port_index,
103 v = std::move(v)]() mutable {
104 if(auto n = weak_node.lock())
105 {
106 n->template control_updated_from_ui<control_value_type, NPred>(
107 std::move(v), port_index);
108 }
109 });
110 }
111 }
112};
113
114template <typename Node, typename Field>
116{
117 using ExecNode = safe_node<Node>;
119
120 Model& element;
121 const Execution::Context& ctx;
122 const std::shared_ptr<ExecNode>& node_ptr;
123 QObject* parent;
124
125 void invoke_update(Field& param, int k)
126 {
127 avnd::effect_container<Node>& eff = node_ptr->impl;
128 {
129 for(auto state : eff.full_state())
130 {
131 if constexpr(avnd::dynamic_ports_port<Field>)
132 {
133 if_possible(param.ports[k].update(state.effect));
134 }
135 else
136 {
137 if_possible(param.update(state.effect));
138 }
139 }
140 }
141 }
142};
143template <typename Node, typename Field, std::size_t N, std::size_t NField>
145
146template <typename Node, typename Field, std::size_t N, std::size_t NField>
148{
149 using ExecNode = safe_node<Node>;
151
152 void initialize_control(Field& param, Process::ControlInlet* inlet, int k)
153 {
154 // Initialize the control with the current value of the inlet if it is not an optional
155 if constexpr(avnd::dynamic_ports_port<Field>)
156 {
157 using port_type = avnd::dynamic_port_type<Field>;
158 if constexpr(avnd::parameter_port<port_type>)
159 {
160 if constexpr(!requires { param.ports[0].value.reset(); })
161 {
162 this->node_ptr->from_ossia_value(
163 param, inlet->value(), param.ports[k].value, avnd::field_index<NField>{});
164 }
165 }
166 }
167 else
168 {
169 if constexpr(!requires { param.value.reset(); })
170 {
171 this->node_ptr->from_ossia_value(
172 param, inlet->value(), param.value, avnd::field_index<NField>{});
173 }
174 }
175 }
176
177 void update_controller(Field& param, Process::ControlInlet* inlet)
178 {
179 // FIXME proper tag
180 if constexpr(requires { param.update_controller; })
181 {
182 param.update_controller
183 = [inlet = QPointer{inlet}, self = QPointer{&this->element}](auto&& value) {
184 if(!self || !inlet)
185 return;
186
187 // Notify the UI if the object has the power
188 // to actually change the value of the control
189 ossia::qt::run_async(qApp, [self, inlet, val = std::move(value)] {
190 if(!self || !inlet)
191 return;
192
193 // FIXME better to use in_edit queue ?
194 // FIXME not too efficient but which choice do we have ?
195 static const Field field;
196 oscr::to_ossia_value(field, val);
197 inlet->setValue(val);
198 });
199 };
200 }
201 }
202
207 dynamic_ports_component_data<Node>& control_data, Field& param,
208 Process::ControlInlet* inlet, int k)
209 {
210 if constexpr(requires { control_data.m_connectedControls; })
211 {
212 auto it = control_data.m_connectedControls.find(inlet);
213 if(it != control_data.m_connectedControls.end())
214 {
215 if constexpr(!avnd::dynamic_ports_port<Field>)
216 return false;
217 if(it->second.first == k)
218 return false;
219 QObject::disconnect(it->second.second);
220 control_data.m_connectedControls.erase(it);
221 }
222 }
223
224 // Connect to changes
225 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
226 QMetaObject::Connection connection;
227 if constexpr(avnd::dynamic_ports_port<Field>)
228 {
229 connection = QObject::connect(
230 inlet, &Process::ControlInlet::valueChanged, this->parent,
232 }
233 else
234 {
235 connection = QObject::connect(
236 inlet, &Process::ControlInlet::valueChanged, this->parent,
237 con_unvalidated<Node, Field, N, NField>{this->ctx, weak_node, param});
238 }
239
240 if constexpr(requires { control_data.m_connectedControls; })
241 control_data.m_connectedControls.emplace(inlet, std::pair{k, connection});
242 return true;
243 }
244
248 void push_current_value(Field& param, Process::ControlInlet* inlet, int k)
249 {
250 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
251 if constexpr(avnd::dynamic_ports_port<Field>)
252 {
254 inlet->value());
255 }
256 else
257 {
258 con_unvalidated<Node, Field, N, NField>{this->ctx, weak_node, param}(
259 inlet->value());
260 }
261 }
262
263 // Used on initial creation
264 void connect_control_to_ui(
265 dynamic_ports_component_data<Node>& control_data, Field& param,
266 Process::ControlInlet* inlet, int k)
267 {
268 reconnect_control_to_ui(control_data, param, inlet, k);
269 this->update_controller(param, inlet);
270 }
271};
272
273template <typename Node, avnd::soundfile_port Field, std::size_t N, std::size_t NField>
274struct setup_control_for_exec<Node, Field, N, NField>
275 : setup_control_for_exec_base<Node, Field>
276{
277 using ExecNode = safe_node<Node>;
279
280 void initialize_control(Field& param, Process::ControlInlet* inlet, int k)
281 {
282 // FIXME handle dynamic ports correctly
283 // First we can load it directly since execution hasn't started yet
284 if(auto hdl = loadSoundfile(inlet->value(), this->ctx.doc, this->ctx.execState))
285 this->node_ptr->soundfile_loaded(
286 hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
287 }
288
289 void connect_control_to_ui(
291 int k)
292 {
293 // Connect to changes
294 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
295 std::weak_ptr<ossia::execution_state> weak_st = this->ctx.execState;
296 QObject::connect(
297 inlet, &Process::ControlInlet::valueChanged, this->parent,
298 [&ctx = this->ctx, weak_node = std::move(weak_node),
299 weak_st = std::move(weak_st)](const ossia::value& v) {
300 if(auto n = weak_node.lock())
301 if(auto st = weak_st.lock())
302 if(auto file = loadSoundfile(v, ctx.doc, st))
303 {
304 ctx.executionQueue.enqueue([f = std::move(file), weak_node]() mutable {
305 auto n = weak_node.lock();
306 if(!n)
307 return;
308
309 // We store the sound file handle returned in this lambda so that it gets
310 // GC'd in the main thread
311 f = n->soundfile_loaded(
312 f, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
313 });
314 }
315 });
316 }
317};
318
319template <typename Node, avnd::midifile_port Field, std::size_t N, std::size_t NField>
320struct setup_control_for_exec<Node, Field, N, NField>
321 : setup_control_for_exec_base<Node, Field>
322{
323 using ExecNode = safe_node<Node>;
324 using Model = ProcessModel<Node>;
325
326 void initialize_control(Field& param, Process::ControlInlet* inlet, int k)
327 {
328 // FIXME handle dynamic ports correctly
329
330 // First we can load it directly since execution hasn't started yet
331 if(auto hdl = loadMidifile(inlet->value(), this->ctx.doc))
332 this->node_ptr->midifile_loaded(
333 hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
334 }
335
336 void connect_control_to_ui(
337 dynamic_ports_component_data<Node>&, Field& param, Process::ControlInlet* inlet,
338 int k)
339 {
340 // Connect to changes
341 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
342 std::weak_ptr<ossia::execution_state> weak_st = this->ctx.execState;
343 QObject::connect(
344 inlet, &Process::ControlInlet::valueChanged, this->parent,
345 [inlet, &ctx = this->ctx,
346 weak_node = std::move(weak_node)](const ossia::value& v) {
347 if(auto n = weak_node.lock())
348 if(auto file = loadMidifile(v, ctx.doc))
349 {
350 ctx.executionQueue.enqueue([f = std::move(file), weak_node]() mutable {
351 auto n = weak_node.lock();
352 if(!n)
353 return;
354
355 // We store the sound file handle returned in this lambda so that it gets
356 // GC'd in the main thread
357 f = n->midifile_loaded(
358 f, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
359 });
360 }
361 });
362 }
363};
364
365template <typename Node, avnd::raw_file_port Field, std::size_t N, std::size_t NField>
366struct setup_control_for_exec<Node, Field, N, NField>
367 : setup_control_for_exec_base<Node, Field>
368{
369 using ExecNode = safe_node<Node>;
370 using Model = ProcessModel<Node>;
371
372 static constexpr bool has_text = requires { decltype(Field::file)::text; };
373 static constexpr bool has_mmap = requires { decltype(Field::file)::mmap; };
374
375 void initialize_control(Field& param, Process::ControlInlet* inlet, int k)
376 {
377 // FIXME handle dynamic ports correctly
378
379 // First we can load it directly since execution hasn't started yet
380 if(auto hdl = loadRawfile(inlet->value(), this->ctx.doc, has_text, has_mmap))
381 {
382 if constexpr(avnd::port_can_process<Field>)
383 {
384 // FIXME also do it when we get a run-time message from the exec engine,
385 // OSC, etc
386 auto func = executePortPreprocess<Field>(*hdl);
387 this->node_ptr->file_loaded(
388 hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
389 if(func)
390 func(this->node_ptr->impl.effect);
391 }
392 else
393 {
394 this->node_ptr->file_loaded(
395 hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
396 }
397 }
398 }
399
400 void connect_control_to_ui(
401 dynamic_ports_component_data<Node>&, Field& param, Process::ControlInlet* inlet,
402 int k)
403 {
404 // Connect to changes
405 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
406 std::weak_ptr<ossia::execution_state> weak_st = this->ctx.execState;
407 QObject::connect(
408 inlet, &Process::ControlInlet::valueChanged, this->parent,
409 [inlet, &ctx = this->ctx, weak_node = std::move(weak_node)] {
410 if(auto n = weak_node.lock())
411 if(auto file = loadRawfile(inlet->value(), ctx.doc, has_text, has_mmap))
412 {
413 if constexpr(avnd::port_can_process<Field>)
414 {
415 auto func = executePortPreprocess<Field>(*file);
416 ctx.executionQueue.enqueue(
417 [f = std::move(file), weak_node, ff = std::move(func)]() mutable {
418 auto n = weak_node.lock();
419 if(!n)
420 return;
421
422 // We store the sound file handle returned in this lambda so that it gets
423 // GC'd in the main thread
424 f = n->file_loaded(
425 f, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
426 if(ff)
427 ff(n->impl.effect);
428 });
429 }
430 else
431 {
432 ctx.executionQueue.enqueue([f = std::move(file), weak_node]() mutable {
433 auto n = weak_node.lock();
434 if(!n)
435 return;
436
437 // We store the sound file handle returned in this lambda so that it gets
438 // GC'd in the main thread
439 f = n->file_loaded(
440 f, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
441 });
442 }
443 }
444 });
445 }
446};
447
448// folder_port: a path-valued string control. Resolve <LIBRARY>: / <PROJECT>: /
449// document-relative paths through score::locateFilePath before the object reads
450// them -- exactly like the file ports above -- otherwise the object receives the
451// raw library-relative string and cannot find the directory.
452template <typename Node, folder_control_port Field, std::size_t N, std::size_t NField>
453struct setup_control_for_exec<Node, Field, N, NField>
454 : setup_control_for_exec_base<Node, Field>
455{
456 using ExecNode = safe_node<Node>;
457 using Model = ProcessModel<Node>;
458
459 void initialize_control(Field& param, Process::ControlInlet* inlet, int k)
460 {
461 if constexpr(!requires { param.value.reset(); })
462 {
463 this->node_ptr->from_ossia_value(
464 param, resolvePathValue(inlet->value(), this->ctx.doc), param.value,
465 avnd::field_index<NField>{});
466 }
467 }
468
469 void connect_control_to_ui(
470 dynamic_ports_component_data<Node>&, Field& param, Process::ControlInlet* inlet,
471 int k)
472 {
473 std::weak_ptr<ExecNode> weak_node = this->node_ptr;
474 QObject::connect(
475 inlet, &Process::ControlInlet::valueChanged, this->parent,
476 [&ctx = this->ctx, weak_node = std::move(weak_node),
477 field = &param](const ossia::value& val) {
478 // Resolve the path on every change, then run the normal control-update
479 // path (from_ossia_value + control_updated_from_ui) via con_unvalidated.
480 con_unvalidated<Node, Field, N, NField>{ctx, weak_node, *field}(
481 resolvePathValue(val, ctx.doc));
482 });
483 }
484};
485
489template <typename Field>
491 = !ossia_port<avnd::concrete_port_type<Field>>
492 && (!avnd::dynamic_ports_port<Field>
493 || avnd::parameter_port<avnd::concrete_port_type<Field>>);
494
495template <typename Node>
497{
498 using ExecNode = safe_node<Node>;
500
501 Model& element;
502 const Execution::Context& ctx;
503 const std::shared_ptr<ExecNode>& node_ptr;
505 QObject* parent;
506
507 // Main function being invoked, which dispatches to all the actual implementations
508 template <typename Field, std::size_t N, std::size_t NField>
509 constexpr void
510 operator()(Field& param, avnd::predicate_index<N> np, avnd::field_index<NField> nf)
511 {
512 const auto ports = element.avnd_input_idx_to_model_ports(NField);
513
514 if constexpr(avnd::dynamic_ports_port<Field>)
515 {
516 param.ports.resize(ports.size());
517 }
518
519 if constexpr(field_has_ui_controls<Field>)
520 {
521 int k = 0;
522 for(auto p : ports)
523 {
524 if(auto inlet = qobject_cast<Process::ControlInlet*>(p))
525 {
527 element, ctx, node_ptr, parent};
528
529 setup.initialize_control(param, inlet, k);
530
531 setup.invoke_update(param, k);
532
533 setup.connect_control_to_ui(control_data, param, inlet, k);
534 }
535 k++;
536 // Else it's an unhandled value inlet
537 }
538 }
539 }
540};
541
542// Only used when dynamic ports are added
543template <typename Node>
545{
546 using ExecNode = safe_node<Node>;
548
549 Model& element;
550 const Execution::Context& ctx;
551 const std::shared_ptr<ExecNode>& node_ptr;
553 QObject* parent;
554
555 // Main function being invoked, which dispatches to all the actual implementations
556 template <typename Field, std::size_t N, std::size_t NField>
557 constexpr void
558 operator()(Field& param, avnd::predicate_index<N> np, avnd::field_index<NField> nf)
559 {
560 if constexpr(field_has_ui_controls<Field>)
561 {
562 const auto ports = element.avnd_input_idx_to_model_ports(NField);
563 int k = 0;
564 for(auto p : ports)
565 {
566 if(auto inlet = qobject_cast<Process::ControlInlet*>(p))
567 {
569 element, ctx, node_ptr, parent};
570
571 // A port that was just added: connect it and give the exec side its
572 // current value. Ports that were already there keep their connection
573 // and their value.
574 if(setup.reconnect_control_to_ui(control_data, param, inlet, k))
575 setup.push_current_value(param, inlet, k);
576 }
577 k++;
578 // Else it's an unhandled value inlet
579 }
580 }
581 }
582};
583}
Definition Port.hpp:218
Definition score-plugin-avnd/Crousti/ProcessModel.hpp:88
Definition ExecutorPortSetup.hpp:36
TreeNode< DeviceExplorerNode > Node
Definition DeviceNode.hpp:74
Definition Controls.hpp:27
constexpr bool field_has_ui_controls
Definition ExecutorPortSetup.hpp:491
Definition ExecutionContext.hpp:196
Definition PortForward.hpp:23
Definition PortForward.hpp:27
Definition ExecutorPortSetup.hpp:81
Definition ExecutorPortSetup.hpp:42
Definition ExecutorPortSetup.hpp:545
Definition ExecutorPortSetup.hpp:497
Definition ExecutorPortSetup.hpp:15
Definition ExecutorPortSetup.hpp:116
Definition ExecutorPortSetup.hpp:148
bool reconnect_control_to_ui(dynamic_ports_component_data< Node > &control_data, Field &param, Process::ControlInlet *inlet, int k)
Definition ExecutorPortSetup.hpp:206
void push_current_value(Field &param, Process::ControlInlet *inlet, int k)
Definition ExecutorPortSetup.hpp:248