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