diff --git a/maxmsp/help/mc.zerr.combinator~.maxhelp b/maxmsp/help/mc.zerr.combinator~.maxhelp index 8beabf7..acdbdb5 100644 --- a/maxmsp/help/mc.zerr.combinator~.maxhelp +++ b/maxmsp/help/mc.zerr.combinator~.maxhelp @@ -307,8 +307,20 @@ "maxclass" : "comment", "numinlets" : 1, "numoutlets" : 0, - "patching_rect" : [ 40.0, 269.0, 705.0, 22.0 ], - "text" : "\"max\": the output is maximum number of envelopes" + "patching_rect" : [ 40.0, 272.0, 705.0, 20.0 ], + "text" : "\"max\": the largest envelope value across the sets. Used when no mode argument is given" + } + + } +, { + "box" : { + "fontsize" : 14.0, + "id" : "obj-54", + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 40.0, 292.0, 705.0, 20.0 ], + "text" : "\"add\": the sum of the envelopes across the sets" } } @@ -319,8 +331,8 @@ "maxclass" : "comment", "numinlets" : 1, "numoutlets" : 0, - "patching_rect" : [ 40.0, 242.0, 705.0, 22.0 ], - "text" : "\"root\": the output is the square root of the multiplied envelopes" + "patching_rect" : [ 40.0, 252.0, 705.0, 20.0 ], + "text" : "\"root\": the geometric mean - the Nth root of the N envelopes multiplied together. Equals a square root for two sets" } } @@ -397,7 +409,7 @@ "maxclass" : "comment", "numinlets" : 1, "numoutlets" : 0, - "patching_rect" : [ 40.0, 216.400002181529999, 705.100000768899918, 22.0 ], + "patching_rect" : [ 40.0, 232.0, 705.100000768899918, 20.0 ], "text" : "Available combination modes:" } @@ -418,12 +430,12 @@ "box" : { "fontsize" : 14.0, "id" : "obj-25", - "linecount" : 3, + "linecount" : 4, "maxclass" : "comment", "numinlets" : 1, "numoutlets" : 0, - "patching_rect" : [ 40.0, 161.400002181529999, 705.100000768899918, 53.0 ], - "text" : "The \\[zerr_combinator~\\] is for combining multiple sets of envelopes in different ways. First argument is the number of sets. The second argument is the number of envelopes per set. The third argument is the combination mode. Available combination modes: - root: - max:" + "patching_rect" : [ 40.0, 161.400002181529999, 705.100000768899918, 70.0 ], + "text" : "The \\[mc.zerr.combinator~\\] combines multiple sets of envelopes in different ways. First argument: the number of envelope sets - the object gets one multichannel inlet per set. The number of envelopes per set comes from the channel count of the incoming multichannel signals and every inlet must carry the same count. Second argument: the combination mode - optional and defaults to \"max\"." } } diff --git a/maxmsp/source/projects/mc.zerr.combinator_tilde/mc.zerr.combinator_tilde.cpp b/maxmsp/source/projects/mc.zerr.combinator_tilde/mc.zerr.combinator_tilde.cpp index fe49cb2..dab26bd 100644 --- a/maxmsp/source/projects/mc.zerr.combinator_tilde/mc.zerr.combinator_tilde.cpp +++ b/maxmsp/source/projects/mc.zerr.combinator_tilde/mc.zerr.combinator_tilde.cpp @@ -15,6 +15,12 @@ #include "./zerr_combinator.hpp" +#include + +/// Combination mode used when the object is created without a mode argument. +/// "max" is what this object has always computed, so existing patches are unaffected. +#define ZERR_COMBINATOR_DEFAULT_MODE "max" + //------------------------------------------------------------------------------ // Type Definitions //------------------------------------------------------------------------------ @@ -24,9 +30,10 @@ * @brief Main data structure for the mc.zerr.combinator~ object */ typedef struct _zerr_combinator { - t_pxobject x_obj; ///< DSP object header (must be first) - long channel_count; ///< Channel count of multichannel signal - long input_count; ///< Number of input channel groups + t_pxobject x_obj; ///< DSP object header (must be first) + long channel_count; ///< Envelopes per set, derived from the inlets' channel count + long input_count; ///< Number of envelope sets, one multichannel inlet each + t_symbol* mode_sym; ///< Combination mode in use, for bang/attribute reporting bool channel_muted; ///< Mute flag for unequal channel counts ZerrCombinator* zc; ///< Pointer to the zerr_combinator implementation } t_zerr_combinator; @@ -41,13 +48,12 @@ void zerr_combinator_free(t_zerr_combinator* x); void zerr_combinator_assist(t_zerr_combinator* x, void* b, long m, long a, char* s); -void zerr_combinator_dsp64(t_zerr_combinator* x, t_object* dsp64, short* count, - double samplerate, long maxvectorsize, long flags); +void zerr_combinator_dsp64(t_zerr_combinator* x, t_object* dsp64, short* count, double samplerate, + long maxvectorsize, long flags); -void zerr_combinator_perform64(t_zerr_combinator* x, t_object* dsp64, - double** ins, long numins, double** outs, - long numouts, long sampleframes, long flags, - void* userparam); +void zerr_combinator_perform64(t_zerr_combinator* x, t_object* dsp64, double** ins, long numins, + double** outs, long numouts, long sampleframes, long flags, + void* userparam); void zerr_combinator_bang(t_zerr_combinator* x); @@ -66,25 +72,21 @@ C74_EXPORT void ext_main(void* r) { t_class* c; - c = class_new("mc.zerr.combinator~", - (method)zerr_combinator_new, - (method)zerr_combinator_free, - sizeof(t_zerr_combinator), - 0L, - A_GIMME, - 0); + c = class_new("mc.zerr.combinator~", (method)zerr_combinator_new, (method)zerr_combinator_free, + sizeof(t_zerr_combinator), 0L, A_GIMME, 0); // Register methods class_addmethod(c, (method)zerr_combinator_dsp64, "dsp64", A_CANT, 0); class_addmethod(c, (method)zerr_combinator_assist, "assist", A_CANT, 0); - class_addmethod(c, (method)zerr_combinator_multichanneloutputs, "multichanneloutputs", A_CANT, 0); + class_addmethod(c, (method)zerr_combinator_multichanneloutputs, "multichanneloutputs", A_CANT, + 0); class_addmethod(c, (method)zerr_combinator_inputchanged, "inputchanged", A_CANT, 0); class_addmethod(c, (method)zerr_combinator_bang, "bang", 0); - // Attributes - CLASS_ATTR_LONG(c, "chans", 0, t_zerr_combinator, channel_count); - CLASS_ATTR_LABEL(c, "chans", 0, "Output Channels"); - CLASS_ATTR_FILTER_CLIP(c, "chans", 1, MC_MAX_CHANS); + // Attributes. "chans" is derived from the inlets' channel count in dsp64, so it is + // readable but not settable -- writing it would only desync it from the real signal. + CLASS_ATTR_LONG(c, "chans", ATTR_SET_OPAQUE_USER, t_zerr_combinator, channel_count); + CLASS_ATTR_LABEL(c, "chans", 0, "Output Channels (read-only)"); CLASS_ATTR_BASIC(c, "chans", 0); // Initialize DSP and register the class @@ -104,38 +106,64 @@ void* zerr_combinator_new(t_symbol* s, long argc, t_atom* argv) if (x) { // Initialize default values - x->zc = NULL; - x->channel_count = 1; // Default: 1 channel output - x->input_count = 2; // Default: combine two sets of envelopes + x->zc = NULL; + x->channel_count = 1; // Until dsp64 reports the inlets' channel count + x->input_count = 2; // Default: combine two sets of envelopes + x->mode_sym = gensym(ZERR_COMBINATOR_DEFAULT_MODE); x->channel_muted = FALSE; + // Positional arguments end at the first @attribute + long offset = attr_args_offset(argc, argv); + // Validate arguments - if (argc < 2) { - object_error((t_object*)x, "requires 2 arguments"); + if (offset < 1) { + object_error((t_object*)x, + "requires at least 1 argument: [mode]"); + return NULL; + } + + // Process argument 1: number of envelope sets + if (atom_gettype(argv) != A_LONG) { + object_error((t_object*)x, + "first argument must be an integer (number of envelope sets)"); + return NULL; + } + x->input_count = atom_getlong(argv); + if (x->input_count < 1) { + object_error((t_object*)x, "number of envelope sets must be at least 1"); return NULL; } - // Process argument 1: number of inputs - if (atom_gettype(argv) == A_LONG) { - x->input_count = atom_getlong(argv); - if (x->input_count < 1) { - object_error((t_object*)x, "input_count must be at least 1"); + // Process argument 2: combine mode. Optional, defaults to "max". + // A bad mode name is recoverable, so report it and fall back rather than + // leaving the user with a dead object box. + const char* mode = ZERR_COMBINATOR_DEFAULT_MODE; + if (offset > 1) { + if (atom_gettype(argv + 1) != A_SYM) { + object_error((t_object*)x, "second argument must be a symbol (combination mode)"); return NULL; } - } else { - object_error((t_object*)x, "first argument must be an integer"); - return NULL; + const char* requested = atom_getsym(argv + 1)->s_name; + if (ZerrCombinator::isValidMode(requested)) { + mode = requested; + } + else { + object_error((t_object*)x, + "unknown combination mode '%s'; expected %s. Using '%s'.", requested, + ZerrCombinator::modeNames(), mode); + } } + x->mode_sym = gensym(mode); - // Process argument 2: combine mode - if (atom_gettype(argv + 1) != A_SYM) { - object_error((t_object*)x, "second argument must be a symbol"); + // Build the wrapper. The core module itself is built in dsp64, once the + // channel count, sample rate and block size are known. + try { + x->zc = new ZerrCombinator((int)x->input_count, mode); + } + catch (const std::exception& e) { + object_error((t_object*)x, "failed to create combinator: %s", e.what()); return NULL; } - t_symbol* arg2 = atom_getsym(argv + 1); - const char* mode = arg2->s_name; - - // TODO: Initialize the ZerrCombinator based on the mode // Process attributes attr_args_process(x, argc, argv); @@ -171,15 +199,22 @@ void zerr_combinator_free(t_zerr_combinator* x) void zerr_combinator_assist(t_zerr_combinator* x, void* b, long m, long a, char* s) { if (m == ASSIST_INLET) { - snprintf(s, 128, "Signal Input %ld: Envelope Group", a + 1); - } else if (m == ASSIST_OUTLET) { + // The number of envelopes per set is the inlet's own channel count, and every + // inlet must agree -- it is not an object argument. + snprintf( + s, 128, + "(multichannel signal) Envelope Set %ld; all sets must have the same channel count", + a + 1); + } + else if (m == ASSIST_OUTLET) { strcpy(s, "(multichannel signal) Combined Envelopes"); } } void zerr_combinator_bang(t_zerr_combinator* x) { - object_post((t_object*)x, "combination mode: %s \n channel count = %ld", "everything", x->channel_count); + object_post((t_object*)x, "combination mode: %s, envelope sets: %ld, envelopes per set: %ld", + x->mode_sym ? x->mode_sym->s_name : "unknown", x->input_count, x->channel_count); } //------------------------------------------------------------------------------ @@ -204,38 +239,51 @@ long zerr_combinator_inputchanged(t_zerr_combinator* x, long index, long count) // DSP Methods //------------------------------------------------------------------------------ -void zerr_combinator_dsp64(t_zerr_combinator* x, t_object* dsp64, short* count, - double samplerate, long maxvectorsize, long flags) +void zerr_combinator_dsp64(t_zerr_combinator* x, t_object* dsp64, short* count, double samplerate, + long maxvectorsize, long flags) { - // Get actual channel counts for diagnostics - long total_input_channels = 0; - for (int i = 0; i < x->input_count; i++) { - long inlet_channels = (long)object_method(dsp64, gensym("getnuminputchannels"), x, i); - total_input_channels += inlet_channels; - } - - // Verify all inlets have the same channel count + // Verify all inlets have the same channel count. The perform routine is added even + // when they don't, so that the muted path actually silences the outlet instead of + // leaving whatever the previous DSP chain wrote there. long inletchannelcount = (long)object_method(dsp64, gensym("getnuminputchannels"), x, 0); for (int i = 1; i < x->input_count; ++i) { if (inletchannelcount != (long)object_method(dsp64, gensym("getnuminputchannels"), x, i)) { + object_error((t_object*)x, + "all envelope sets must have the same channel count; output muted"); x->channel_muted = TRUE; + dsp_add64(dsp64, (t_object*)x, (t_perfroutine64)zerr_combinator_perform64, 0, NULL); return; } } - // All channel counts are equal, enable audio processing - x->channel_muted = FALSE; + x->channel_count = inletchannelcount; + + // Build the core module for this signal shape. Take the sample rate and block size + // from the dsp64 arguments rather than sys_getsr()/sys_getblksize(), which differ + // inside poly~. + zerr::SystemConfigs cfgs; + cfgs.sample_rate = (size_t)samplerate; + cfgs.block_size = (size_t)maxvectorsize; + + if (!x->zc || !x->zc->prepare(cfgs, (int)x->channel_count)) { + object_error((t_object*)x, "failed to configure combinator for %ld channels; output muted", + inletchannelcount); + x->channel_muted = TRUE; + } + else { + x->channel_muted = FALSE; + } + dsp_add64(dsp64, (t_object*)x, (t_perfroutine64)zerr_combinator_perform64, 0, NULL); } -void zerr_combinator_perform64(t_zerr_combinator* x, t_object* dsp64, - double** ins, long numins, double** outs, - long numouts, long sampleframes, long flags, - void* userparam) +void zerr_combinator_perform64(t_zerr_combinator* x, t_object* dsp64, double** ins, long numins, + double** outs, long numouts, long sampleframes, long flags, + void* userparam) { // Zero out the output if the input signals don't have equal channel counts - if (x->channel_muted) { + if (x->channel_muted || !x->zc) { for (long i = 0; i < numouts; i++) { if (outs[i]) { memset(outs[i], 0, sampleframes * sizeof(double)); @@ -244,25 +292,5 @@ void zerr_combinator_perform64(t_zerr_combinator* x, t_object* dsp64, return; } - long num_groups = x->input_count; - - // Process each output channel - for (long channel = 0; channel < numouts; channel++) { - // Process each sample for this channel - for (long sample = 0; sample < sampleframes; sample++) { - double max_val = 0.0; - - // Find maximum value across all input groups for this channel - for (long group = 0; group < num_groups; group++) { - long input_idx = group * numouts + channel; - if (input_idx < numins && ins[input_idx]) { - double val = ins[input_idx][sample]; - max_val = (val > max_val) ? val : max_val; - } - } - - // Output the maximum value - outs[channel][sample] = max_val; - } - } + x->zc->perform(ins, numins, outs, numouts, sampleframes); } \ No newline at end of file diff --git a/maxmsp/source/projects/mc.zerr.combinator_tilde/zerr_combinator.hpp b/maxmsp/source/projects/mc.zerr.combinator_tilde/zerr_combinator.hpp index 2ba39aa..1a9854d 100644 --- a/maxmsp/source/projects/mc.zerr.combinator_tilde/zerr_combinator.hpp +++ b/maxmsp/source/projects/mc.zerr.combinator_tilde/zerr_combinator.hpp @@ -9,11 +9,13 @@ */ #pragma once +#include +#include +#include #include #include #include -#include -#include +#include #include "./envelopecombinator.h" #include "types.h" @@ -21,58 +23,104 @@ /** * @class ZerrCombinator * @brief Manages the combination of multiple envelope signals across channel groups - * + * * This class handles the processing of multiple multichannel envelope signals, - * combining them according to the specified mode. It provides a clean interface + * combining them according to the specified mode. It provides a clean interface * between Max/MSP's audio system and the underlying envelope combination algorithms. + * + * Construction is split in two because Max hands over the object's shape in stages: + * the number of envelope sets is known when the object is created, but the number of + * envelopes per set (the multichannel channel count), the sample rate and the block + * size only arrive in dsp64. The core zerr::EnvelopeCombinator therefore cannot be + * built by the constructor -- prepare() builds it. */ class ZerrCombinator { - public: + public: /** * @brief Creates a new ZerrCombinator instance - * @param sys_config System configuration containing sample rate and block size settings - * @param inputCount List of audio features to extract from the input signal - * @param mode + * @param inputCount Number of envelope sets to combine (one multichannel inlet each) + * @param mode Combination mode: "add", "root" or "max" + * @throws std::invalid_argument if inputCount < 1 or mode is not a known mode */ - ZerrCombinator(const zerr::SystemConfigs& sys_config, int inputCount, std::string mode) - : systemConfigs { sys_config } - , inputBuffer(inputCount, std::vector(sys_config.block_size, 0.0)) - // , combinator { std::make_unique(sys_config, spkrCfgFile, selectionMode) } + ZerrCombinator(int inputCount, std::string mode) + : inputCount{inputCount}, combMode{std::move(mode)} { + if (inputCount < 1) { + throw std::invalid_argument("ZerrCombinator: inputCount must be at least 1"); + } + if (!isValidMode(combMode)) { + throw std::invalid_argument("ZerrCombinator: unknown combination mode: " + combMode); + } } - // Disable copying but allow moving - ZerrCombinator(const ZerrCombinator&) = delete; + // Neither copyable nor movable: holds an atomic and is owned by a raw pointer in the + // Max object struct, so neither operation is needed. + ZerrCombinator(const ZerrCombinator&) = delete; ZerrCombinator& operator=(const ZerrCombinator&) = delete; - ZerrCombinator(ZerrCombinator&&) = default; - ZerrCombinator& operator=(ZerrCombinator&&) = default; + ZerrCombinator(ZerrCombinator&&) = delete; + ZerrCombinator& operator=(ZerrCombinator&&) = delete; /** - * @brief Initializes all internal components and prepares the object for processing - * @return true if initialization was successful, false otherwise - * @throws std::runtime_error if memory allocation fails + * @brief Checks a combination mode name without constructing anything + * @param mode Mode name to check + * @return true if the core supports this mode + * + * Duplicates the mode names in zerr::EnvelopeCombinator::initialize() so that a typo in + * an object argument can be reported at creation time rather than at DSP start. Replace + * this with a core-side parser once zerr::EnvelopeCombinator exposes one. */ - bool initialize() + [[nodiscard]] static bool isValidMode(const std::string& mode) noexcept { - // Initialize the Envelope Generator module - if (!combinator->initialize()) { - return false; - }; + return mode == "add" || mode == "root" || mode == "max"; + } + + /** + * @brief Human-readable list of the supported modes, for error messages + */ + [[nodiscard]] static const char* modeNames() noexcept { return "add, root or max"; } - // assgin Max/MSP print method the the logger function - // Because we want to see logs in the Max/MSP log window - // auto printFunc = [](const std::string& msg) { - // post(msg.c_str()); - // }; - // combinator->setPrinter(printFunc); + /** + * @brief Builds (or rebuilds) the core module for a given signal shape + * @param cfg Sample rate and block size, as reported by dsp64 + * @param numChannel Number of envelopes per set, i.e. the inlets' channel count + * @return true if the combinator is ready to process + * + * Called from dsp64 on the main thread, where allocation is safe. Idempotent: returns + * immediately when the shape has not changed, so toggling DSP does not reallocate. + */ + bool prepare(const zerr::SystemConfigs& cfg, int numChannel) + { + if (numChannel < 1 || cfg.block_size == 0) { + return false; + } - // outputCount = combinator->getNumSpeakers(); - // post("ZerrCombinator::initialize outputCount is %d", outputCount); + if (ready.load(std::memory_order_acquire) && numChannel == this->numChannel && + cfg.block_size == systemConfigs.block_size && + cfg.sample_rate == systemConfigs.sample_rate) { + return true; + } - inputBuffer.resize(inputCount, zerr::Samples(systemConfigs.block_size, 0.0f)); - outputBuffer.resize(outputCount, zerr::Samples(systemConfigs.block_size, 0.0f)); + auto next = + std::make_unique(inputCount, numChannel, cfg, combMode); + if (!next->initialize()) { + return false; + } - return true; + zerr::Blocks nextIn(next->numInlet, zerr::Samples(cfg.block_size, 0.0)); + zerr::Blocks nextOut(next->numOutlet, zerr::Samples(cfg.block_size, 0.0)); + + // MSP rebuilds the DSP chain around dsp64, so perform() is not running for this + // object while we swap. The flag covers the states MSP does not: never prepared, + // and prepare() having failed. + ready.store(false, std::memory_order_release); + combinator.swap(next); + inputBuffer.swap(nextIn); + outputBuffer.swap(nextOut); + systemConfigs = cfg; + this->numChannel = numChannel; + ready.store(true, std::memory_order_release); + + return true; // the previous combinator dies with `next` here } /** @@ -82,54 +130,89 @@ class ZerrCombinator { * @param outs Array of pointers to output audio buffers * @param numouts Number of output channels * @param sampleframes Number of samples to process + * + * noexcept by contract: this runs on the audio thread, inside Max's C call stack, where + * an escaping exception would terminate the host. Anything unexpected zeroes the output. */ - void perform(double** ins, long numins, double** outs, long numouts, long sampleframes) + void perform(double** ins, long numins, double** outs, long numouts, long sampleframes) noexcept { - if (!ins || !outs || numins < inputCount || numouts < outputCount) { - throw std::invalid_argument("Invalid buffer pointers or sizes in perform()"); + const long numIn = static_cast(inputBuffer.size()); + const long numOut = static_cast(outputBuffer.size()); + + if (!ready.load(std::memory_order_acquire) || !ins || !outs || numins < numIn || + numouts < numOut) { + silence(outs, numouts, sampleframes); + return; } - for (int i = 0; i < numins; ++i) { - std::copy_n(ins[i], sampleframes, inputBuffer[i].begin()); + // The core sizes its buffers from block_size; never read or write past them. + const long count = + std::min(sampleframes, static_cast(systemConfigs.block_size)); + + for (long i = 0; i < numIn; ++i) { + if (!ins[i]) { + silence(outs, numouts, sampleframes); + return; + } + std::copy_n(ins[i], count, inputBuffer[i].begin()); } outputBuffer = combinator->perform(inputBuffer); - for (int i = 0; i < numouts; ++i) { - std::copy_n(outputBuffer[i].begin(), sampleframes, outs[i]); + for (long i = 0; i < numOut; ++i) { + if (!outs[i]) { + continue; + } + std::copy_n(outputBuffer[i].begin(), count, outs[i]); + if (count < sampleframes) { + std::memset(outs[i] + count, 0, (sampleframes - count) * sizeof(double)); + } } } /** - * @brief Gets the number of output channels - * @return Number of output channels based on enabled feature extractors + * @brief Gets the combination mode this object was created with */ - [[nodiscard]] int getOutputCount() const noexcept { return outputCount; } + [[nodiscard]] const std::string& getMode() const noexcept { return combMode; } /** - * @brief Gets the number of input channels - * @return Number of input channels (currently fixed at 1) + * @brief Gets the number of envelope sets being combined */ - [[nodiscard]] constexpr int getInputCount() const noexcept { return inputCount; } + [[nodiscard]] int getInputCount() const noexcept { return inputCount; } /** - * @brief Gets the total number of ports (inlets + outlets) - * @return Total count of all audio ports + * @brief Gets the number of envelopes per set, 0 until prepare() has succeeded */ - [[nodiscard]] int getPortCount() const noexcept { return inputCount + outputCount; } + [[nodiscard]] int getChannelCount() const noexcept { return numChannel; } ~ZerrCombinator() = default; - private: - int inputCount = 2; /**< Number of signal inlets for receiving audio input */ - int outputCount = 1; /**< Number of signal outlets based on enabled feature extractors */ + private: + /** + * @brief Zeroes every output buffer; the fallback whenever processing cannot proceed + */ + static void silence(double** outs, long numouts, long sampleframes) noexcept + { + if (!outs) { + return; + } + for (long i = 0; i < numouts; ++i) { + if (outs[i]) { + std::memset(outs[i], 0, sampleframes * sizeof(double)); + } + } + } + + int inputCount; /**< Number of envelope sets, one multichannel inlet each */ + int numChannel{0}; /**< Envelopes per set; known only once prepare() has run */ + std::string combMode; /**< Combination mode: "add", "root" or "max" */ - zerr::SystemConfigs systemConfigs; /**< System configuration settings */ + zerr::SystemConfigs systemConfigs{0, 0}; /**< System configuration settings */ - zerr::Blocks inputBuffer; /**< Buffer for storing incoming audio samples */ - zerr::Blocks outputBuffer; /**< Multi-channel buffer for storing processed audio samples */ + std::atomic ready{false}; /**< True once the core module is built and buffers sized */ - std::unique_ptr combinator; /**< Core component that implements the feature extraction algorithms */ + zerr::Blocks inputBuffer; /**< Buffer for storing incoming audio samples */ + zerr::Blocks outputBuffer; /**< Multi-channel buffer for storing processed audio samples */ - std::string zerr_cfg; /**< Path to configuration file */ -}; \ No newline at end of file + std::unique_ptr combinator; /**< Core combination algorithms */ +};