From 9d15a79295168789083875a892ea0db5008b27c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Thu, 31 Oct 2024 23:27:21 -0400 Subject: [PATCH] [core] Add a way to define pure mapping objects --- examples/Advanced/Utilities/MapTool.hpp | 65 ++++++++- include/avnd/binding/ossia/builtin_ports.hpp | 19 +++ include/avnd/binding/ossia/data_node.hpp | 130 ++++++++++++++++++ include/avnd/binding/ossia/node.hpp | 27 ++-- .../binding/ossia/port_run_postprocess.hpp | 1 + include/avnd/concepts/audio_processor.hpp | 16 +++ include/avnd/concepts/generic.hpp | 6 + include/avnd/concepts/processor.hpp | 9 +- .../wrappers/bus_host_process_adapter.hpp | 94 ++++++++----- include/avnd/wrappers/effect_container.hpp | 16 ++- include/halp/controls.sliders.hpp | 17 +++ 11 files changed, 334 insertions(+), 66 deletions(-) diff --git a/examples/Advanced/Utilities/MapTool.hpp b/examples/Advanced/Utilities/MapTool.hpp index 668ccc38..4dffe076 100644 --- a/examples/Advanced/Utilities/MapTool.hpp +++ b/examples/Advanced/Utilities/MapTool.hpp @@ -22,20 +22,23 @@ struct MapTool halp_meta(description, "Map a value to a given domain in various ways") halp_meta(uuid, "ae6e3c9f-40cf-493a-8dc8-d45e75a07213") halp_flag(cv); + halp_flag(stateless); - struct + struct ins { - halp::spinbox_f32<"Min"> in_min; - halp::spinbox_f32<"Max"> in_max; + halp::spinbox_f32<"Min", halp::free_range_min<>> in_min; + halp::spinbox_f32<"Max", halp::free_range_max<>> in_max; + halp::toggle<"Learn min"> min_learn; halp::toggle<"Learn max"> max_learn; + halp::enum_t range_behaviour; halp::knob_f32<"Curve", halp::range{-1., 1., 0.}> curve; halp::toggle<"Invert"> invert; - halp::toggle<"Smooth"> smooth; + halp::knob_f32<"Smooth"> smooth; - halp::spinbox_f32<"Out min"> out_min; - halp::spinbox_f32<"Out max"> out_max; + halp::spinbox_f32<"Out min", halp::free_range_min<>> out_min; + halp::spinbox_f32<"Out max", halp::free_range_max<>> out_max; } inputs; struct @@ -83,7 +86,10 @@ struct MapTool to_01 = wrap(to_01); // - Curve - to_01 = std::pow(to_01, inputs.curve.value + 1); + if(inputs.curve.value >= 0) + to_01 = std::pow(to_01, std::pow(16., inputs.curve.value)); + else + to_01 = 1. - std::pow(1. - to_01, std::pow(16., -inputs.curve.value)); // - Invert if(inputs.invert) @@ -92,5 +98,50 @@ struct MapTool /// 3. Unscale return to_01 * (inputs.out_max - inputs.out_min) + inputs.out_min; } + + struct ui + { + halp_meta(layout, halp::layouts::hbox) + struct + { + halp_meta(layout, halp::layouts::vbox) + halp_meta(background, halp::colors::mid) + struct + { + halp_meta(layout, halp::layouts::hbox) + halp::control<&ins::in_min> p; + halp::control<&ins::min_learn> l; + } min; + struct + { + halp_meta(layout, halp::layouts::hbox) + halp::control<&ins::in_max> p; + halp::control<&ins::max_learn> l; + } max; + } in_range; + + struct + { + halp_meta(layout, halp::layouts::vbox) + halp_meta(background, halp::colors::mid) + + halp::control<&ins::range_behaviour> rb; + halp::control<&ins::invert> i; + struct + { + halp_meta(layout, halp::layouts::hbox) + halp::control<&ins::curve> c; + halp::control<&ins::smooth> s; + } Knobs; + } filter; + + struct + { + halp_meta(layout, halp::layouts::vbox) + halp_meta(background, halp::colors::mid) + halp::control<&ins::out_min> min; + halp::control<&ins::out_max> max; + } out_range; + }; }; } diff --git a/include/avnd/binding/ossia/builtin_ports.hpp b/include/avnd/binding/ossia/builtin_ports.hpp index 250a611e..e1c94f5d 100644 --- a/include/avnd/binding/ossia/builtin_ports.hpp +++ b/include/avnd/binding/ossia/builtin_ports.hpp @@ -24,6 +24,25 @@ struct builtin_arg_audio_ports } }; +template +struct builtin_arg_value_ports +{ + void init(ossia::inlets& inlets, ossia::outlets& outlets) { } +}; + +template +struct builtin_arg_value_ports +{ + ossia::value_inlet in; + ossia::value_outlet out; + + void init(ossia::inlets& inlets, ossia::outlets& outlets) + { + inlets.push_back(&in); + outlets.push_back(&out); + } +}; + template struct builtin_message_value_ports { diff --git a/include/avnd/binding/ossia/data_node.hpp b/include/avnd/binding/ossia/data_node.hpp index 654c137a..53fdeddb 100644 --- a/include/avnd/binding/ossia/data_node.hpp +++ b/include/avnd/binding/ossia/data_node.hpp @@ -7,6 +7,7 @@ namespace oscr // Special case for the easy non-audio case template + requires(!(avnd::tag_cv && avnd::tag_stateless)) class safe_node : public safe_node_base> { public: @@ -46,4 +47,133 @@ class safe_node : public safe_node_base> this->finish_run(); } }; + +template + requires(avnd::tag_cv && avnd::tag_stateless) +class safe_node : public safe_node_base> +{ +public: + using safe_node_base>::safe_node_base; + + bool exec_once{}; + + constexpr bool scan_audio_input_channels() { return false; } + // This function goes from a host-provided tick to what the plugin expects + template + auto invoke_effect(auto&& val, const Tick& t) + { + return this->impl.effect(val); + /* + // clang-format off + if constexpr(std::is_integral_v) + { + static_assert(!avnd::has_tick); + if_possible_r(this->impl.effect(val, t)) + else if_possible_r(this->impl.effect(val)) + else static_assert(std::is_void_v, "impossible to call"); + } + else + { + if constexpr (avnd::has_tick) + { + // Do the process call + if_possible_r(this->impl.effect(val, t)) + else if_possible_r(this->impl.effect(val, t.frames)) + else if_possible_r(this->impl.effect(val, t.frames, t)) + else if_possible_r(this->impl.effect(val)) + else static_assert(std::is_void_v, "impossible to call"); + } + else + { + if_possible_r(this->impl.effect(val, t.frames)) + else if_possible_r(this->impl.effect(val)) + else static_assert(std::is_void_v, "impossible to call"); + } + } + // clang-format on +*/ + } + + template + struct process_value + { + safe_node& self; + const Tick& tick; + ossia::value_port& out; + int ts{}; + void operator()() { } + void operator()(ossia::impulse) { out.write_value(self.invoke_effect(0, tick), 0); } + void operator()(bool v) { out.write_value(self.invoke_effect(v ? 1 : 0, tick), 0); } + void operator()(int v) { out.write_value(self.invoke_effect(v, tick), 0); } + void operator()(float v) { out.write_value(self.invoke_effect(v, tick), 0); } + void operator()(const std::string& v) + { + out.write_value(self.invoke_effect(std::stof(v), tick), 0); + } + template + void operator()(std::array v) + { + std::array res; + for(int i = 0; i < N; i++) + res[i] = self.invoke_effect(v[i], tick); + out.write_value(res, 0); + } + + // FIXME handle recursion + void operator()(const std::vector& v) + { + std::vector res; + res.reserve(v.size()); + + for(std::size_t i = 0; i < v.size(); i++) + res.push_back(self.invoke_effect(ossia::convert(v[i]), tick)); + + out.write_value(std::move(res), 0); + } + void operator()(const ossia::value_map_type& v) + { + ossia::value_map_type res; + for(auto& [k, val] : v) + { + res.emplace_back(k, self.invoke_effect(ossia::convert(val), tick)); + } + out.write_value(std::move(res), 0); + } + }; + + OSSIA_MAXIMUM_INLINE + void run(const ossia::token_request& tk, ossia::exec_state_facade st) noexcept override + { + // FIXME handle splitting execution multiple times per-input for e.g. time_independent mapping objects + if constexpr(avnd::tag_single_exec) + { + if(std::exchange(exec_once, true)) + { + return; + } + } + + auto [start, frames] = st.timings(tk); + + if(!this->prepare_run(tk, st, start, frames)) + { + this->finish_run(); + return; + } + + // Smooth + this->process_smooth(); + + const auto tick + = avnd::get_tick_or_frames(this->impl, tick_info{*this, tk, st, frames}); + process_value proc{*this, tick, *this->arg_value_ports.out}; + + for(const ossia::timed_value& val : this->arg_value_ports.in->get_data()) + { + val.value.apply(proc); + } + + this->finish_run(); + } +}; } diff --git a/include/avnd/binding/ossia/node.hpp b/include/avnd/binding/ossia/node.hpp index 52c9e3c3..52c53713 100644 --- a/include/avnd/binding/ossia/node.hpp +++ b/include/avnd/binding/ossia/node.hpp @@ -120,6 +120,8 @@ class safe_node_base_base : public ossia::nonowning_graph_node [[no_unique_address]] oscr::builtin_arg_audio_ports audio_ports; + [[no_unique_address]] oscr::builtin_arg_value_ports arg_value_ports; + [[no_unique_address]] oscr::builtin_message_value_ports message_ports; [[no_unique_address]] oscr::inlet_storage ossia_inlets; @@ -188,6 +190,7 @@ class safe_node_base : public safe_node_base_base this->sample_rate = sample_rate; this->audio_ports.init(this->m_inlets, this->m_outlets); + this->arg_value_ports.init(this->m_inlets, this->m_outlets); this->message_ports.init(this->m_inlets); this->soundfiles.init(this->impl); @@ -628,27 +631,31 @@ class safe_node_base : public safe_node_base_base // FIXME these concepts are super messy template -concept real_mono_processor = avnd::mono_per_sample_arg_processor - || avnd::mono_per_sample_port_processor - || avnd::monophonic_single_port_audio_effect - || avnd::mono_per_channel_arg_processor - || avnd::mono_per_channel_port_processor; +concept real_mono_processor = !avnd::tag_cv + && (avnd::mono_per_sample_arg_processor + || avnd::mono_per_sample_port_processor + || avnd::monophonic_single_port_audio_effect + || avnd::mono_per_channel_arg_processor + || avnd::mono_per_channel_port_processor); template concept real_good_mono_processor = real_mono_processor || real_mono_processor; template -concept mono_generator = avnd::monophonic_single_port_generator - || avnd::monophonic_single_port_generator; +concept mono_generator = !avnd::tag_cv + && (avnd::monophonic_single_port_generator + || avnd::monophonic_single_port_generator); template concept ossia_compatible_nonaudio_processor - = !(avnd::audio_argument_processor || avnd::audio_port_processor); + = avnd::tag_cv + || !(avnd::audio_argument_processor || avnd::audio_port_processor); template concept ossia_compatible_audio_processor - = avnd::poly_sample_array_input_port_count > 0 - || avnd::poly_sample_array_output_port_count > 0; + = !avnd::tag_cv + && (avnd::poly_sample_array_input_port_count > 0 + || avnd::poly_sample_array_output_port_count > 0); template class safe_node; diff --git a/include/avnd/binding/ossia/port_run_postprocess.hpp b/include/avnd/binding/ossia/port_run_postprocess.hpp index d03cb682..4b2785d3 100644 --- a/include/avnd/binding/ossia/port_run_postprocess.hpp +++ b/include/avnd/binding/ossia/port_run_postprocess.hpp @@ -117,6 +117,7 @@ struct process_after_run assert(N == ctrl.ports.size()); for(int i = 0; i < N; i++) { + // FIXME double-check all the "0", most likely they should be the tick start timestamp instead write_value( ctrl.ports[i], *port[i], ctrl.ports[i].value, 0, avnd::field_index{}); } diff --git a/include/avnd/concepts/audio_processor.hpp b/include/avnd/concepts/audio_processor.hpp index f52eb26c..8b9cf59e 100644 --- a/include/avnd/concepts/audio_processor.hpp +++ b/include/avnd/concepts/audio_processor.hpp @@ -2,6 +2,8 @@ /* SPDX-License-Identifier: GPL-3.0-or-later OR BSL-1.0 OR CC0-1.0 OR CC-PDCC OR 0BSD */ +#include + #include namespace avnd @@ -15,4 +17,18 @@ concept can_bypass = requires(T t) { t.bypass; }; template concept can_prepare = requires(T t) { t.prepare({}); }; +/** + * This tag indicates that a processor is to be treated + * as supporting CV-like control for its I/O, even if it looks like an + * audio processor. + */ +AVND_DEFINE_TAG(cv) + +/** + * This tag indicates that an object is stateless, e.g. + * the same object instance can be reused across multiple invocations + * for separate input values ; the output only depends + * on the inputs and nothing else. + */ +AVND_DEFINE_TAG(stateless) } diff --git a/include/avnd/concepts/generic.hpp b/include/avnd/concepts/generic.hpp index c3174130..6e191b89 100644 --- a/include/avnd/concepts/generic.hpp +++ b/include/avnd/concepts/generic.hpp @@ -16,6 +16,12 @@ namespace avnd action; \ } +#define if_possible_r(action) \ + if constexpr(requires { action; }) \ + { \ + return action; \ + } + #define value_if_possible(A, X, B) \ []() consteval \ { \ diff --git a/include/avnd/concepts/processor.hpp b/include/avnd/concepts/processor.hpp index 90456449..e8444340 100644 --- a/include/avnd/concepts/processor.hpp +++ b/include/avnd/concepts/processor.hpp @@ -4,6 +4,7 @@ #include #include +#include #include // clang-format off @@ -286,15 +287,15 @@ concept bus_port_processor template concept audio_argument_processor = - sample_arg_processor + (sample_arg_processor || channel_arg_processor - || bus_arg_processor + || bus_arg_processor) && !tag_cv ; template concept audio_port_processor = - sample_port_processor + (sample_port_processor || channel_port_processor - || bus_port_processor + || bus_port_processor) && !tag_cv ; } diff --git a/include/avnd/wrappers/bus_host_process_adapter.hpp b/include/avnd/wrappers/bus_host_process_adapter.hpp index 834df071..70934b77 100644 --- a/include/avnd/wrappers/bus_host_process_adapter.hpp +++ b/include/avnd/wrappers/bus_host_process_adapter.hpp @@ -26,24 +26,6 @@ concept audio_arg_output = avnd::sample_arg_processor || avnd::channel_arg_processor || avnd::bus_arg_processor; -template -consteval int total_input_count() -{ - if(audio_arg_input) - return avnd::inputs_type::size + 1; - else - return avnd::inputs_type::size; -} - -template -consteval int total_output_count() -{ - if(audio_arg_output) - return avnd::outputs_type::size + 1; - else - return avnd::outputs_type::size; -} - // !! Important, keep this in sync with safe_node constructor order template void port_visit_dispatcher(auto&& func_inlets, auto&& func_outlets) @@ -53,33 +35,69 @@ void port_visit_dispatcher(auto&& func_inlets, auto&& func_outlets) if constexpr(avnd::mono_per_sample_arg_processor) { - struct + if constexpr(avnd::tag_cv) { - static consteval auto name() { return "Audio In"; } - double sample; - } fake_in; - struct + struct + { + static consteval auto name() { return "Value In"; } + double value; + } fake_in; + struct + { + static consteval auto name() { return "Value Out"; } + double value; + } fake_out; + func_inlets(fake_in, avnd::field_index<0>{}); + func_outlets(fake_out, avnd::field_index<0>{}); + } + else { - static consteval auto name() { return "Audio Out"; } - double sample; - } fake_out; - func_inlets(fake_in, avnd::field_index<0>{}); - func_outlets(fake_out, avnd::field_index<0>{}); + struct + { + static consteval auto name() { return "Audio In"; } + double sample; + } fake_in; + struct + { + static consteval auto name() { return "Audio Out"; } + double sample; + } fake_out; + func_inlets(fake_in, avnd::field_index<0>{}); + func_outlets(fake_out, avnd::field_index<0>{}); + } } else if constexpr(avnd::mono_per_sample_arg_processor) { - struct + if constexpr(avnd::tag_cv) { - static consteval auto name() { return "Audio In"; } - float sample; - } fake_in; - struct + struct + { + static consteval auto name() { return "Value In"; } + float value; + } fake_in; + struct + { + static consteval auto name() { return "Value Out"; } + float value; + } fake_out; + func_inlets(fake_in, avnd::field_index<0>{}); + func_outlets(fake_out, avnd::field_index<0>{}); + } + else { - static consteval auto name() { return "Audio Out"; } - float sample; - } fake_out; - func_inlets(fake_in, avnd::field_index<0>{}); - func_outlets(fake_out, avnd::field_index<0>{}); + struct + { + static consteval auto name() { return "Audio In"; } + float sample; + } fake_in; + struct + { + static consteval auto name() { return "Audio Out"; } + float sample; + } fake_out; + func_inlets(fake_in, avnd::field_index<0>{}); + func_outlets(fake_out, avnd::field_index<0>{}); + } } else if constexpr(avnd::mono_per_channel_arg_processor) { diff --git a/include/avnd/wrappers/effect_container.hpp b/include/avnd/wrappers/effect_container.hpp index d40cfe79..50f30992 100644 --- a/include/avnd/wrappers/effect_container.hpp +++ b/include/avnd/wrappers/effect_container.hpp @@ -198,7 +198,8 @@ struct effect_container }; template - requires(!has_inputs && !has_outputs && !monophonic_audio_processor) + requires( + !has_inputs && !has_outputs && !monophonic_audio_processor && !tag_cv) struct effect_container { using type = T; @@ -234,7 +235,8 @@ struct effect_container }; template - requires(!has_inputs && !has_outputs && monophonic_audio_processor) + requires( + !has_inputs && !has_outputs && monophonic_audio_processor && !tag_cv) struct effect_container { using type = T; @@ -287,7 +289,7 @@ struct effect_container }; template - requires(avnd::inputs_is_type && !avnd::has_outputs) + requires(avnd::inputs_is_type && !avnd::has_outputs && !tag_cv) struct effect_container { using type = T; @@ -328,7 +330,7 @@ struct effect_container }; template - requires avnd::inputs_is_type && avnd::outputs_is_type + requires(avnd::inputs_is_type && avnd::outputs_is_type && !tag_cv) struct effect_container { using type = T; @@ -382,7 +384,7 @@ struct effect_container }; template - requires avnd::inputs_is_type && avnd::outputs_is_value + requires(avnd::inputs_is_type && avnd::outputs_is_value && !tag_cv) struct effect_container { using type = T; @@ -434,7 +436,7 @@ struct effect_container }; template - requires avnd::inputs_is_value && avnd::outputs_is_value + requires(avnd::inputs_is_value && avnd::outputs_is_value && !tag_cv) struct effect_container { using type = T; @@ -488,7 +490,7 @@ struct effect_container } }; template - requires(avnd::inputs_is_value && !avnd::has_outputs) + requires(avnd::inputs_is_value && !avnd::has_outputs && !tag_cv) struct effect_container { using type = T; diff --git a/include/halp/controls.sliders.hpp b/include/halp/controls.sliders.hpp index c1ccb429..dc4ea65e 100644 --- a/include/halp/controls.sliders.hpp +++ b/include/halp/controls.sliders.hpp @@ -23,6 +23,23 @@ inline constexpr auto default_range = range{0., 127., 64.}; template inline constexpr auto default_irange = irange{0, 127, 64}; +// Range still constrained as many ui widgets bail out past some ~2^24 value +template +inline constexpr auto free_range_min = range{ + std::numeric_limits::lowest() / 256., std::numeric_limits::max() / 256., + 0.}; +template +inline constexpr auto free_range_max = range{ + std::numeric_limits::lowest() / 256., std::numeric_limits::max() / 256., + 1.}; + +template +inline constexpr auto free_positive_range_min + = range{0., std::numeric_limits::max() / 256., 0.}; +template +inline constexpr auto free_positive_range_max + = range{0., std::numeric_limits::max() / 256., 1.}; + template struct init_range_t {