diff --git a/src/plugins/score-plugin-avnd/AvndProcesses/AddressTools.hpp b/src/plugins/score-plugin-avnd/AvndProcesses/AddressTools.hpp index 2da158418a..f3ab04e177 100644 --- a/src/plugins/score-plugin-avnd/AvndProcesses/AddressTools.hpp +++ b/src/plugins/score-plugin-avnd/AvndProcesses/AddressTools.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -74,6 +75,11 @@ struct PatternSelector : halp::lineedit<"Pattern", ""> } ossia::traversal::apply(*p.m_path, p.roots); + std::sort( + p.roots.begin(), p.roots.end(), + [](ossia::net::node_base* lhs, ossia::net::node_base* rhs) { + return doj::alphanum_compare{}(lhs->osc_address(), rhs->osc_address()); + }); devices_dirty = false; } diff --git a/src/plugins/score-plugin-avnd/AvndProcesses/Alphanum.hpp b/src/plugins/score-plugin-avnd/AvndProcesses/Alphanum.hpp new file mode 100644 index 0000000000..4913821046 --- /dev/null +++ b/src/plugins/score-plugin-avnd/AvndProcesses/Alphanum.hpp @@ -0,0 +1,133 @@ +#pragma once +/* + Released under the MIT License - https://opensource.org/licenses/MIT + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include + +namespace doj +{ +struct alphanum_compare +{ + static constexpr bool alphanum_isdigit(const char c) noexcept + { + return c >= '0' && c <= '9'; + } + /** + compare l and r with strcmp() semantics, but using + the "Alphanum Algorithm". This function is designed to read + through the l and r strings only one time, for + maximum performance. It does not allocate memory for + substrings. It can either use the C-library functions isdigit() + and atoi() to honour your locale settings, when recognizing + digit characters when you "#define ALPHANUM_LOCALE=1" or use + it's own digit character handling which only works with ASCII + digit characters, but provides better performance. + + @param l NULL-terminated C-style string + @param r NULL-terminated C-style string + @return negative if lr + */ + static constexpr int impl(std::string_view ll, std::string_view rr) noexcept + { + enum mode_t + { + STRING, + NUMBER + } mode + = STRING; + + const char* l = ll.data(); + const char* r = rr.data(); + while(l != ll.end() && r != rr.end() && *l && *r) + { + if(mode == STRING) + { + char l_char{}, r_char{}; + while((l != ll.end() && r != rr.end()) && (l_char = *l) && (r_char = *r)) + { + // check if this are digit characters + const bool l_digit = alphanum_isdigit(l_char), + r_digit = alphanum_isdigit(r_char); + // if both characters are digits, we continue in NUMBER mode + if(l_digit && r_digit) + { + mode = NUMBER; + break; + } + // if only the left character is a digit, we have a result + if(l_digit) + return -1; + // if only the right character is a digit, we have a result + if(r_digit) + return +1; + // compute the difference of both characters + const int diff = l_char - r_char; + // if they differ we have a result + if(diff != 0) + return diff; + // otherwise process the next characters + ++l; + ++r; + } + } + else // mode==NUMBER + { + // get the left number + unsigned long l_int = 0; + while(l != ll.end() && *l && alphanum_isdigit(*l)) + { + // TODO: this can overflow + l_int = l_int * 10 + *l - '0'; + ++l; + } + + // get the right number + unsigned long r_int = 0; + while(r != rr.end() && *r && alphanum_isdigit(*r)) + { + // TODO: this can overflow + r_int = r_int * 10 + *r - '0'; + ++r; + } + + // if the difference is not equal to zero, we have a comparison result + const long diff = l_int - r_int; + if(diff != 0) + return diff; + + // otherwise we process the next substring in STRING mode + mode = STRING; + } + } + + if(*r) + return -1; + if(*l) + return +1; + return 0; + } + + constexpr bool operator()(std::string_view l, std::string_view r) const noexcept + { + return impl(l.data(), r.data()) < 0; + } +}; +} diff --git a/src/plugins/score-plugin-avnd/AvndProcesses/DeviceRecorder.hpp b/src/plugins/score-plugin-avnd/AvndProcesses/DeviceRecorder.hpp index d361b49a01..c01c144996 100644 --- a/src/plugins/score-plugin-avnd/AvndProcesses/DeviceRecorder.hpp +++ b/src/plugins/score-plugin-avnd/AvndProcesses/DeviceRecorder.hpp @@ -406,6 +406,7 @@ class Reader namespace avnd_tools { + /** Records the input into a CSV. * To record an entire device: can be a pattern expression such as foo:// * @@ -430,6 +431,7 @@ struct DeviceRecorder : PatternObject std::chrono::steady_clock::time_point first_ts; fmt::memory_buffer buf; bool active{}; + bool first_is_timestamp = false; int num_params = 0; void setActive(bool b) @@ -526,10 +528,14 @@ struct DeviceRecorder : PatternObject std::string filename; std::vector roots; std::chrono::steady_clock::time_point first_ts; + + // FIXME boost::multi_array boost::container::flat_map m_map; - boost::container::flat_map> m_vec; + boost::container::flat_map> m_vec_ts; + std::vector> m_vec_no_ts; bool active{}; bool loops{}; + bool first_is_timestamp = false; int num_params{}; void setActive(bool b) @@ -573,12 +579,10 @@ struct DeviceRecorder : PatternObject } auto data = (const char*)f.map(0, f.size()); - m_vec.clear(); m_map.clear(); csv2::Reader<> r; r.parse_view({data, data + f.size()}); - m_vec.reserve(r.rows()); int columns = r.cols(); auto header = r.header(); @@ -589,88 +593,169 @@ struct DeviceRecorder : PatternObject if(auto p = node->get_parameter()) params[node->osc_address()] = p; + std::string v; + v.reserve(128); int i = 0; - for(csv2::Reader<>::Row::CellIterator header_it = ++header.begin(); - header_it != header.end(); ++header_it) + auto header_it = header.begin(); + if(first_is_timestamp) + ++header_it; + for(; header_it != header.end(); ++header_it) { auto addr = *header_it; - std::string a; - addr.read_raw_value(a); - if(auto it = params.find(a); it != params.end()) + v.clear(); + addr.read_raw_value(v); + if(auto it = params.find(v); it != params.end()) { - m_map[i - 1] = it->second; + m_map[i] = it->second; } i++; } - std::string v; - v.reserve(128); - for(const auto& row : r) + m_vec_ts.clear(); + m_vec_no_ts.clear(); + if(first_is_timestamp) { - if(row.length() > 1) + m_vec_ts.reserve(r.rows()); + for(const auto& row : r) { - auto it = row.begin(); - const auto& ts = *it; + parse_row_with_timestamps(columns, row, v); + v.clear(); + } + } + else + { + m_vec_no_ts.reserve(r.rows()); + for(const auto& row : r) + { + parse_row_no_timestamps(columns, row, v); + v.clear(); + } + } + first_ts = std::chrono::steady_clock::now(); + } - ts.read_value(v); - if(auto tstamp = ossia::parse_strict(v)) - { - auto& vec = this->m_vec[*tstamp]; - vec.resize(columns - 1); - int i = 0; + void parse_cell_impl( + const std::string& v, ossia::net::parameter_base& param, ossia::value& out) + { + if(!v.empty()) + { + std::optional res; + if(v.starts_with('"') && v.ends_with('"')) + res = State::parseValue(std::string_view(v).substr(1, v.size() - 2)); + else + res = State::parseValue(v); - for(++it; it != row.end(); ++it) - { - if(auto param = m_map[i]) - { - const auto& cell = *it; - v.clear(); - cell.read_value(v); - - if(!v.empty()) - { - std::optional res; - if(v.starts_with('"') && v.ends_with('"')) - res = State::parseValue(std::string_view(v).substr(1, v.size() - 2)); - else - res = State::parseValue(v); - - if(res) - { - vec[i] = std::move(*res); - if(vec[i].get_type() != param->get_value_type()) - { - ossia::convert(vec[i], param->get_value_type()); - } - } - } - v.clear(); - } - i++; - } + if(res) + { + out = std::move(*res); + if(auto t = param.get_value_type(); out.get_type() != t) + { + ossia::convert(out, t); } } + } + } + + void + parse_cell(const auto& cell, std::string& v, std::vector& vec, int i) + { + if(auto param = m_map[i]) + { + v.clear(); + cell.read_value(v); + parse_cell_impl(v, *param, vec[i]); v.clear(); } - first_ts = std::chrono::steady_clock::now(); } - void read() + void parse_row_no_timestamps(int columns, auto& row, std::string& v) + { + auto& vec = this->m_vec_no_ts.emplace_back(columns); + int i = 0; + + for(auto it = row.begin(); it != row.end(); ++it) + { + parse_cell(*it, v, vec, i); + i++; + } + } + + void parse_row_with_timestamps(int columns, auto& row, std::string& v) { - if(m_vec.empty()) + if(row.length() <= 1) return; - using namespace std::chrono; - auto ts = duration_cast(steady_clock::now() - first_ts).count(); - if(loops) - ts %= m_vec.rbegin()->first + 1; - read(ts); + auto it = row.begin(); + const auto& ts = *it; + + ts.read_value(v); + auto tstamp = ossia::parse_strict(v); + if(!tstamp) + return; + auto& vec = this->m_vec_ts[*tstamp]; + vec.resize(columns - 1); + int i = 0; + + for(++it; it != row.end(); ++it) + { + parse_cell(*it, v, vec, i); + i++; + } + } + + void read() + { + if(first_is_timestamp) + { + if(m_vec_ts.empty()) + return; + + using namespace std::chrono; + auto ts = duration_cast(steady_clock::now() - first_ts).count(); + if(loops) + ts %= m_vec_ts.rbegin()->first + 1; + read_ts(ts); + } + else + { + if(m_vec_no_ts.empty()) + return; + + using namespace std::chrono; + auto ts = duration_cast(steady_clock::now() - first_ts).count(); + if(loops && ts >= std::ssize(m_vec_no_ts)) + ts = 0; + read_no_ts(ts); + } } - void read(int64_t timestamp) + void read_no_ts(int64_t timestamp) { - auto it = m_vec.lower_bound(timestamp); - if(it != m_vec.end()) + if(timestamp < 0) + return; + if(timestamp >= std::ssize(m_vec_no_ts)) + return; + auto it = m_vec_no_ts.begin() + timestamp; + if(it != m_vec_no_ts.end()) + { + int i = 0; + for(auto& v : *it) + { + if(v.valid()) + { + if(auto p = m_map.find(i); p != m_map.end()) + { + p->second->push_value(v); + } + } + i++; + } + } + } + void read_ts(int64_t timestamp) + { + auto it = m_vec_ts.lower_bound(timestamp); + if(it != m_vec_ts.end()) { int i = 0; for(auto& v : it->second) @@ -694,7 +779,8 @@ struct DeviceRecorder : PatternObject struct inputs_t { PatternSelector pattern; - halp::time_chooser<"Interval"> time; + halp::time_chooser<"Interval", halp::range{.min = 0.00001, .max = 5., .init = 0.25}> + time; struct : halp::lineedit<"File pattern", ""> { void update(DeviceRecorder& self) { self.update(); } @@ -704,6 +790,10 @@ struct DeviceRecorder : PatternObject halp__enum("Mode", None, None, Record, Playback, Loop) void update(DeviceRecorder& self) { self.setMode(); } } mode; + struct ts : halp::toggle<"Timestamped", halp::default_on_toggle> + { + halp_meta(description, "Set to true to use the first column as timestamp") + } timestamped; } inputs; struct @@ -716,6 +806,7 @@ struct DeviceRecorder : PatternObject std::shared_ptr player; std::string path; std::vector roots; + bool first_is_timestamp{}; void operator()() { @@ -724,6 +815,8 @@ struct DeviceRecorder : PatternObject swap(recorder->roots, roots); player->filename = recorder->filename; player->roots = recorder->roots; + player->first_is_timestamp = first_is_timestamp; + recorder->first_is_timestamp = first_is_timestamp; recorder->reopen(); player->reopen(); } @@ -734,11 +827,14 @@ struct DeviceRecorder : PatternObject std::shared_ptr recorder; std::shared_ptr player; std::string path; + bool first_is_timestamp{}; void operator()() { using namespace std; swap(recorder->filename, path); player->filename = recorder->filename; + player->first_is_timestamp = first_is_timestamp; + recorder->first_is_timestamp = first_is_timestamp; recorder->reopen(); player->reopen(); } @@ -771,16 +867,20 @@ struct DeviceRecorder : PatternObject }; using worker_message = ossia::variant< - reset_message, reset_path_message, process_message, playback_message, - activate_message>; + std::unique_ptr, reset_path_message, process_message, + playback_message, activate_message>; struct { std::function request; static void work(worker_message&& mess) { - ossia::visit( - [&](M&& msg) { std::forward(msg)(); }, std::move(mess)); + ossia::visit([&](M&& msg) { + if constexpr(requires { *msg; }) + (*std::forward(msg))(); + else + std::forward(msg)(); + }, std::move(mess)); } } worker; @@ -792,7 +892,8 @@ struct DeviceRecorder : PatternObject } void update() { - worker.request(reset_path_message{record_impl, play_impl, inputs.filename}); + worker.request( + reset_path_message{record_impl, play_impl, inputs.filename, inputs.timestamped}); } void operator()(const halp::tick_musical& tk) @@ -813,7 +914,8 @@ struct DeviceRecorder : PatternObject if(!std::exchange(started, true)) { inputs.pattern.reprocess(); - worker.request(reset_message{record_impl, play_impl, inputs.filename, roots}); + worker.request(std::make_unique( + record_impl, play_impl, inputs.filename, roots, inputs.timestamped)); } switch(inputs.mode)