Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions core/include/modules/envelopecombinator.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include "logger.h"
#include "types.h"
#include "utils.h"
#include <functional>
#include <atomic>

namespace zerr {
/**
Expand Down Expand Up @@ -45,19 +45,56 @@ class EnvelopeCombinator {
/**
* @brief Process input envelope blocks and combine using selected mode
* @param in Input envelope blocks to process
* @return Combined output envelope blocks
* @return Reference to the internal output buffer, valid until the next perform()
*
* Taken and returned by reference so that nothing is allocated on the audio thread.
*/
Blocks perform(Blocks in);
const Blocks& perform(const Blocks& in);
/**
* @brief Change the combination mode
* @param mode Mode name: "add", "root" or "max"
* @return false, leaving the mode unchanged, if the name is not a known mode
*
* Safe to call from a control thread while perform() runs on an audio thread: the
* mode is a lock-free atomic that perform() loads once per block, so a block is
* never processed partly in one mode and partly in another.
*/
bool set_mode(const std::string& mode) noexcept;
/**
* @brief Change the combination mode
* @param mode The mode to switch to
*/
void set_mode(CombMode mode) noexcept;
/**
* @brief Get the combination mode currently in use
*/
[[nodiscard]] CombMode get_mode() const noexcept
{
return combMode.load(std::memory_order_relaxed);
}
/**
* @brief Get the name of the combination mode currently in use
* @return "add", "root" or "max"
*/
[[nodiscard]] const char* get_mode_name() const noexcept { return toString(get_mode()); }
~EnvelopeCombinator() = default;

private:
using ProcessFunction = void (EnvelopeCombinator::*)();
ProcessFunction processFunc;

int numSource; /**< Number of envelope sources to combine */
int numChannel; /**< Number of channels per source */
zerr::SystemConfigs systemCfgs; /**< system configuration: sample_rate, block_size */
std::string combMode; /**< Mode for combining envelopes */

/**< Mode for combining envelopes. Atomic because a control thread may change it while
the audio thread is inside perform(). */
std::atomic<CombMode> combMode{CombMode::Max};
static_assert(std::atomic<CombMode>::is_always_lock_free,
"CombMode must be lock-free so set_mode() never blocks the audio thread");

bool modeValid{true}; /**< False when the constructor was given an unknown mode name */
/**< The mode name as given to the constructor, kept only so initialize() can name it in
its error message. Control thread only -- never read while perform() runs. */
std::string requestedModeName;
bool prepared{false}; /**< True once initialize() has sized the buffers */

Logger logger; /**< Logger instance for debug/error messages */

Expand Down
72 changes: 72 additions & 0 deletions core/include/utils/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,78 @@ inline GenMode parseGenMode(const std::string& s)
throw std::invalid_argument("Unknown GenMode: " + s);
}

/**< Strategy for combining envelopes coming from several sources */
enum class CombMode {
Add, /**< Sum the envelopes across sources */
Root, /**< Geometric mean: the Nth root of the N sources multiplied together */
Max, /**< Largest envelope value across sources */
};

/**
* @brief Parse a string into a CombMode enum value without throwing
* @param s The string to parse ("add", "root" or "max")
* @param out Receives the parsed value; left untouched when the string is not a mode
* @return true if the string named a mode
*
* The non-throwing counterpart of parseCombMode, for callers on a host's C callback
* stack where an escaping exception would terminate the process.
*/
inline bool tryParseCombMode(const std::string& s, CombMode& out) noexcept
{
if (s == "add") {
out = CombMode::Add;
return true;
}
if (s == "root") {
out = CombMode::Root;
return true;
}
if (s == "max") {
out = CombMode::Max;
return true;
}
return false;
}

/**
* @brief Parse a string into a CombMode enum value
* @param s The string to parse ("add", "root" or "max")
* @return CombMode The corresponding enum value
* @throws std::invalid_argument if the string is not a valid mode
*/
inline CombMode parseCombMode(const std::string& s)
{
CombMode mode;
if (!tryParseCombMode(s, mode)) {
throw std::invalid_argument("Unknown CombMode: " + s);
}
return mode;
}

/**
* @brief Render a CombMode as the string that names it
*/
inline const char* toString(CombMode mode) noexcept
{
switch (mode) {
case CombMode::Add:
return "add";
case CombMode::Root:
return "root";
case CombMode::Max:
return "max";
}
return "max";
}

