@@ -221,28 +221,10 @@ struct ADSR : Module { | |||
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 | |||
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 { | |||
std::string getDisplayValueString() override { | |||
switch ((EnvelopeMode) getValue()) { | |||
@@ -259,10 +241,10 @@ struct ADSR : Module { | |||
configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope"); | |||
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<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); | |||
} | |||
@@ -28,10 +28,6 @@ struct Mex : Module { | |||
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 { | |||
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 { | |||
for (int i = 0; i < 8; i++) { | |||
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" | |||
using simd::float_4; | |||
// equal sum crossfade, -1 <= p <= 1 | |||
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. | |||
// 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 { | |||
float out = 0.f; | |||
@@ -114,7 +80,7 @@ struct Morphader : Module { | |||
}; | |||
static const int NUM_MIXER_CHANNELS = 4; | |||
const simd::float_4 normal10VSimd = {10.f}; | |||
const float_4 normal10VSimd = {10.f}; | |||
ExpLogSlewLimiter slewLimiter; | |||
// 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 | |||
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(); | |||
slewLimiter.setSlew(slewLambda); | |||
const float masterCrossfadeValue = slewLimiter.process(deltaTime, params[FADER_PARAM].getValue()); | |||
@@ -192,8 +158,8 @@ struct Morphader : Module { | |||
void process(const ProcessArgs& args) override { | |||
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++) { | |||
@@ -204,10 +170,10 @@ struct Morphader : Module { | |||
maxChannels = std::max(maxChannels, channels); | |||
} | |||
simd::float_4 out[4] = {0.f}; | |||
float_4 out[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())) { | |||
case CV_MODE: { | |||
@@ -221,7 +187,9 @@ struct Morphader : Module { | |||
out[c / 4] = equalPowerCrossfade(inA, inB, channelCrossfades[i]); | |||
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])); | |||
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 | |||
} | |||
@@ -641,25 +641,39 @@ struct Muxlicer : Module { | |||
void dataFromJson(json_t* rootJ) override { | |||
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"); | |||
quadraticGatesOnly = json_boolean_value(quadraticJ); | |||
if (quadraticJ) { | |||
quadraticGatesOnly = json_boolean_value(quadraticJ); | |||
} | |||
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"); | |||
mainClockMultDiv.multDiv = json_integer_value(mainClockMultDivJ); | |||
if (mainClockMultDivJ) { | |||
mainClockMultDiv.multDiv = json_integer_value(mainClockMultDivJ); | |||
} | |||
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"); | |||
playState = (PlayState) json_integer_value(playStateJ); | |||
if (playStateJ) { | |||
playState = (PlayState) json_integer_value(playStateJ); | |||
} | |||
json_t* outputClockFollowsPlayModeJ = json_object_get(rootJ, "outputClockFollowsPlayMode"); | |||
outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); | |||
if (outputClockFollowsPlayModeJ) { | |||
outputClockFollowsPlayMode = json_boolean_value(outputClockFollowsPlayModeJ); | |||
} | |||
updateParamFromMainClockMultDiv(); | |||
} | |||