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