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