/**
* @brief The supported combination modes as a human-readable list, for error messages
*
* Single source of truth for wrappers reporting a bad mode, so a new mode only has to be
* added here and in tryParseCombMode.
*/
inline const char* combModeNames() noexcept { return "add, root or max"; }

using ConfigPath = std::string; /**< Path string for configuration files */

using Params = std::vector<Param>; /**< Vector container for parameter values */
Expand Down
60 changes: 45 additions & 15 deletions core/src/modules/envelopecombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ EnvelopeCombinator::EnvelopeCombinator(int numSource, int numChannel, SystemConf
this->numSource = numSource;
this->numChannel = numChannel;
this->systemCfgs = systemCfgs;
this->combMode = combMode;

// Parse now, report in initialize(). The constructor cannot fail, and wrappers
// already gate on initialize()'s return value.
CombMode parsed;
modeValid = tryParseCombMode(combMode, parsed);
if (modeValid) {
this->combMode.store(parsed, std::memory_order_relaxed);
}
this->requestedModeName = combMode;

numInlet = numSource * numChannel;
numOutlet = numChannel;
Expand All @@ -30,29 +38,51 @@ bool EnvelopeCombinator::initialize()
inputBuffer.resize(numInlet, Samples(systemCfgs.block_size, 0.0f));
outputBuffer.resize(numOutlet, Samples(systemCfgs.block_size, 0.0f));

if (combMode == "add") {
processFunc = &EnvelopeCombinator::_process_add;
}
else if (combMode == "root") {
processFunc = &EnvelopeCombinator::_process_root;
}
else if (combMode == "max") {
processFunc = &EnvelopeCombinator::_process_max;
}
else {
logger.logError("EnvelopeCombinator::initialize Unknown combination mode: " + combMode);
if (!modeValid) {
logger.logError("EnvelopeCombinator::initialize Unknown combination mode: " +
requestedModeName);
return false;
}

prepared = true;
return true;
}

bool EnvelopeCombinator::set_mode(const std::string& mode) noexcept
{
CombMode parsed;
if (!tryParseCombMode(mode, parsed)) {
return false;
}
set_mode(parsed);
return true;
}

Blocks EnvelopeCombinator::perform(Blocks in)
void EnvelopeCombinator::set_mode(CombMode mode) noexcept
{
// Relaxed is enough: the enum is the only datum published, and it guards nothing else.
combMode.store(mode, std::memory_order_relaxed);
}

const Blocks& EnvelopeCombinator::perform(const Blocks& in)
{
if (!prepared) {
return outputBuffer;
}

inputBuffer = in;

if (processFunc) {
(this->*processFunc)();
// Load once per block so a block is never processed partly in one mode.
switch (combMode.load(std::memory_order_relaxed)) {
case CombMode::Add:
_process_add();
break;
case CombMode::Root:
_process_root();
break;
case CombMode::Max:
_process_max();
break;
}

return outputBuffer;
Expand Down
72 changes: 72 additions & 0 deletions maxmsp/help/mc.zerr.combinator~.maxhelp
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,57 @@
"sig" : 0.0
}

}
, {
"box" : {
"fontsize" : 14.0,
"id" : "obj-60",
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 560.0, 378.0, 280.0, 20.0 ],
"text" : "change mode while DSP is running:"
}

}
, {
"box" : {
"fontsize" : 14.0,
"id" : "obj-61",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 560.0, 404.0, 78.0, 24.0 ],
"text" : "mode max"
}

}
, {
"box" : {
"fontsize" : 14.0,
"id" : "obj-62",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 646.0, 404.0, 84.0, 24.0 ],
"text" : "mode root"
}

}
, {
"box" : {
"fontsize" : 14.0,
"id" : "obj-63",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 738.0, 404.0, 78.0, 24.0 ],
"text" : "mode add"
}

}
],
"lines" : [ {
Expand All @@ -493,6 +544,27 @@
"source" : [ "obj-28", 0 ]
}

}
, {
"patchline" : {
"destination" : [ "obj-28", 0 ],
"source" : [ "obj-61", 0 ]
}

}
, {
"patchline" : {
"destination" : [ "obj-28", 0 ],
"source" : [ "obj-62", 0 ]
}

}
, {
"patchline" : {
"destination" : [ "obj-28", 0 ],
"source" : [ "obj-63", 0 ]
}

}
, {
"patchline" : {
Expand Down
Loading
Loading