| @@ -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(); | |||
| } | |||