Loading...
Searching...
No Matches
DeviceRecorder.hpp
1#pragma once
2#include <State/Value.hpp>
3
4#include <ossia/detail/parse_strict.hpp>
5#include <ossia/network/value/detail/value_conversion_impl.hpp>
6
7#include <QDateTime>
8#include <QFile>
9
10#include <AvndProcesses/AddressTools.hpp>
11#include <AvndProcesses/Utils.hpp>
12#include <csv2/csv2.hpp>
13#include <halp/audio.hpp>
14
15namespace avnd_tools
16{
23{
24 halp_meta(name, "CSV")
25 halp_meta(author, "ossia team")
26 halp_meta(category, "Control/Data files")
27 halp_meta(description, "Record the messages of a device at regular interval")
28 halp_meta(c_name, "avnd_device_recorder")
29 halp_meta(uuid, "7161ca22-5684-48f2-bde7-88933500a7fb")
30 halp_meta(manual_url, "https://ossia.io/score-docs/processes/csv-recorder.html#csv-recorder")
31
32 enum Separator
33 {
34 Colon,
35 Semicolon,
36 Pipe,
37 };
38
39 // Threaded worker
41 {
42 explicit recorder_thread(const score::DocumentContext& context)
43 : context{context}
44 {
45 }
46 const score::DocumentContext& context;
47 QFile f{};
48 std::string filename;
49 std::vector<ossia::net::node_base*> roots;
50 std::chrono::steady_clock::time_point first_ts;
51 fmt::memory_buffer buf;
52 bool active{};
53 bool first_is_timestamp = false;
54 char separator{','};
55 int num_params = 0;
56
57 void setSeparator(Separator sep) noexcept
58 {
59 switch(sep)
60 {
61 default:
62 case Separator::Colon:
63 this->separator = ',';
64 break;
65 case Separator::Semicolon:
66 this->separator = ';';
67 break;
68 case Separator::Pipe:
69 this->separator = '|';
70 break;
71 }
72 }
73
74 void setActive(bool b)
75 {
76 active = b;
77 if(!b)
78 f.close();
79 else
80 reopen();
81 }
82
83 void reopen()
84 {
85 f.close();
86
87 f.setFileName(filter_filename(this->filename, context));
88 if(f.fileName().isEmpty())
89 return;
90
91 if(!active)
92 return;
93
94 f.open(QIODevice::WriteOnly);
95 if(!f.isOpen())
96 return;
97
98 f.write("timestamp");
99 num_params = 0;
100 for(auto in : this->roots)
101 {
102 if(auto p = in->get_parameter())
103 {
104 f.write(&separator, 1);
105 f.write(QByteArray::fromStdString(p->get_node().osc_address()));
106
107 num_params++;
108 }
109 }
110 f.write("\n");
111 f.flush();
112
113 first_ts = std::chrono::steady_clock::now();
114 buf.clear();
115 buf.reserve(512);
116 }
117
118 void write()
119 {
120 if(!f.isOpen())
121 return;
122
123 using namespace std::chrono;
124 const auto ts
125 = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
126 write(ts);
127 }
128
129 void write(int64_t timestamp)
130 {
131 f.write(QString::number(timestamp).toUtf8());
132 std::string separator_bufs = "\"\n\r\t";
133 separator_bufs += this->separator;
134 for(auto in : this->roots)
135 {
136 if(auto p = in->get_parameter())
137 {
138 f.write(&separator, 1);
139 buf.clear();
140
141 ossia::apply(ossia::detail::fmt_writer{buf}, p->value());
142
143 std::string_view sv(buf.data(), buf.data() + buf.size());
144 if(sv.find_first_of(separator_bufs) != std::string_view::npos)
145 {
146 // FIXME quote escaping
147 f.write("\"", 1);
148 f.write(buf.data(), buf.size());
149 f.write("\"", 1);
150 }
151 else
152 {
153 f.write(buf.data(), buf.size());
154 }
155 }
156 }
157 f.write("\n");
158 f.flush();
159 }
160 };
161
163 {
164 explicit player_thread(const score::DocumentContext& context)
165 : context{context}
166 {
167 }
168 const score::DocumentContext& context;
169 QFile f{};
170 std::string filename;
171 std::vector<ossia::net::node_base*> roots;
172 std::chrono::steady_clock::time_point first_ts;
173 int64_t nots_index{};
174
175 // FIXME boost::multi_array
176 boost::container::flat_map<int, ossia::net::parameter_base*> m_map;
177 boost::container::flat_map<int64_t, std::vector<ossia::value>> m_vec_ts;
178 std::vector<std::vector<ossia::value>> m_vec_no_ts;
179 bool active{};
180 bool loops{};
181 bool first_is_timestamp = false;
182 char separator{','};
183 int num_params{};
184
185 void setSeparator(Separator sep) noexcept
186 {
187 switch(sep)
188 {
189 default:
190 case Separator::Colon:
191 this->separator = ',';
192 break;
193 case Separator::Semicolon:
194 this->separator = ';';
195 break;
196 case Separator::Pipe:
197 this->separator = '|';
198 break;
199 }
200 }
201
202 void setActive(bool b)
203 {
204 active = b;
205 if(!b)
206 f.close();
207 else
208 reopen();
209 }
210
211 void setLoops(bool b) { loops = b; }
212
213 template <typename CsvReader>
214 void read(std::string_view data)
215 {
216 CsvReader r;
217 r.parse_view(data);
218 int columns = r.cols();
219
220 auto header = r.header();
221
222 boost::container::flat_map<std::string, ossia::net::parameter_base*> params;
223
224 for(auto node : roots)
225 if(auto p = node->get_parameter())
226 params[node->osc_address()] = p;
227
228 std::string v;
229 v.reserve(128);
230 int i = 0;
231 auto header_it = header.begin();
232 if(first_is_timestamp)
233 {
234 // this library increments upon dereference...
235 // just doing ++header_it does not go to the next cell, but
236 // to the next character so we have to do it just when skipping the ,
237
238 *header_it;
239 ++header_it;
240 }
241
242 for(; header_it != header.end(); ++header_it)
243 {
244 auto addr = *header_it;
245 v.clear();
246 addr.read_raw_value(v);
247 if(auto it = params.find(v); it != params.end())
248 {
249 m_map[i] = it->second;
250 }
251 i++;
252 }
253
254 v.clear();
255 m_vec_ts.clear();
256 m_vec_no_ts.clear();
257 if(first_is_timestamp)
258 {
259 // FIXME confirm why we need to re-parse.
260 CsvReader r;
261 r.parse_view(data);
262 m_vec_ts.reserve(r.rows());
263 for(const auto& row : r)
264 {
265 parse_row_with_timestamps(columns, row, v);
266 v.clear();
267 }
268 }
269 else
270 {
271 m_vec_no_ts.reserve(r.rows());
272 for(const auto& row : r)
273 {
274 parse_row_no_timestamps(columns, row, v);
275 v.clear();
276 }
277 }
278 first_ts = std::chrono::steady_clock::now();
279 }
280
281 void reopen()
282 {
283 f.close();
284
285 f.setFileName(filter_filename(this->filename, context));
286 if(f.fileName().isEmpty())
287 return;
288
289 if(!active)
290 return;
291
292 if(!f.open(QIODevice::ReadOnly))
293 return;
294 if(f.size() <= 0)
295 return;
296
297 // FIXME not valid when the OSC device changes
298 // We need to parse the header instead and have a map.
299 num_params = 0;
300 for(auto in : this->roots)
301 {
302 if([[maybe_unused]] auto p = in->get_parameter())
303 {
304 num_params++;
305 }
306 }
307
308 auto data = (const char*)f.map(0, f.size());
309 m_map.clear();
310
311 switch(separator)
312 {
313 default:
314 case ',':
315 read<csv2::Reader<>>({data, data + f.size()});
316 break;
317 case ';':
318 read<csv2::Reader<csv2::delimiter<';'>>>({data, data + f.size()});
319 break;
320 }
321 }
322
323 void parse_cell_impl(
324 const std::string& v, ossia::net::parameter_base& param, ossia::value& out)
325 {
326 if(!v.empty())
327 {
328 std::optional<ossia::value> res;
329 if(v.starts_with('"') && v.ends_with('"'))
330 res = State::parseValue(std::string_view(v).substr(1, v.size() - 2));
331 else
332 res = State::parseValue(v);
333
334 if(res)
335 {
336 out = std::move(*res);
337 if(auto t = param.get_value_type(); out.get_type() != t)
338 {
339 ossia::convert(out, t);
340 }
341 }
342 }
343 }
344
345 void
346 parse_cell(const auto& cell, std::string& v, std::vector<ossia::value>& vec, int i)
347 {
348 if(auto param = m_map[i])
349 {
350 v.clear();
351 cell.read_value(v);
352 parse_cell_impl(v, *param, vec[i]);
353 v.clear();
354 }
355 }
356
357 void parse_row_no_timestamps(int columns, auto& row, std::string& v)
358 {
359 auto& vec = this->m_vec_no_ts.emplace_back(columns);
360 int i = 0;
361
362 for(auto it = row.begin(); it != row.end(); ++it)
363 {
364 parse_cell(*it, v, vec, i);
365 i++;
366 }
367 }
368
369 void parse_row_with_timestamps(int columns, auto& row, std::string& v)
370 {
371 if(row.length() <= 1)
372 return;
373
374 auto it = row.begin();
375 const auto& ts = *it;
376
377 v.clear();
378 ts.read_value(v);
379 auto tstamp = ossia::parse_strict<int64_t>(v);
380 if(!tstamp)
381 return;
382 v.clear();
383 auto& vec = this->m_vec_ts[*tstamp];
384 vec.resize(columns - 1);
385 int i = 0;
386
387 for(++it; it != row.end(); ++it)
388 {
389 parse_cell(*it, v, vec, i);
390 i++;
391 }
392 }
393
394 void read()
395 {
396 if(first_is_timestamp)
397 {
398 if(m_vec_ts.empty())
399 return;
400
401 using namespace std::chrono;
402 auto ts = duration_cast<milliseconds>(steady_clock::now() - first_ts).count();
403 if(loops)
404 ts %= m_vec_ts.rbegin()->first + 1;
405 read_ts(ts);
406 }
407 else
408 {
409 if(m_vec_no_ts.empty())
410 return;
411
412 using namespace std::chrono;
413 if(loops && nots_index >= std::ssize(m_vec_no_ts))
414 nots_index = 0;
415 read_no_ts(nots_index++);
416 }
417 }
418
419 void read_no_ts(int64_t timestamp)
420 {
421 if(timestamp < 0)
422 return;
423 if(timestamp >= std::ssize(m_vec_no_ts))
424 return;
425 auto it = m_vec_no_ts.begin() + timestamp;
426 if(it != m_vec_no_ts.end())
427 {
428 int i = 0;
429 for(auto& v : *it)
430 {
431 if(v.valid())
432 {
433 if(auto p = m_map.find(i); p != m_map.end())
434 {
435 p->second->push_value(v);
436 }
437 }
438 i++;
439 }
440 }
441 }
442
443 void read_ts(int64_t timestamp)
444 {
445 auto it = m_vec_ts.lower_bound(timestamp);
446 if(it != m_vec_ts.end())
447 {
448 if(it != m_vec_ts.begin())
449 --it;
450 int i = 0;
451 for(auto& v : it->second)
452 {
453 if(v.valid())
454 {
455 if(auto p = m_map.find(i); p != m_map.end())
456 {
457 p->second->push_value(v);
458 }
459 }
460 i++;
461 }
462 }
463 else
464 {
465 int i = 0;
466 for(auto& v : m_vec_ts.rbegin()->second)
467 {
468 if(v.valid())
469 {
470 if(auto p = m_map.find(i); p != m_map.end())
471 {
472 p->second->push_value(v);
473 }
474 }
475 i++;
476 }
477 }
478 }
479 };
480
481 // Object definition
482 struct inputs_t
483 {
484 PatternSelector pattern;
485 halp::time_chooser<"Interval", halp::range{.min = 0.00001, .max = 5., .init = 0.25}>
486 time;
487 struct : halp::lineedit<"File pattern", "">
488 {
489 void update(DeviceRecorder& self) { self.update(); }
490 } filename;
491 struct
492 {
493 halp__enum("Mode", None, None, Record, Playback, Loop)
494 void update(DeviceRecorder& self) { self.setMode(); }
495 } mode;
496 struct ts : halp::toggle<"Timestamped", halp::default_on_toggle>
497 {
498 halp_meta(description, "Set to true to use the first column as timestamp")
499 } timestamped;
500
501 halp::enum_t<Separator, "Separator"> separator;
502 } inputs;
503
504 struct
505 {
506 } outputs;
507
509 {
510 std::shared_ptr<recorder_thread> recorder;
511 std::shared_ptr<player_thread> player;
512 std::string path;
513 std::vector<ossia::net::node_base*> roots;
514 bool first_is_timestamp{};
515 Separator separator{};
516
517 void operator()()
518 {
519 using namespace std;
520 swap(recorder->filename, path);
521 swap(recorder->roots, roots);
522 player->filename = recorder->filename;
523 player->roots = recorder->roots;
524 player->first_is_timestamp = first_is_timestamp;
525 player->setSeparator(separator);
526
527 recorder->first_is_timestamp = first_is_timestamp;
528 recorder->setSeparator(separator);
529
530 recorder->reopen();
531 player->reopen();
532 }
533 };
534
536 {
537 std::shared_ptr<recorder_thread> recorder;
538 std::shared_ptr<player_thread> player;
539 std::string path;
540 bool first_is_timestamp{};
541 Separator separator{};
542 void operator()()
543 {
544 using namespace std;
545 swap(recorder->filename, path);
546
547 player->filename = recorder->filename;
548 player->first_is_timestamp = first_is_timestamp;
549 player->setSeparator(separator);
550
551 recorder->first_is_timestamp = first_is_timestamp;
552 recorder->setSeparator(separator);
553
554 recorder->reopen();
555 player->reopen();
556 }
557 };
558
560 {
561 std::shared_ptr<recorder_thread> recorder;
562 void operator()() { recorder->write(); }
563 };
564
566 {
567 std::shared_ptr<player_thread> player;
568 void operator()() { player->read(); }
569 };
570
572 {
573 std::shared_ptr<recorder_thread> recorder;
574 std::shared_ptr<player_thread> player;
575 using mode_type = decltype(DeviceRecorder::inputs_t{}.mode.value);
576 mode_type mode{};
577 Separator separator{Separator::Colon};
578 void operator()()
579 {
580 recorder->setSeparator(separator);
581 recorder->setActive(mode == mode_type::Record);
582 player->setSeparator(separator);
583 player->setActive(mode == mode_type::Playback || mode == mode_type::Loop);
584 player->setLoops(mode == mode_type::Loop);
585 }
586 };
587
588 using worker_message = ossia::variant<
589 std::unique_ptr<reset_message>, reset_path_message, process_message,
591
592 struct
593 {
594 std::function<void(worker_message)> request;
595 static void work(worker_message&& mess)
596 {
597 ossia::visit([&]<typename M>(M&& msg) {
598 if constexpr(requires { *msg; })
599 (*std::forward<M>(msg))();
600 else
601 std::forward<M>(msg)();
602 }, std::move(mess));
603 }
604 } worker;
605
606 using tick = halp::tick_musical;
607
608 void setMode()
609 {
610 if(!record_impl)
611 return;
612 worker.request(
613 activate_message{
614 record_impl, play_impl, inputs.mode.value, inputs.separator.value});
615 }
616
617 void prepare()
618 {
619 SCORE_ASSERT(ossia_document_context);
620 record_impl = std::make_shared<recorder_thread>(*ossia_document_context);
621 play_impl = std::make_shared<player_thread>(*ossia_document_context);
622 setMode();
623 update();
624 }
625
626 void update()
627 {
628 if(!record_impl)
629 return;
630 worker.request(
631 reset_path_message{
632 record_impl, play_impl, inputs.filename, inputs.timestamped,
633 inputs.separator});
634 }
635
636 void operator()(const halp::tick_musical& tk)
637 {
638 int64_t elapsed_ns = 0.;
639 if(!first_message_sent_pos)
640 first_message_sent_pos = tk.position_in_nanoseconds;
641 if(last_message_sent_pos)
642 elapsed_ns = tk.position_in_nanoseconds - *last_message_sent_pos;
643
644 if(elapsed_ns > 0 && elapsed_ns < inputs.time.value * 1e9)
645 return;
646 last_message_sent_pos = tk.position_in_nanoseconds;
647
648 if(!m_path)
649 return;
650
651 if(!std::exchange(started, true))
652 {
653 inputs.pattern.reprocess();
654 worker.request(
655 std::unique_ptr<reset_message>(new reset_message{
656 record_impl, play_impl, inputs.filename, roots, inputs.timestamped,
657 inputs.separator}));
658 }
659
660 switch(inputs.mode)
661 {
662 case decltype(inputs.mode)::None:
663 break;
664 case decltype(inputs.mode)::Record:
665 worker.request(process_message{record_impl});
666 break;
667 case decltype(inputs.mode)::Playback:
668 case decltype(inputs.mode)::Loop:
669 worker.request(playback_message{play_impl});
670 break;
671 }
672 }
673
674 const score::DocumentContext* ossia_document_context{};
675 std::shared_ptr<recorder_thread> record_impl;
676 std::shared_ptr<player_thread> play_impl;
677 std::optional<int64_t> first_message_sent_pos;
678 std::optional<int64_t> last_message_sent_pos;
679 bool started{};
680};
681}
STL namespace.
Definition DeviceRecorder.hpp:572
Definition DeviceRecorder.hpp:497
Definition DeviceRecorder.hpp:483
Definition DeviceRecorder.hpp:566
Definition DeviceRecorder.hpp:163
Definition DeviceRecorder.hpp:560
Definition DeviceRecorder.hpp:41
Definition DeviceRecorder.hpp:509
Definition DeviceRecorder.hpp:536
Definition DeviceRecorder.hpp:23
Definition AddressTools.hpp:21
Definition AddressTools.hpp:28
Definition DocumentContext.hpp:18