@@ -221,28 +221,10 @@ struct ADSR : Module { | |||||
static constexpr float maxStageTime = 10.f; // in seconds | static constexpr float maxStageTime = 10.f; // in seconds | ||||
// given a value from the slider and/or cv (rescaled to range 0 to 1), transform into the appropriate time in seconds | // given a value from the slider and/or cv (rescaled to range 0 to 1), transform into the appropriate time in seconds | ||||
static float convertCVToTimeInSeconds(float cv) { | |||||
float cv2 = cv * cv; | |||||
// according to hardware, slider appears to respond roughly as a quartic | |||||
return minStageTime + (maxStageTime - minStageTime) * cv2 * cv2; | |||||
static float convertCVToTimeInSeconds(float cv) { | |||||
return minStageTime * std::pow(maxStageTime / minStageTime, cv); | |||||
} | } | ||||
// given a time in seconds, transform into the appropriate CV/slider value (in range 0, 1) | |||||
static float convertTimeInSecondsToCV(float timeInSecs) { | |||||
// according to hardware, slider appears to respond roughly as a quartic | |||||
return std::pow((timeInSecs - minStageTime) / (maxStageTime - minStageTime), 0.25f); | |||||
} | |||||
struct StageTimeParam : ParamQuantity { | |||||
std::string getDisplayValueString() override { | |||||
return string::f("%.3f", convertCVToTimeInSeconds(getValue())); | |||||
} | |||||
void setDisplayValue(float v) override { | |||||
ParamQuantity::setDisplayValue(convertTimeInSecondsToCV(v)); | |||||
} | |||||
}; | |||||
struct TriggerGateParamQuantity : ParamQuantity { | struct TriggerGateParamQuantity : ParamQuantity { | ||||
std::string getDisplayValueString() override { | std::string getDisplayValueString() override { | ||||
switch ((EnvelopeMode) getValue()) { | switch ((EnvelopeMode) getValue()) { | ||||
@@ -259,10 +241,10 @@ struct ADSR : Module { | |||||
configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope"); | configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope"); | ||||
configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape"); | configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape"); | ||||
configParam<StageTimeParam>(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s"); | |||||
configParam<StageTimeParam>(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s"); | |||||
configParam(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s", maxStageTime / minStageTime, minStageTime); | |||||
configParam(DECAY_PARAM, 0.f, 1.f, 0.f, "Decay time", "s", maxStageTime / minStageTime, minStageTime); | |||||
configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f); | configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f); | ||||
configParam<StageTimeParam>(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s"); | |||||
configParam(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s", maxStageTime / minStageTime, minStageTime); | |||||
cvDivider.setDivision(16); | cvDivider.setDivision(16); | ||||
} | } | ||||
@@ -28,10 +28,6 @@ struct Mex : Module { | |||||
dsp::SchmittTrigger gateInTrigger; | dsp::SchmittTrigger gateInTrigger; | ||||
// this expander communicates with the mother module (Muxlicer) purely | |||||
// through this pointer (it cannot modify Muxlicer, read-only) | |||||
Muxlicer const* mother = nullptr; | |||||
struct GateSwitchParamQuantity : ParamQuantity { | struct GateSwitchParamQuantity : ParamQuantity { | ||||
std::string getDisplayValueString() override { | std::string getDisplayValueString() override { | ||||
@@ -52,72 +48,59 @@ struct Mex : Module { | |||||
} | } | ||||
} | } | ||||
Muxlicer* findHostModulePtr(Module* module) { | |||||
if (module) { | |||||
if (module->leftExpander.module) { | |||||
// if it's Muxlicer, we're done | |||||
if (module->leftExpander.module->model == modelMuxlicer) { | |||||
return reinterpret_cast<Muxlicer*>(module->leftExpander.module); | |||||
} | |||||
// if it's Mex, keep recursing | |||||
else if (module->leftExpander.module->model == modelMex) { | |||||
return findHostModulePtr(module->leftExpander.module); | |||||
} | |||||
} | |||||
} | |||||
return nullptr; | |||||
} | |||||
void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
for (int i = 0; i < 8; i++) { | for (int i = 0; i < 8; i++) { | ||||
lights[i].setBrightness(0.f); | lights[i].setBrightness(0.f); | ||||
} | } | ||||
if (leftExpander.module) { | |||||
// this expander is active if: | |||||
// * muxlicer is to the left or | |||||
if (leftExpander.module->model == modelMuxlicer) { | |||||
mother = reinterpret_cast<Muxlicer*>(leftExpander.module); | |||||
} | |||||
// * an active Mex is to the left | |||||
else if (leftExpander.module->model == modelMex) { | |||||
Mex* moduleMex = reinterpret_cast<Mex*>(leftExpander.module); | |||||
if (moduleMex) { | |||||
mother = moduleMex->mother; | |||||
} | |||||
} | |||||
else { | |||||
mother = nullptr; | |||||
} | |||||
Muxlicer const* mother = findHostModulePtr(this); | |||||
if (mother) { | |||||
if (mother) { | |||||
float gate = 0.f; | |||||
float gate = 0.f; | |||||
if (mother->playState != Muxlicer::STATE_STOPPED) { | |||||
const int currentStep = clamp(mother->addressIndex, 0, 7); | |||||
StepState state = (StepState) params[STEP_PARAM + currentStep].getValue(); | |||||
if (state == MUXLICER_MODE) { | |||||
gate = mother->isAllGatesOutHigh; | |||||
if (mother->playState != Muxlicer::STATE_STOPPED) { | |||||
const int currentStep = clamp(mother->addressIndex, 0, 7); | |||||
StepState state = (StepState) params[STEP_PARAM + currentStep].getValue(); | |||||
if (state == MUXLICER_MODE) { | |||||
gate = mother->isAllGatesOutHigh; | |||||
} | |||||
else if (state == GATE_IN_MODE) { | |||||
// gate in will convert non-gate signals to gates (via schmitt trigger) | |||||
// if input is present | |||||
if (inputs[GATE_IN_INPUT].isConnected()) { | |||||
gateInTrigger.process(inputs[GATE_IN_INPUT].getVoltage()); | |||||
gate = gateInTrigger.isHigh(); | |||||
} | } | ||||
else if (state == GATE_IN_MODE) { | |||||
// gate in will convert non-gate signals to gates (via schmitt trigger) | |||||
// if input is present | |||||
if (inputs[GATE_IN_INPUT].isConnected()) { | |||||
gateInTrigger.process(inputs[GATE_IN_INPUT].getVoltage()); | |||||
gate = gateInTrigger.isHigh(); | |||||
} | |||||
// otherwise the main Muxlicer output clock (including divisions/multiplications) | |||||
// is normalled in | |||||
else { | |||||
gate = mother->isOutputClockHigh; | |||||
} | |||||
// otherwise the main Muxlicer output clock (including divisions/multiplications) | |||||
// is normalled in | |||||
else { | |||||
gate = mother->isOutputClockHigh; | |||||
} | } | ||||
lights[currentStep].setBrightness(gate); | |||||
} | } | ||||
outputs[OUT_OUTPUT].setVoltage(gate * 10.f); | |||||
// if there's another Mex to the right, update it to also point at the message we just received, | |||||
// i.e. just forward on the message | |||||
if (rightExpander.module && rightExpander.module->model == modelMex) { | |||||
Mex* moduleMexRight = reinterpret_cast<Mex*>(rightExpander.module); | |||||
// assign current message pointer to the right expander | |||||
moduleMexRight->mother = mother; | |||||
} | |||||
lights[currentStep].setBrightness(gate); | |||||
} | } | ||||
} | |||||
// if we've become disconnected, i.e. no module to the left, then break the connection | |||||
// which will propagate to all expanders to the right | |||||
else { | |||||
mother = nullptr; | |||||
outputs[OUT_OUTPUT].setVoltage(gate * 10.f); | |||||
} | } | ||||
} | } | ||||
}; | }; | ||||
@@ -1,5 +1,6 @@ | |||||
#include "plugin.hpp" | #include "plugin.hpp" | ||||
using simd::float_4; | |||||
// equal sum crossfade, -1 <= p <= 1 | // equal sum crossfade, -1 <= p <= 1 | ||||
template <typename T> | template <typename T> | ||||
@@ -16,43 +17,8 @@ inline T equalPowerCrossfade(T a, T b, const float p) { | |||||
// TExponentialSlewLimiter doesn't appear to work as is required for this application. | // TExponentialSlewLimiter doesn't appear to work as is required for this application. | ||||
// I think it is due to the absence of the logic that stops the output rising / falling too quickly, | // I think it is due to the absence of the logic that stops the output rising / falling too quickly, | ||||
// i.e. faster than the original signal? I think the following modification would yield the | |||||
// expected behaviour (see 2 lines in process). | |||||
/* | |||||
template <typename T = float> | |||||
struct TExponentialSlewLimiter { | |||||
T out = 0.f; | |||||
T riseLambda = 0.f; | |||||
T fallLambda = 0.f; | |||||
void reset() { | |||||
out = 0.f; | |||||
} | |||||
void setRiseFall(T riseLambda, T fallLambda) { | |||||
this->riseLambda = riseLambda; | |||||
this->fallLambda = fallLambda; | |||||
} | |||||
T process(T deltaTime, T in) { | |||||
// MODIFICATION: | |||||
T rising = in > out; | |||||
T lambda = simd::ifelse(rising, riseLambda, fallLambda); | |||||
T y = out + (in - out) * lambda * deltaTime; | |||||
// If the change from the old out to the new out is too small for floats, set `in` directly. | |||||
out = simd::ifelse(out == y, in, y); | |||||
// MODIFICATION: | |||||
out = simd::ifelse(rising, simd::ifelse(out > in, in, y), simd::ifelse(out < in, in, y)); | |||||
return out; | |||||
} | |||||
DEPRECATED T process(T in) { | |||||
return process(1.f, in); | |||||
} | |||||
}; | |||||
*/ | |||||
// For now, I provide this implementation (essentialy the same as SlewLimiter.cpp), but ideally I | |||||
// would replace with updated library function | |||||
// i.e. faster than the original signal? For now, we use this implementation (essentialy the same as | |||||
// SlewLimiter.cpp) | |||||
struct ExpLogSlewLimiter { | struct ExpLogSlewLimiter { | ||||
float out = 0.f; | float out = 0.f; | ||||
@@ -114,7 +80,7 @@ struct Morphader : Module { | |||||
}; | }; | ||||
static const int NUM_MIXER_CHANNELS = 4; | static const int NUM_MIXER_CHANNELS = 4; | ||||
const simd::float_4 normal10VSimd = {10.f}; | |||||
const float_4 normal10VSimd = {10.f}; | |||||
ExpLogSlewLimiter slewLimiter; | ExpLogSlewLimiter slewLimiter; | ||||
// minimum and maximum slopes in volts per second, they specify the time to get | // minimum and maximum slopes in volts per second, they specify the time to get | ||||
@@ -152,9 +118,9 @@ struct Morphader : Module { | |||||
} | } | ||||
// determine the cross-fade between -1 (A) and +1 (B) for each of the 4 channels | // determine the cross-fade between -1 (A) and +1 (B) for each of the 4 channels | ||||
simd::float_4 determineChannelCrossfades(const float deltaTime) { | |||||
float_4 determineChannelCrossfades(const float deltaTime) { | |||||
simd::float_4 channelCrossfades = {}; | |||||
float_4 channelCrossfades = {}; | |||||
const float slewLambda = 2.0f / params[FADER_LAG_PARAM].getValue(); | const float slewLambda = 2.0f / params[FADER_LAG_PARAM].getValue(); | ||||
slewLimiter.setSlew(slewLambda); | slewLimiter.setSlew(slewLambda); | ||||
const float masterCrossfadeValue = slewLimiter.process(deltaTime, params[FADER_PARAM].getValue()); | const float masterCrossfadeValue = slewLimiter.process(deltaTime, params[FADER_PARAM].getValue()); | ||||
@@ -192,8 +158,8 @@ struct Morphader : Module { | |||||
void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
int maxChannels = 1; | int maxChannels = 1; | ||||
simd::float_4 mix[4] = {0.f}; | |||||
const simd::float_4 channelCrossfades = determineChannelCrossfades(args.sampleTime); | |||||
float_4 mix[4] = {}; | |||||
const float_4 channelCrossfades = determineChannelCrossfades(args.sampleTime); | |||||
for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { | for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { | ||||
@@ -204,10 +170,10 @@ struct Morphader : Module { | |||||
maxChannels = std::max(maxChannels, channels); | maxChannels = std::max(maxChannels, channels); | ||||
} | } | ||||
simd::float_4 out[4] = {0.f}; | |||||
float_4 out[4] = {}; | |||||
for (int c = 0; c < channels; c += 4) { | for (int c = 0; c < channels; c += 4) { | ||||
simd::float_4 inA = inputs[A_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[A_LEVEL + i].getValue(); | |||||
simd::float_4 inB = inputs[B_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[B_LEVEL + i].getValue(); | |||||
float_4 inA = inputs[A_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[A_LEVEL + i].getValue(); | |||||
float_4 inB = inputs[B_INPUT + i].getNormalVoltageSimd(normal10VSimd, c) * params[B_LEVEL + i].getValue(); | |||||
switch (static_cast<CrossfadeMode>(params[MODE + i].getValue())) { | switch (static_cast<CrossfadeMode>(params[MODE + i].getValue())) { | ||||
case CV_MODE: { | case CV_MODE: { | ||||
@@ -221,7 +187,9 @@ struct Morphader : Module { | |||||
out[c / 4] = equalPowerCrossfade(inA, inB, channelCrossfades[i]); | out[c / 4] = equalPowerCrossfade(inA, inB, channelCrossfades[i]); | ||||
break; | break; | ||||
} | } | ||||
default: assert(false); | |||||
default: { | |||||
out[c / 4] = 0.f; | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -257,7 +225,11 @@ struct Morphader : Module { | |||||
lights[B_LED + i].setBrightness(equalSumCrossfade(0.f, 1.f, channelCrossfades[i])); | lights[B_LED + i].setBrightness(equalSumCrossfade(0.f, 1.f, channelCrossfades[i])); | ||||
break; | break; | ||||
} | } | ||||
default: assert(false); | |||||
default: { | |||||
lights[A_LED + i].setBrightness(0.f); | |||||
lights[B_LED + i].setBrightness(0.f); | |||||
break; | |||||
} | |||||
} | } | ||||
} // end loop over mixer channels | } // end loop over mixer channels | ||||
} | } | ||||
@@ -641,25 +641,39 @@ struct Muxlicer : Module { | |||||
void dataFromJson(json_t* rootJ) override { | void dataFromJson(json_t* rootJ) override { | ||||
json_t* modeJ = json_object_get(rootJ, "modeCOMIO"); | json_t* modeJ = json_object_get(rootJ, "modeCOMIO"); | ||||
modeCOMIO = (Muxlicer::ModeCOMIO) json_integer_value(modeJ); | |||||
if (modeJ) { | |||||
modeCOMIO = (Muxlicer::ModeCOMIO) json_integer_value(modeJ); | |||||
} | |||||
json_t* quadraticJ = json_object_get(rootJ, "quadraticGatesOnly"); | json_t* quadraticJ = json_object_get(rootJ, "quadraticGatesOnly"); | ||||
quadraticGatesOnly = json_boolean_value(quadraticJ); | |||||
if (quadraticJ) { | |||||
quadraticGatesOnly = json_boolean_value(quadraticJ); | |||||
} | |||||
json_t* allInNormalVoltageJ = json_object_get(rootJ, "allInNormalVoltage"); | json_t* allInNormalVoltageJ = json_object_get(rootJ, "allInNormalVoltage"); | ||||
allInNormalVoltage = json_integer_value(allInNormalVoltageJ); | |||||
if (allInNormalVoltageJ) { | |||||
allInNormalVoltage = json_integer_value(allInNormalVoltageJ); | |||||
} | |||||
json_t* mainClockMultDivJ = json_object_get(rootJ, "mainClockMultDiv"); | json_t* mainClockMultDivJ = json_object_get(rootJ, "mainClockMultDiv"); | ||||
mainClockMultDiv.multDiv = json_integer_value(mainClockMultDivJ); | |||||
if (mainClockMultDivJ) { | |||||
mainClockMultDiv.multDiv = json_integer_value(mainClockMultDivJ); | |||||
} | |||||
json_t* outputClockMultDivJ = json_object_get(rootJ, "outputClockMultDiv"); | json_t* outputClockMultDivJ = json_object_get(rootJ, "outputClockMultDiv"); | ||||
outputClockMultDiv.multDiv = json_integer_value(outputClockMultDivJ); | |||||
if (outputClockMultDivJ) { | |||||
outputClockMultDiv.multDiv = json_integer_value(outputClockMultDivJ); | |||||
} | |||||
json_t* playStateJ = json_object_get(rootJ, "playState"); | json_t* playStateJ = json_object_get(rootJ, "playState"); | ||||
playState = (PlayState) json_integer_value(playStateJ); | |||||
if (playStateJ) { | |||||
playState = (PlayState) json_integer_value(playStateJ); | |||||
} | |||||
json_t* outputClockFollowsPlayModeJ = json_object_get(rootJ, "outputClockFollowsPlayMode"); | json_t* outputClockFollowsPlayModeJ = json_object_get(rootJ, "outputClockFollowsPlayMode"); | ||||
outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); | |||||
if (outputClockFollowsPlayModeJ) { | |||||
outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); | |||||
} | |||||
updateParamFromMainClockMultDiv(); | updateParamFromMainClockMultDiv(); | ||||
} | } | ||||