From 7c550854352c3bd72080fa00b68c6e935a11ac25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Tue, 30 Jun 2026 15:03:58 -0400 Subject: [PATCH] csound: add csound backend --- cmake/avendish.cmake | 3 + cmake/avendish.csound.cmake | 119 +++++++++++++++ include/avnd/binding/csound/all.hpp | 7 + .../avnd/binding/csound/audio_processor.hpp | 144 ++++++++++++++++++ include/avnd/binding/csound/configure.hpp | 68 +++++++++ include/avnd/binding/csound/helpers.hpp | 57 +++++++ include/avnd/binding/csound/inputs.hpp | 47 ++++++ .../avnd/binding/csound/message_processor.hpp | 93 +++++++++++ include/avnd/binding/csound/outputs.hpp | 38 +++++ include/avnd/binding/csound/prototype.cpp.in | 47 ++++++ include/avnd/binding/csound/typestrings.hpp | 89 +++++++++++ 11 files changed, 712 insertions(+) create mode 100644 cmake/avendish.csound.cmake create mode 100644 include/avnd/binding/csound/all.hpp create mode 100644 include/avnd/binding/csound/audio_processor.hpp create mode 100644 include/avnd/binding/csound/configure.hpp create mode 100644 include/avnd/binding/csound/helpers.hpp create mode 100644 include/avnd/binding/csound/inputs.hpp create mode 100644 include/avnd/binding/csound/message_processor.hpp create mode 100644 include/avnd/binding/csound/outputs.hpp create mode 100644 include/avnd/binding/csound/prototype.cpp.in create mode 100644 include/avnd/binding/csound/typestrings.hpp diff --git a/cmake/avendish.cmake b/cmake/avendish.cmake index 4a7afb35..afa40e17 100644 --- a/cmake/avendish.cmake +++ b/cmake/avendish.cmake @@ -18,6 +18,7 @@ option(AVND_ENABLE_PD "Enable Pure Data backend" ON) option(AVND_ENABLE_MAX "Enable Max/MSP backend" ON) option(AVND_ENABLE_VST3 "Enable VST3 backend" ON) option(AVND_ENABLE_CLAP "Enable CLAP backend" ON) +option(AVND_ENABLE_CSOUND "Enable Csound opcode backend" ON) option(AVND_ENABLE_VINTAGE "Enable VST 2.4 backend" ON) option(AVND_ENABLE_TOUCHDESIGNER "Enable TouchDesigner backend" ON) option(AVND_ENABLE_PYTHON "Enable Python backend" ON) @@ -276,6 +277,7 @@ include(avendish.python) include(avendish.vintage) include(avendish.vst3) include(avendish.clap) +include(avendish.csound) include(avendish.ossia) include(avendish.standalone) include(avendish.touchdesigner) @@ -332,6 +334,7 @@ function(avnd_make_audioplug) _avnd_dispatch_backend(vintage ${ARGV}) _avnd_dispatch_backend(clap ${ARGV}) _avnd_dispatch_backend(vst3 ${ARGV}) + _avnd_dispatch_backend(csound ${ARGV}) _avnd_dispatch_backend(wasm ${ARGV}) avnd_make_example_host(${ARGV}) _avnd_dispatch_backend(gstreamer ${ARGV} PROCESSOR_TYPE AUDIO) diff --git a/cmake/avendish.csound.cmake b/cmake/avendish.csound.cmake new file mode 100644 index 00000000..fb1a747b --- /dev/null +++ b/cmake/avendish.csound.cmake @@ -0,0 +1,119 @@ +find_path(CSOUND_HEADER NAMES csdl.h PATH_SUFFIXES csound) +if(NOT CSOUND_HEADER) + message(STATUS "Csound headers (csdl.h) not found, skipping bindings...") + function(avnd_make_csound) + endfunction() + + return() +endif() + +# Shared PCH, reused by every per-object external (mirrors Avendish_pd_pch). +add_library(Avendish_csound_pch STATIC "${AVND_SOURCE_DIR}/src/dummy.cpp") + +target_include_directories( + Avendish_csound_pch + PRIVATE + ${CSOUND_HEADER} +) + +target_precompile_headers(Avendish_csound_pch + PUBLIC + include/avnd/binding/csound/all.hpp + include/avnd/prefix.hpp +) + +target_link_libraries(Avendish_csound_pch + PUBLIC + DisableExceptions +) + +avnd_common_setup("" "Avendish_csound_pch") + +function(avnd_make_csound) + cmake_parse_arguments(AVND "" "TARGET;MAIN_FILE;MAIN_CLASS;C_NAME" "" ${ARGN}) + + string(MAKE_C_IDENTIFIER "${AVND_MAIN_CLASS}" MAIN_OUT_FILE) + + configure_file( + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/prototype.cpp.in" + "${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_csound.cpp" + @ONLY + NEWLINE_STYLE LF + ) + + if(APPLE) + set_source_files_properties("${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_csound.cpp" PROPERTIES COMPILE_FLAGS -Wno-unreachable-code) + endif() + + set(AVND_FX_TARGET "${AVND_TARGET}_csound") + add_library(${AVND_FX_TARGET} MODULE) + + set_target_properties( + ${AVND_FX_TARGET} + PROPERTIES + OUTPUT_NAME "${AVND_C_NAME}" + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY csound + RUNTIME_OUTPUT_DIRECTORY csound + ARCHIVE_OUTPUT_DIRECTORY csound + ) + + target_sources( + ${AVND_FX_TARGET} + PRIVATE + "${AVND_MAIN_FILE}" + "${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_csound.cpp" + ) + + target_include_directories( + ${AVND_FX_TARGET} + PRIVATE + "${CSOUND_HEADER}" + ) + + target_compile_definitions( + ${AVND_FX_TARGET} + PRIVATE + AVND_CSOUND=1 + ) + + if(NOT MSVC) + target_precompile_headers(${AVND_FX_TARGET} + REUSE_FROM + Avendish_csound_pch + ) + endif() + + target_link_libraries( + ${AVND_FX_TARGET} + PUBLIC + DisableExceptions + Avendish::Avendish_csound + ) + + # Opcode plugins resolve the API through the CSOUND* function table at load + # time, so the engine symbols are deliberately left undefined in the library. + if(APPLE) + target_link_libraries(${AVND_FX_TARGET} PRIVATE -Wl,-undefined,dynamic_lookup) + elseif(UNIX) + target_link_libraries(${AVND_FX_TARGET} PRIVATE -Wl,--allow-shlib-undefined) + endif() + + avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}") +endfunction() + +add_library(Avendish_csound INTERFACE) +target_link_libraries(Avendish_csound INTERFACE Avendish) +target_include_directories(Avendish_csound INTERFACE "${CSOUND_HEADER}") +add_library(Avendish::Avendish_csound ALIAS Avendish_csound) + +target_sources(Avendish PRIVATE + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/all.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/audio_processor.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/configure.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/helpers.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/inputs.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/message_processor.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/outputs.hpp" + "${AVND_SOURCE_DIR}/include/avnd/binding/csound/typestrings.hpp" +) diff --git a/include/avnd/binding/csound/all.hpp b/include/avnd/binding/csound/all.hpp new file mode 100644 index 00000000..8f7aef26 --- /dev/null +++ b/include/avnd/binding/csound/all.hpp @@ -0,0 +1,7 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include +#include diff --git a/include/avnd/binding/csound/audio_processor.hpp b/include/avnd/binding/csound/audio_processor.hpp new file mode 100644 index 00000000..f6bd9483 --- /dev/null +++ b/include/avnd/binding/csound/audio_processor.hpp @@ -0,0 +1,144 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace csound +{ +/** + * Csound opcode wrapping an Avendish audio processor. + * + * Csound allocates `sizeof(audio_opcode)` zero-initialised bytes and fills the + * `args[]` pointers before calling init() (i-time) then perf() (each k-cycle). + * The non-trivial C++ members are placement-new'd over that memory at i-time and + * destroyed through a registered deinit callback (note deactivation / reset). + */ +template +struct audio_opcode +{ + static constexpr int n_args + = (sig::n_out + sig::n_in) > 0 ? (sig::n_out + sig::n_in) : 1; + static constexpr int in_chans = sig::n_audio_in; + static constexpr int out_chans = sig::n_audio_out; + + // ── Csound-managed: opcode header + one pointer per declared argument ── + OPDS h; + MYFLT* args[n_args]; + + // ── Our private state, constructed at i-time over the engine's memory ── + bool initialized; + avnd::effect_container implementation; + avnd::process_adapter processor; + + static int deinit(CSOUND*, void* p) noexcept + { + auto* self = static_cast(p); + if(self->initialized) + { + self->processor.~process_adapter(); + self->implementation.~effect_container(); + self->initialized = false; + } + return OK; + } + + int init(CSOUND* cs) + { + g_csound = cs; + + if(!initialized) + { + ::new(&implementation) avnd::effect_container{}; + ::new(&processor) avnd::process_adapter{}; + cs->RegisterDeinitCallback(cs, this, &audio_opcode::deinit); + initialized = true; + } + + const avnd::process_setup setup{ + .input_channels = in_chans, + .output_channels = out_chans, + .frames_per_buffer = (int)h.insdshead->ksmps, + .rate = (double)cs->GetSr(cs)}; + + processor.allocate_buffers(setup, MYFLT{}); + avnd::prepare(implementation, setup); + + if constexpr(avnd::has_inputs) + avnd::init_controls(implementation); + + implementation.init_channels(in_chans, out_chans); + return OK; + } + + int perf(CSOUND* cs) + { + g_csound = cs; + + const uint32_t ksmps = h.insdshead->ksmps; + const uint32_t offset = h.insdshead->ksmps_offset; + const uint32_t early = h.insdshead->ksmps_no_end; + const uint32_t nsmps = ksmps - early; + + // Pull k-rate / string control inputs into the parameters. + if constexpr(avnd::has_inputs) + inputs::read(implementation, args); + + // Csound passes one MYFLT[ksmps] vector per audio channel; advance every + // pointer by `offset` so we only touch the sample-accurate active region. + std::array in; + for(int c = 0; c < in_chans; ++c) + in[c] = args[sig::audio_in_off + c] + offset; + + std::array out; + for(int c = 0; c < out_chans; ++c) + { + MYFLT* o = args[sig::audio_out_off + c]; + // Sample-accuracy: silence the lead-in and tail samples we won't write. + if(offset > 0) + std::fill_n(o, offset, MYFLT{}); + if(early > 0) + std::fill_n(o + nsmps, early, MYFLT{}); + out[c] = o + offset; + } + + processor.process( + implementation, avnd::span{in.data(), (std::size_t)in_chans}, + avnd::span{out.data(), (std::size_t)out_chans}, + (int)(nsmps - offset)); + + // Push control / value outputs. + if constexpr(avnd::has_outputs) + outputs::write(implementation, args); + + return OK; + } + + static int register_opcode(CSOUND* cs) + { + static constexpr auto name = avnd::get_c_identifier(); + return cs->AppendOpcode( + cs, name.data(), (int)sizeof(audio_opcode), 0, sig::thread, + sig::outypes.data(), sig::intypes.data(), + +[](CSOUND* c, void* p) { return static_cast(p)->init(c); }, + +[](CSOUND* c, void* p) { return static_cast(p)->perf(c); }, + nullptr); + } +}; +} diff --git a/include/avnd/binding/csound/configure.hpp b/include/avnd/binding/csound/configure.hpp new file mode 100644 index 00000000..069bc790 --- /dev/null +++ b/include/avnd/binding/csound/configure.hpp @@ -0,0 +1,68 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include + +#include +#include + +namespace csound +{ +/** + * Routes avnd/halp logging to the Csound console (csound->Message / ErrorMsg) + * through the thread-local engine pointer set in helpers.hpp. + */ +struct logger +{ + template + static void log(T&&... args) + { + std::ostringstream str; + ((str << args), ...); + csound::post(str.str()); + } + + template + static void error(T&&... args) + { + std::ostringstream str; + ((str << args), ...); + csound::bug(str.str()); + } + + template + static void trace(T&&... args) noexcept + { + logger::log(std::forward(args)...); + } + template + static void debug(T&&... args) noexcept + { + logger::log(std::forward(args)...); + } + template + static void info(T&&... args) noexcept + { + logger::log(std::forward(args)...); + } + template + static void warn(T&&... args) noexcept + { + logger::error(std::forward(args)...); + } + template + static void critical(T&&... args) noexcept + { + logger::error(std::forward(args)...); + } +}; + +static_assert(avnd::logger); + +struct config +{ + using logger_type = csound::logger; +}; +} diff --git a/include/avnd/binding/csound/helpers.hpp b/include/avnd/binding/csound/helpers.hpp new file mode 100644 index 00000000..42af2719 --- /dev/null +++ b/include/avnd/binding/csound/helpers.hpp @@ -0,0 +1,57 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include + +// csdl.h is the plugin-opcode C API: it pulls in csoundCore.h (OPDS, MYFLT, +// STRINGDAT, the CSOUND function-pointer table, OK/NOTOK) without requiring a +// link against libcsound. +#include + +// interlocks.h (included by csdl.h) defines very short opcode-flag macros that +// collide with standard-library and avnd template parameters — notably _CR, +// which clobbers 's common-ratio parameter. We never use these flags +// (opcodes are registered with flags = 0), so scrub them immediately. Every +// csound binding header includes THIS header instead of directly, so +// the macros are defined and removed within this single translation step and +// can never leak into a header parsed afterwards. +#undef ZR +#undef ZW +#undef ZB +#undef WI +#undef TR +#undef TW +#undef TB +#undef _CR +#undef _CW +#undef _CB +#undef SK +#undef WR +#undef IR +#undef IW +#undef IB +#undef _QQ + +namespace csound +{ +/** + * Engine pointer for the opcode currently being init'd / performed. + * Set at the top of every init()/perf() so the logger (configure.hpp) can route + * messages to the host. Thread-local because Csound may run instruments on + * several worker threads. + */ +inline thread_local CSOUND* g_csound = nullptr; + +inline void post(std::string_view s) noexcept +{ + if(g_csound) + g_csound->Message(g_csound, "%.*s\n", (int)s.size(), s.data()); +} + +inline void bug(std::string_view s) noexcept +{ + if(g_csound) + g_csound->ErrorMsg(g_csound, "%.*s\n", (int)s.size(), s.data()); +} +} diff --git a/include/avnd/binding/csound/inputs.hpp b/include/avnd/binding/csound/inputs.hpp new file mode 100644 index 00000000..e79d971c --- /dev/null +++ b/include/avnd/binding/csound/inputs.hpp @@ -0,0 +1,47 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include // + macro scrub +#include +#include +#include +#include +#include + +namespace csound +{ +/** + * Reads the opcode's k-rate (and string) input arguments into the processor's + * control parameters, in the canonical declaration order matching sig. + * + * `args` is the full opcode args[] array; control inputs live at + * sig::ctrl_in_off + (predicate index of the parameter). + */ +template +struct inputs +{ + static void read(avnd::effect_container& impl, MYFLT** args) + { + if constexpr(sig::n_ctrl_in > 0) + { + avnd::parameter_input_introspection::for_all_n( + avnd::get_inputs(impl), + [&](F& field, avnd::predicate_index) { + MYFLT* slot = args[sig::ctrl_in_off + Idx]; + if constexpr(avnd::string_parameter) + { + // Csound stores a STRINGDAT* in the MYFLT* argument slot. + auto* str = reinterpret_cast(slot); + if(str && str->data) + avnd::apply_control(field, str->data); + } + else + { + avnd::apply_control(field, (double)*slot); + } + }); + } + } +}; +} diff --git a/include/avnd/binding/csound/message_processor.hpp b/include/avnd/binding/csound/message_processor.hpp new file mode 100644 index 00000000..3a426d01 --- /dev/null +++ b/include/avnd/binding/csound/message_processor.hpp @@ -0,0 +1,93 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace csound +{ +/** + * Csound k-rate opcode wrapping an Avendish processor that has no audio ports + * (a pure control / data object). It reads its control inputs, runs the + * object's `operator()` once per control period when it has a no-argument form, + * and writes its control outputs. Message dispatch is Phase 2. + */ +template +struct message_opcode +{ + static constexpr int n_args + = (sig::n_out + sig::n_in) > 0 ? (sig::n_out + sig::n_in) : 1; + + OPDS h; + MYFLT* args[n_args]; + + bool initialized; + avnd::effect_container implementation; + + static int deinit(CSOUND*, void* p) noexcept + { + auto* self = static_cast(p); + if(self->initialized) + { + self->implementation.~effect_container(); + self->initialized = false; + } + return OK; + } + + int init(CSOUND* cs) + { + g_csound = cs; + + if(!initialized) + { + ::new(&implementation) avnd::effect_container{}; + cs->RegisterDeinitCallback(cs, this, &message_opcode::deinit); + initialized = true; + } + + if constexpr(avnd::has_inputs) + avnd::init_controls(implementation); + return OK; + } + + int perf(CSOUND* cs) + { + g_csound = cs; + + if constexpr(avnd::has_inputs) + inputs::read(implementation, args); + + for(auto state : implementation.full_state()) + { + if constexpr(requires { state.effect(); }) + state.effect(); + } + + if constexpr(avnd::has_outputs) + outputs::write(implementation, args); + return OK; + } + + static int register_opcode(CSOUND* cs) + { + static constexpr auto name = avnd::get_c_identifier(); + return cs->AppendOpcode( + cs, name.data(), (int)sizeof(message_opcode), 0, sig::thread, + sig::outypes.data(), sig::intypes.data(), + +[](CSOUND* c, void* p) { return static_cast(p)->init(c); }, + +[](CSOUND* c, void* p) { return static_cast(p)->perf(c); }, + nullptr); + } +}; +} diff --git a/include/avnd/binding/csound/outputs.hpp b/include/avnd/binding/csound/outputs.hpp new file mode 100644 index 00000000..ff32a618 --- /dev/null +++ b/include/avnd/binding/csound/outputs.hpp @@ -0,0 +1,38 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include // + macro scrub +#include +#include +#include +#include + +namespace csound +{ +/** + * Writes the processor's control / value output ports to the opcode's k-rate + * output arguments, in the canonical order matching sig. + * + * String outputs would require allocating a STRINGDAT through the engine; that + * is deferred (rare for audio plug-ins), so only scalar outputs are emitted. + */ +template +struct outputs +{ + static void write(avnd::effect_container& impl, MYFLT** args) + { + if constexpr(sig::n_ctrl_out > 0) + { + avnd::parameter_output_introspection::for_all_n( + avnd::get_outputs(impl), + [&](F& field, avnd::predicate_index) { + if constexpr(!avnd::string_parameter) + { + *args[sig::ctrl_out_off + Idx] = (MYFLT)field.value; + } + }); + } + } +}; +} diff --git a/include/avnd/binding/csound/prototype.cpp.in b/include/avnd/binding/csound/prototype.cpp.in new file mode 100644 index 00000000..ea1a46a4 --- /dev/null +++ b/include/avnd/binding/csound/prototype.cpp.in @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +// clang-format off +#include <@AVND_MAIN_FILE@> + +#include +#include +#include +#include + +using type = decltype(avnd::configure())::type; +// clang-format on + +// Csound calls these four entry points on every plugin shared object it loads +// from its opcode directory. One Avendish processor is compiled per library, so +// csoundModuleInit registers exactly one opcode. +extern "C" { + +AVND_EXPORTED_SYMBOL int csoundModuleCreate(CSOUND* csound) +{ + (void)csound; + return 0; +} + +AVND_EXPORTED_SYMBOL int csoundModuleInit(CSOUND* csound) +{ + return [](CSOUND* cs) { + if constexpr(avnd::audio_processor) + return csound::audio_opcode::register_opcode(cs); + else + return csound::message_opcode::register_opcode(cs); + }(csound); +} + +AVND_EXPORTED_SYMBOL int csoundModuleDestroy(CSOUND* csound) +{ + (void)csound; + return 0; +} + +// Reports the Csound API version + sizeof(MYFLT) so the engine can reject an +// ABI-incompatible plugin (cf. the LINKAGE macro in csdl.h). +AVND_EXPORTED_SYMBOL int csoundModuleInfo(void) +{ + return (CS_APIVERSION << 16) + (CS_APISUBVER << 8) + (int)sizeof(MYFLT); +} +} diff --git a/include/avnd/binding/csound/typestrings.hpp b/include/avnd/binding/csound/typestrings.hpp new file mode 100644 index 00000000..221ac6dc --- /dev/null +++ b/include/avnd/binding/csound/typestrings.hpp @@ -0,0 +1,89 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include +#include +#include +#include + +#include +#include + +namespace csound +{ +/** + * Compile-time Csound opcode signature for an Avendish processor T. + * + * Csound writes one MYFLT* per declared argument into the opcode struct, in the + * order "outputs first, then inputs". We lay everything out canonically and + * derive BOTH the type-signature strings and the arg-slot offsets from the same + * introspection walk so the two can never drift: + * + * args = [ audio out (M × 'a') ][ ctrl out ('k'/'S') ] ← outypes + * [ audio in (N × 'a') ][ ctrl in ('k'/'S') ] ← intypes + * + * N/M (audio channel counts) come from a `halp::fixed_audio_bus<…,K>` when the + * author fixes them; from a dynamic bus they default to mono (1); when the + * processor has no audio port on a side, that side has 0 audio args. + */ +template +struct sig +{ + // Presence is taken from bus_introspection (which covers both port-based audio + // and the arg-based operator() shapes), the channel count from + // input_channels()/output_channels() — a fixed_audio_bus<…,K> gives K, + // a dynamic bus or an arg-based processor falls back to mono. + static constexpr bool has_audio_in = avnd::bus_introspection::input_busses > 0; + static constexpr bool has_audio_out = avnd::bus_introspection::output_busses > 0; + + static constexpr int n_audio_in = has_audio_in ? avnd::input_channels(1) : 0; + static constexpr int n_audio_out = has_audio_out ? avnd::output_channels(1) : 0; + + static constexpr int n_ctrl_in = avnd::parameter_input_introspection::size; + static constexpr int n_ctrl_out = avnd::parameter_output_introspection::size; + + static constexpr int n_in = n_audio_in + n_ctrl_in; + static constexpr int n_out = n_audio_out + n_ctrl_out; + + // Offsets into the opcode struct's args[] array. + static constexpr int audio_out_off = 0; + static constexpr int ctrl_out_off = n_audio_out; + static constexpr int audio_in_off = n_out; // inputs follow all outputs + static constexpr int ctrl_in_off = n_out + n_audio_in; + + // i-time + perf-time (perf in the kopadr slot, aopadr unused — modern Csound + // convention, cf. /usr/include/csound/plugin.h). + static constexpr int thread = 3; + + static consteval auto build_intypes() + { + std::array r{}; + int k = 0; + for(; k < n_audio_in; ++k) + r[k] = 'a'; + avnd::parameter_input_introspection::for_all([&](F) { + r[k++] = avnd::string_parameter ? 'S' : 'k'; + }); + r[n_in] = '\0'; + return r; + } + + static consteval auto build_outypes() + { + std::array r{}; + int k = 0; + for(; k < n_audio_out; ++k) + r[k] = 'a'; + avnd::parameter_output_introspection::for_all([&](F) { + r[k++] = avnd::string_parameter ? 'S' : 'k'; + }); + r[n_out] = '\0'; + return r; + } + + static constexpr auto intypes = build_intypes(); + static constexpr auto outypes = build_outypes(); +}; +}