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