diff --git a/CHANGELOG.md b/CHANGELOG.md index 0869697..20f9914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v2.9.0 + * MuDi, Slew, Atte, Mixer2, AxBC initial releases + * Fix missing port information (multiple modules) + * Fix StereoStrip issue at very low sample rates + ## v2.8.2 * EvenVCO * Upsample Hard Sync and FM inputs diff --git a/plugin.json b/plugin.json index d2a2274..dacab15 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "2.8.2", + "version": "2.9.0", "license": "GPL-3.0-or-later", "name": "Befaco", "brand": "Befaco", @@ -359,6 +359,65 @@ "Polyphonic", "Utility" ] + }, + { + "slug": "Mixer2", + "name": "Mixer", + "description": "Utilitarian audio and CV mixer", + "manualUrl": "https://www.befaco.org/mixer-v2/", + "tags": [ + "Hardware clone", + "Mixer", + "Polyphonic" + ] + }, + { + "slug": "Atte", + "name": "Atte", + "description": "Quad Attenuator/Inverter, Splitter, and Offset Generator", + "manualUrl": "https://www.befaco.org/atte/", + "tags": [ + "Attenuator", + "Hardware clone", + "Polyphonic" + ] + }, + { + "slug": "AxBC", + "name": "AxBC", + "description": "Voltage-Controlled Voltage Processor", + "manualUrl": "https://www.befaco.org/axbc/", + "tags": [ + "Ring Modulator", + "Attenuator", + "Dual", + "Hardware clone", + "Polyphonic" + ] + }, + { + "slug": "Slew", + "name": "Slew", + "description": "Voltage-controlled lag processor and slope detector", + "manualUrl": "https://www.befaco.org/slew/", + "tags": [ + "Slew Limiter", + "Envelope Follower", + "Hardware clone", + "Polyphonic" + ] + }, + { + "slug": "MuDi", + "name": "MuDi", + "description": "Clock Multiple, Conditioner, and Divider in a compact 2HP format", + "manualUrl": "https://www.befaco.org/mudi/", + "tags": [ + "Clock generator", + "Clock modulator", + "Hardware clone", + "Polyphonic" + ] } ] } \ No newline at end of file diff --git a/res/panels/Atte.svg b/res/panels/Atte.svg new file mode 100644 index 0000000..11438f8 --- /dev/null +++ b/res/panels/Atte.svg @@ -0,0 +1,610 @@ + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/AxBC.svg b/res/panels/AxBC.svg new file mode 100644 index 0000000..e5e2e42 --- /dev/null +++ b/res/panels/AxBC.svg @@ -0,0 +1,1173 @@ + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/Mixer2.svg b/res/panels/Mixer2.svg new file mode 100644 index 0000000..df3bc94 --- /dev/null +++ b/res/panels/Mixer2.svg @@ -0,0 +1,723 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + +   + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/MuDi.svg b/res/panels/MuDi.svg new file mode 100644 index 0000000..1e4030a --- /dev/null +++ b/res/panels/MuDi.svg @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/panels/Slew.svg b/res/panels/Slew.svg new file mode 100644 index 0000000..f68ca0a --- /dev/null +++ b/res/panels/Slew.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RISING + FALLING + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ABC.cpp b/src/ABC.cpp index 56ce2f7..c30162a 100644 --- a/src/ABC.cpp +++ b/src/ABC.cpp @@ -2,15 +2,6 @@ using simd::float_4; -template -static T clip(T x) { - // Pade approximant of x/(1 + x^12)^(1/12) - const T limit = 1.16691853009184f; - x = clamp(x * 0.1f, -limit, limit); - return 10.0f * (x + 1.45833f * simd::pow(x, 13) + 0.559028f * simd::pow(x, 25) + 0.0427035f * simd::pow(x, 37)) - / (1.0f + 1.54167f * simd::pow(x, 12) + 0.642361f * simd::pow(x, 24) + 0.0579909f * simd::pow(x, 36)); -} - struct ABC : Module { enum ParamIds { B1_LEVEL_PARAM, @@ -102,10 +93,10 @@ struct ABC : Module { float b = 0.f; for (int c = 0; c < channels; c++) b += std::pow(lastOut[c / 4][c % 4], 2); - b = std::sqrt(b); + b = std::sqrt(b / channels); lights[outLight + 0].setBrightness(0.0f); lights[outLight + 1].setBrightness(0.0f); - lights[outLight + 2].setBrightness(b); + lights[outLight + 2].setBrightness(b / 5.f); } } diff --git a/src/Atte.cpp b/src/Atte.cpp new file mode 100644 index 0000000..2a55cbb --- /dev/null +++ b/src/Atte.cpp @@ -0,0 +1,188 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct Atte : Module { + enum ParamId { + GAIN_A_PARAM, + GAIN_B_PARAM, + GAIN_C_PARAM, + GAIN_D_PARAM, + MODE_A_PARAM, + MODE_B_PARAM, + MODE_C_PARAM, + MODE_D_PARAM, + PARAMS_LEN + }; + enum InputId { + A_INPUT, + B_INPUT, + C_INPUT, + D_INPUT, + INPUTS_LEN + }; + enum OutputId { + A_OUTPUT, + B_OUTPUT, + C_OUTPUT, + D_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(A_LIGHT, 3), + ENUMS(B_LIGHT, 3), + ENUMS(C_LIGHT, 3), + ENUMS(D_LIGHT, 3), + LIGHTS_LEN + }; + const int NUM_CHANNELS = 4; + + dsp::ClockDivider lightDivider; + int normalledVoltageIdx = 2; // 0 - +1V, 1 - +5V, 2 - +10V + const float normalledVoltages[3] = {1.f, 5.f, 10.f}; + + Atte() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(GAIN_A_PARAM, 0.f, 1.f, 1.f, "Gain A"); + configParam(GAIN_B_PARAM, 0.f, 1.f, 1.f, "Gain B"); + configParam(GAIN_C_PARAM, 0.f, 1.f, 1.f, "Gain C"); + configParam(GAIN_D_PARAM, 0.f, 1.f, 1.f, "Gain D"); + configSwitch(MODE_A_PARAM, 0.f, 1.f, 1.f, "Mode A", {"Inverse Attenutation", "Attenuation"}); + configSwitch(MODE_B_PARAM, 0.f, 1.f, 1.f, "Mode B", {"Inverse Attenutation", "Attenuation"}); + configSwitch(MODE_C_PARAM, 0.f, 1.f, 1.f, "Mode C", {"Inverse Attenutation", "Attenuation"}); + configSwitch(MODE_D_PARAM, 0.f, 1.f, 1.f, "Mode D", {"Inverse Attenutation", "Attenuation"}); + + auto inputA = configInput(A_INPUT, "A"); + inputA->description = "Normalled to +10V"; + auto inputB = configInput(B_INPUT, "B"); + inputB->description = "Normalled to input A"; + auto inputC = configInput(C_INPUT, "C"); + inputC->description = "Normalled to input B"; + auto inputD = configInput(D_INPUT, "D"); + inputD->description = "Normalled to input C"; + + configOutput(A_OUTPUT, "A"); + configOutput(B_OUTPUT, "B"); + configOutput(C_OUTPUT, "C"); + configOutput(D_OUTPUT, "D"); + + lightDivider.setDivision(32); + } + + void process(const ProcessArgs& args) override { + + const bool updateLights = lightDivider.process(); + const float deltaTime = args.sampleTime * lightDivider.getDivision(); + + const float normalledVoltage = normalledVoltages[normalledVoltageIdx]; + float_4 previousChannelNormalledVoltage[4] = {normalledVoltage, normalledVoltage, normalledVoltage, normalledVoltage}; + int previousChannelPolyphony = 1; + + // loop over the 4 channels + for (int channel = 0; channel < NUM_CHANNELS; channel += 1) { + // polyphony setting is normalled from the previous channel + const int numPolyphonyEngines = std::max(1, inputs[A_INPUT + channel].isConnected() ? inputs[A_INPUT + channel].getChannels() : previousChannelPolyphony); + previousChannelPolyphony = numPolyphonyEngines; + + // loop over the polyphony engines + for (int c = 0; c < numPolyphonyEngines; c += 4) { + + float_4 inA = inputs[A_INPUT + channel].getNormalPolyVoltageSimd(previousChannelNormalledVoltage[c / 4], c); + const float gainMode = (params[MODE_A_PARAM + channel].getValue() ? 1.f : -1.f); + outputs[A_OUTPUT + channel].setVoltageSimd(inA * gainMode * params[GAIN_A_PARAM + channel].getValue(), c); + + previousChannelNormalledVoltage[c / 4] = inA; + } + + outputs[A_OUTPUT + channel].setChannels(numPolyphonyEngines); + + if (updateLights) { + if (numPolyphonyEngines > 1) { + lights[A_LIGHT + 0 + channel * 3].setBrightness(0.f); + lights[A_LIGHT + 1 + channel * 3].setBrightness(0.f); + float sum = 0.f; + for (int c = 0; c < numPolyphonyEngines; c += 4) { + sum += std::pow(outputs[A_OUTPUT + channel].getVoltage(c), 2); + } + lights[A_LIGHT + 2 + channel * 3].setBrightness(std::sqrt(sum / numPolyphonyEngines) / 10.f); + } + else { + // green for positive voltage, red for negative voltage + lights[A_LIGHT + 0 + channel * 3].setSmoothBrightness(outputs[A_OUTPUT + channel].getVoltage() < 0.f ? -outputs[A_OUTPUT + channel].getVoltage() / 10.f : 0.f, deltaTime); + lights[A_LIGHT + 1 + channel * 3].setSmoothBrightness(outputs[A_OUTPUT + channel].getVoltage() > 0.f ? +outputs[A_OUTPUT + channel].getVoltage() / 10.f : 0.f, deltaTime); + lights[A_LIGHT + 2 + channel * 3].setBrightness(0.f); + } + } + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "normalledVoltageIdx", json_integer(normalledVoltageIdx)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* normalledVoltageIdxJ = json_object_get(rootJ, "normalledVoltageIdx"); + if (normalledVoltageIdxJ) { + normalledVoltageIdx = json_integer_value(normalledVoltageIdxJ); + } + } +}; + + +struct AtteWidget : ModuleWidget { + AtteWidget(Atte* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Atte.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParam(mm2px(Vec(1.168, 10.207)), module, Atte::MODE_A_PARAM)); + addParam(createParamCentered(mm2px(Vec(12.2, 13.8)), module, Atte::GAIN_A_PARAM)); + addParam(createParam(mm2px(Vec(1.168, 26.174)), module, Atte::MODE_B_PARAM)); + addParam(createParamCentered(mm2px(Vec(12.2, 29.767)), module, Atte::GAIN_B_PARAM)); + addParam(createParam(mm2px(Vec(1.168, 42.14)), module, Atte::MODE_C_PARAM)); + addParam(createParamCentered(mm2px(Vec(12.2, 45.733)), module, Atte::GAIN_C_PARAM)); + addParam(createParam(mm2px(Vec(1.168, 58.107)), module, Atte::MODE_D_PARAM)); + addParam(createParamCentered(mm2px(Vec(12.2, 61.7)), module, Atte::GAIN_D_PARAM)); + + addInput(createInputCentered(mm2px(Vec(5.0, 76.6)), module, Atte::A_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 88.9)), module, Atte::B_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 101.2)), module, Atte::C_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 113.5)), module, Atte::D_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(14.978, 76.6)), module, Atte::A_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(14.978, 88.9)), module, Atte::B_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(14.978, 101.2)), module, Atte::C_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(14.978, 113.5)), module, Atte::D_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(2.9, 20.85)), module, Atte::A_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(2.9, 36.817)), module, Atte::B_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(2.9, 52.783)), module, Atte::C_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(2.9, 68.75)), module, Atte::D_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + + Atte* module = dynamic_cast(this->module); + assert(module); + + // user can pick +1V, +5V or +10V for the normalled voltage + menu->addChild(createIndexPtrSubmenuItem("Normalled voltage", {"+1V", "+5V", "+10V"}, &module->normalledVoltageIdx)); + } + + void step() override { + Atte* module = dynamic_cast(this->module); + + if (module) { + module->getInputInfo(Atte::A_INPUT)->description = "Normalled to +" + string::f("%.0gV", module->normalledVoltages[module->normalledVoltageIdx]); + } + + ModuleWidget::step(); + } +}; + + +Model* modelAtte = createModel("Atte"); \ No newline at end of file diff --git a/src/AxBC.cpp b/src/AxBC.cpp new file mode 100644 index 0000000..7c4e92c --- /dev/null +++ b/src/AxBC.cpp @@ -0,0 +1,233 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct AxBC : Module { + enum ParamId { + GAIN_B1_PARAM, + B1_PARAM, + GAIN_C1_PARAM, + C1_PARAM, + GAIN_B2_PARAM, + B2_PARAM, + GAIN_C2_PARAM, + C2_PARAM, + MODE_PARAM, + PARAMS_LEN + }; + enum InputId { + A1_INPUT, + B1_INPUT, + C1_INPUT, + A2_INPUT, + B2_INPUT, + C2_INPUT, + INPUTS_LEN + }; + enum OutputId { + OUT_1_OUTPUT, + OUT_2_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(OUT_1_MINUS_LIGHT, 3), + ENUMS(OUT_1_PLUS_LIGHT, 3), + ENUMS(OUT_2_MINUS_LIGHT, 3), + ENUMS(OUT_2_PLUS_LIGHT, 3), + LIGHTS_LEN + }; + const float gains[3] = {-1.f, +1.f, +2.f}; + bool applyClipping = true; + dsp::ClockDivider lightDivider; + + AxBC() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(B1_PARAM, 0.f, 1.f, 1.f, "B1"); + configParam(C1_PARAM, 0.f, 1.f, 0.f, "C1"); + configParam(B2_PARAM, 0.f, 1.f, 1.f, "B2"); + configParam(C2_PARAM, 0.f, 1.f, 0.f, "C2"); + configSwitch(GAIN_B1_PARAM, 0.f, 2.f, 1.f, "Gain Mode", {"x -1", "x 1", "x 2"}); + configSwitch(GAIN_C1_PARAM, 0.f, 2.f, 1.f, "Gain Mode", {"x -1", "x 1", "x 2"}); + configSwitch(GAIN_B2_PARAM, 0.f, 2.f, 1.f, "Gain Mode", {"x -1", "x 1", "x 2"}); + configSwitch(GAIN_C2_PARAM, 0.f, 2.f, 1.f, "Gain Mode", {"x -1", "x 1", "x 2"}); + auto mode = configSwitch(MODE_PARAM, 0.f, 1.f, 0.f, "Mix mode", {"Mix", "Mult"}); + mode->description = "Mix: channel 1 is mixed into channel 2, if channel 1 output is unpatched.\n" + "Mult: a copy of A1 is normalled to A2 input, if A2 is unpatched."; + + configInput(A1_INPUT, "A1"); + configInput(B1_INPUT, "B1"); + configInput(C1_INPUT, "C1"); + configInput(A2_INPUT, "A2"); + configInput(B2_INPUT, "B2"); + configInput(C2_INPUT, "C2"); + + configOutput(OUT_1_OUTPUT, "Out 1"); + configOutput(OUT_2_OUTPUT, "Out 2"); + + lightDivider.setDivision(64); + } + + void process(const ProcessArgs& args) override { + const int numPolyphonyEngines = std::max({1, + inputs[A1_INPUT].getChannels(), inputs[B1_INPUT].getChannels(), inputs[C1_INPUT].getChannels(), + inputs[A2_INPUT].getChannels(), inputs[B2_INPUT].getChannels(), inputs[C2_INPUT].getChannels()}); + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + const float_4 inA1 = inputs[A1_INPUT].getPolyVoltageSimd(c); + const float_4 inB1 = inputs[B1_INPUT].getNormalPolyVoltageSimd(5.f, c) / 5.f; + const float_4 inC1 = inputs[C1_INPUT].getNormalPolyVoltageSimd(5.f, c); + + const float gainB1 = params[B1_PARAM].getValue() * gains[(int) params[GAIN_B1_PARAM].getValue()]; + const float gainC1 = params[C1_PARAM].getValue() * gains[(int) params[GAIN_C1_PARAM].getValue()]; + + // ch1: a * b + c + const float_4 out1 = inA1 * gainB1 * inB1 + gainC1 * inC1; + + const float_4 inA2 = inputs[A2_INPUT].getNormalPolyVoltageSimd(inA1 * params[MODE_PARAM].getValue(), c); + const float_4 inB2 = inputs[B2_INPUT].getNormalPolyVoltageSimd(5.f, c) / 5.f; + const float_4 inC2 = inputs[C2_INPUT].getNormalPolyVoltageSimd(5.f, c); + + const float gainB2 = params[B2_PARAM].getValue() * gains[(int) params[GAIN_B2_PARAM].getValue()]; + const float gainC2 = params[C2_PARAM].getValue() * gains[(int) params[GAIN_C2_PARAM].getValue()]; + + // ch2: a * b + c + const float_4 out2 = inA2 * gainB2 * inB2 + gainC2 * inC2; + // if we're in mix mode and out1 is not connected, mix ch1 into ch2 + const bool isCh1MixedIntoCh2 = (params[MODE_PARAM].getValue() == 0.f) && !outputs[OUT_1_OUTPUT].isConnected(); + + if (applyClipping) { + outputs[OUT_1_OUTPUT].setVoltageSimd(clip(out1), c); + outputs[OUT_2_OUTPUT].setVoltageSimd(clip(out1 * isCh1MixedIntoCh2 + out2), c); + } + else { + outputs[OUT_1_OUTPUT].setVoltageSimd(out1, c); + outputs[OUT_2_OUTPUT].setVoltageSimd(out1 * isCh1MixedIntoCh2 + out2, c); + } + } + + outputs[OUT_1_OUTPUT].setChannels(numPolyphonyEngines); + outputs[OUT_2_OUTPUT].setChannels(numPolyphonyEngines); + + if (lightDivider.process()) { + const float lightTime = args.sampleTime * lightDivider.getDivision(); + processLEDs(lightTime, numPolyphonyEngines); + } + } + + void processLEDs(const float lightTime, const int channels) { + + // monophonic uses red and green LEDs + if (channels == 1) { + + const float redValue1 = -std::min(0.f, outputs[OUT_1_OUTPUT].getVoltage() / 5.f); + const float greenValue1 = +std::max(0.f, outputs[OUT_1_OUTPUT].getVoltage() / 5.f); + lights[OUT_1_MINUS_LIGHT + 0].setSmoothBrightness(redValue1, lightTime); + lights[OUT_1_MINUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_1_MINUS_LIGHT + 2].setBrightness(0.f); + + lights[OUT_1_PLUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_1_PLUS_LIGHT + 1].setSmoothBrightness(greenValue1, lightTime); + lights[OUT_1_PLUS_LIGHT + 2].setBrightness(0.f); + + const float redValue2 = -std::min(0.f, outputs[OUT_2_OUTPUT].getVoltage() / 5.f); + const float greenValue2 = +std::max(0.f, outputs[OUT_2_OUTPUT].getVoltage() / 5.f); + lights[OUT_2_MINUS_LIGHT + 0].setSmoothBrightness(redValue2, lightTime); + lights[OUT_2_MINUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_2_MINUS_LIGHT + 2].setBrightness(0.f); + + lights[OUT_2_PLUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_2_PLUS_LIGHT + 1].setSmoothBrightness(greenValue2, lightTime); + lights[OUT_2_PLUS_LIGHT + 2].setBrightness(0.f); + } + // polyphonic uses blue LEDs, but seperated by signal polarity + else { + float sumNeg1 = 0.f, sumPos1 = 0.f; + float sumNeg2 = 0.f, sumPos2 = 0.f; + for (int c = 0; c < channels; c++) { + sumNeg1 += -std::min(outputs[OUT_1_OUTPUT].getVoltage(c), 0.f); + sumPos1 += +std::max(outputs[OUT_1_OUTPUT].getVoltage(c), 0.f); + + sumNeg2 += -std::min(outputs[OUT_2_OUTPUT].getVoltage(c), 0.f); + sumPos2 += +std::max(outputs[OUT_2_OUTPUT].getVoltage(c), 0.f); + } + lights[OUT_1_MINUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_1_MINUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_1_MINUS_LIGHT + 2].setBrightness(sumNeg1 / channels / 5.f); + + lights[OUT_1_PLUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_1_PLUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_1_PLUS_LIGHT + 2].setBrightness(sumPos1 / channels / 5.f); + + lights[OUT_2_MINUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_2_MINUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_2_MINUS_LIGHT + 2].setBrightness(sumNeg2 / channels / 5.f); + + lights[OUT_2_PLUS_LIGHT + 0].setBrightness(0.f); + lights[OUT_2_PLUS_LIGHT + 1].setBrightness(0.f); + lights[OUT_2_PLUS_LIGHT + 2].setBrightness(sumPos2 / channels / 5.f); + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "applyClipping", json_boolean(applyClipping)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* clipJ = json_object_get(rootJ, "applyClipping"); + if (clipJ) { + applyClipping = json_boolean_value(clipJ); + } + } +}; + + +struct AxBCWidget : ModuleWidget { + AxBCWidget(AxBC* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/AxBC.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParam(mm2px(Vec(5.327, 12.726)), module, AxBC::GAIN_B1_PARAM)); + addParam(createParamCentered(mm2px(Vec(19.875, 16.316)), module, AxBC::B1_PARAM)); + addParam(createParam(mm2px(Vec(20.93, 29.723)), module, AxBC::GAIN_C1_PARAM)); + addParam(createParamCentered(mm2px(Vec(9.898, 33.333)), module, AxBC::C1_PARAM)); + addParam(createParam(mm2px(Vec(5.327, 46.724)), module, AxBC::GAIN_B2_PARAM)); + addParam(createParamCentered(mm2px(Vec(19.875, 50.315)), module, AxBC::B2_PARAM)); + addParam(createParam(mm2px(Vec(20.93, 63.73)), module, AxBC::GAIN_C2_PARAM)); + addParam(createParamCentered(mm2px(Vec(9.898, 67.318)), module, AxBC::C2_PARAM)); + addParam(createParam(mm2px(Vec(3.471, 111.231)), module, AxBC::MODE_PARAM)); + + addInput(createInputCentered(mm2px(Vec(4.885, 84.785)), module, AxBC::A1_INPUT)); + addInput(createInputCentered(mm2px(Vec(14.885, 84.785)), module, AxBC::B1_INPUT)); + addInput(createInputCentered(mm2px(Vec(24.885, 84.785)), module, AxBC::C1_INPUT)); + addInput(createInputCentered(mm2px(Vec(4.885, 98.175)), module, AxBC::A2_INPUT)); + addInput(createInputCentered(mm2px(Vec(14.885, 98.175)), module, AxBC::B2_INPUT)); + addInput(createInputCentered(mm2px(Vec(24.862, 98.175)), module, AxBC::C2_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(14.907, 114.02)), module, AxBC::OUT_1_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(24.862, 114.02)), module, AxBC::OUT_2_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(12.04, 107.465)), module, AxBC::OUT_1_MINUS_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(17.758, 107.465)), module, AxBC::OUT_1_PLUS_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(21.996, 107.465)), module, AxBC::OUT_2_MINUS_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(27.681, 107.465)), module, AxBC::OUT_2_PLUS_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + AxBC* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createSubmenuItem("Hardware compatibility", "", + [ = ](Menu * menu) { + menu->addChild(createBoolPtrMenuItem("Clip outputs at ±10V", "", &module->applyClipping)); + })); + } +}; + + +Model* modelAxBC = createModel("AxBC"); \ No newline at end of file diff --git a/src/Burst.cpp b/src/Burst.cpp index 912609c..09a633b 100644 --- a/src/Burst.cpp +++ b/src/Burst.cpp @@ -198,6 +198,10 @@ struct Burst : Module { configInput(TIME_INPUT, "Time Division/Multiplication"); configInput(PROBABILITY_INPUT, "Probability"); configInput(TRIGGER_INPUT, "Trigger"); + + configOutput(TEMPO_OUTPUT, "Tempo"); + configOutput(EOC_OUTPUT, "End-of-cycle"); + configOutput(OUT_OUTPUT, "Burst"); ledUpdate.setDivision(ledUpdateRate); } diff --git a/src/DualAtenuverter.cpp b/src/DualAtenuverter.cpp index 46c8f77..1ab20ab 100644 --- a/src/DualAtenuverter.cpp +++ b/src/DualAtenuverter.cpp @@ -30,6 +30,12 @@ struct DualAtenuverter : Module { configParam(OFFSET1_PARAM, -10.0, 10.0, 0.0, "Ch 1 offset", " V"); configParam(ATEN2_PARAM, -1.0, 1.0, 0.0, "Ch 2 gain"); configParam(OFFSET2_PARAM, -10.0, 10.0, 0.0, "Ch 2 offset", " V"); + + configInput(IN1_INPUT, "In 1"); + configInput(IN2_INPUT, "In 2"); + configOutput(OUT1_OUTPUT, "Out 1"); + configOutput(OUT2_OUTPUT, "Out 2"); + configBypass(IN1_INPUT, OUT1_OUTPUT); configBypass(IN2_INPUT, OUT2_OUTPUT); } @@ -79,7 +85,7 @@ struct DualAtenuverter : Module { else { lights[OUT1_LIGHT + 0].setBrightness(0.0f); lights[OUT1_LIGHT + 1].setBrightness(0.0f); - lights[OUT1_LIGHT + 2].setBrightness(10.0f); + lights[OUT1_LIGHT + 2].setBrightness(1.0f); } if (channels2 == 1) { @@ -90,7 +96,7 @@ struct DualAtenuverter : Module { else { lights[OUT2_LIGHT + 0].setBrightness(0.0f); lights[OUT2_LIGHT + 1].setBrightness(0.0f); - lights[OUT2_LIGHT + 2].setBrightness(10.0f); + lights[OUT2_LIGHT + 2].setBrightness(1.0f); } } }; diff --git a/src/Mixer.cpp b/src/Mixer.cpp index 7b813fc..358afad 100644 --- a/src/Mixer.cpp +++ b/src/Mixer.cpp @@ -36,6 +36,10 @@ struct Mixer : Module { configParam(CH3_PARAM, 0.0, 1.0, 0.0, "Ch 3 level", "%", 0, 100); configParam(CH4_PARAM, 0.0, 1.0, 0.0, "Ch 4 level", "%", 0, 100); + configInput(IN1_INPUT, "Ch 1"); + configInput(IN2_INPUT, "Ch 2"); + configInput(IN3_INPUT, "Ch 3"); + configInput(IN4_INPUT, "Ch 4"); configOutput(OUT1_OUTPUT, "Main"); configOutput(OUT2_OUTPUT, "Inverted"); } diff --git a/src/Mixer2.cpp b/src/Mixer2.cpp new file mode 100644 index 0000000..730a90c --- /dev/null +++ b/src/Mixer2.cpp @@ -0,0 +1,175 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct Mixer2 : Module { + enum ParamId { + GAIN1_PARAM, + GAIN2_PARAM, + GAIN3_PARAM, + GAIN4_PARAM, + PARAMS_LEN + }; + enum InputId { + CH1_INPUT, + CH2_INPUT, + CH3_INPUT, + CH4_INPUT, + INPUTS_LEN + }; + enum OutputId { + MIX_12_OUPUT, + MIX_34_OUPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(MIX12_LIGHT, 3), + ENUMS(MIX34_LIGHT, 3), + LIGHTS_LEN + }; + + dsp::ClockDivider lightDivider; + bool applyClipping = false; + + Mixer2() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(GAIN1_PARAM, 0.f, 1.f, 1.f, "Gain 1"); + configParam(GAIN2_PARAM, 0.f, 1.f, 1.f, "Gain 2"); + configParam(GAIN3_PARAM, 0.f, 1.f, 1.f, "Gain 3"); + configParam(GAIN4_PARAM, 0.f, 1.f, 1.f, "Gain 4"); + + configInput(CH1_INPUT, "Channel 1"); + configInput(CH2_INPUT, "Channel 2"); + configInput(CH3_INPUT, "Channel 3"); + configInput(CH4_INPUT, "Channel 4"); + + configOutput(MIX_12_OUPUT, "Mix 1+2"); + configOutput(MIX_34_OUPUT, "Mix 3+4 (Master)"); + + lightDivider.setDivision(32); + } + + void process(const ProcessArgs& args) override { + const int numPolyphonyEngines = std::max({1, inputs[CH1_INPUT].getChannels(), inputs[CH2_INPUT].getChannels(), inputs[CH3_INPUT].getChannels(), inputs[CH4_INPUT].getChannels()}); + const bool useMasterMix = !outputs[MIX_12_OUPUT].isConnected(); + + // used for LEDs + float_4 sum12 = 0.f, sum34 = 0.f; + for (int c = 0; c < numPolyphonyEngines; c += 4) { + float_4 out12 = 0.f; + float_4 out34 = 0.f; + + if (inputs[CH1_INPUT].isConnected()) { + out12 += inputs[CH1_INPUT].getVoltageSimd(c) * params[GAIN1_PARAM].getValue(); + } + + if (inputs[CH2_INPUT].isConnected()) { + out12 += inputs[CH2_INPUT].getVoltageSimd(c) * params[GAIN2_PARAM].getValue(); + } + + if (inputs[CH3_INPUT].isConnected()) { + out34 += inputs[CH3_INPUT].getVoltageSimd(c) * params[GAIN3_PARAM].getValue(); + } + + if (inputs[CH4_INPUT].isConnected()) { + out34 += inputs[CH4_INPUT].getVoltageSimd(c) * params[GAIN4_PARAM].getValue(); + } + + const float_4 mix12 = useMasterMix ? float_4::zero() : out12; + const float_4 mix34 = useMasterMix ? out12 + out34 : out34; + + if (applyClipping) { + outputs[MIX_12_OUPUT].setVoltageSimd(clip(mix12), c); + outputs[MIX_34_OUPUT].setVoltageSimd(clip(mix34), c); + } + else { + outputs[MIX_12_OUPUT].setVoltageSimd(mix12, c); + outputs[MIX_34_OUPUT].setVoltageSimd(mix34, c); + } + + sum12 += simd::pow(out12, 2); + sum34 += simd::pow(out34, 2); + } + + outputs[MIX_12_OUPUT].setChannels(numPolyphonyEngines); + outputs[MIX_34_OUPUT].setChannels(numPolyphonyEngines); + + if (lightDivider.process()) { + const float deltaTime = args.sampleTime * lightDivider.getDivision(); + if (numPolyphonyEngines == 1) { + lights[MIX12_LIGHT + 0].setBrightnessSmooth(std::abs(sum12[0]) / 5.f, deltaTime); + lights[MIX12_LIGHT + 1].setBrightness(0.f); + lights[MIX12_LIGHT + 2].setBrightness(0.f); + lights[MIX34_LIGHT + 0].setBrightnessSmooth(std::abs(sum34[0]) / 5.f, deltaTime); + lights[MIX34_LIGHT + 1].setBrightness(0.f); + lights[MIX34_LIGHT + 2].setBrightness(0.f); + } + else { + // TODO: better polyphonic lights? + lights[MIX12_LIGHT + 0].setBrightness(0.f); + lights[MIX12_LIGHT + 1].setBrightness(0.f); + float light12 = std::sqrt((sum12[0] + sum12[1] + sum12[2] + sum12[3]) / numPolyphonyEngines) / 5.f; + lights[MIX12_LIGHT + 2].setBrightnessSmooth(light12, deltaTime); + + lights[MIX34_LIGHT + 0].setBrightness(0.f); + lights[MIX34_LIGHT + 1].setBrightness(0.f); + float light34 = std::sqrt((sum34[0] + sum34[1] + sum34[2] + sum34[3]) / numPolyphonyEngines) / 5.f; + lights[MIX34_LIGHT + 2].setBrightnessSmooth(light34, deltaTime); + } + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "applyClipping", json_boolean(applyClipping)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* applyClippingJ = json_object_get(rootJ, "applyClipping"); + if (applyClippingJ) { + applyClipping = json_boolean_value(applyClippingJ); + } + } +}; + + +struct Mixer2Widget : ModuleWidget { + Mixer2Widget(Mixer2* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Mixer2.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(10.0, 13.49)), module, Mixer2::GAIN1_PARAM)); + addParam(createParamCentered(mm2px(Vec(10.0, 33.6)), module, Mixer2::GAIN2_PARAM)); + addParam(createParamCentered(mm2px(Vec(10.0, 53.5)), module, Mixer2::GAIN3_PARAM)); + addParam(createParamCentered(mm2px(Vec(10.0, 73.3)), module, Mixer2::GAIN4_PARAM)); + + addInput(createInputCentered(mm2px(Vec(5.065, 88.898)), module, Mixer2::CH1_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.0, 88.9)), module, Mixer2::CH2_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 101.2)), module, Mixer2::CH3_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.065, 101.198)), module, Mixer2::CH4_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(5.0, 113.5)), module, Mixer2::MIX_12_OUPUT)); + addOutput(createOutputCentered(mm2px(Vec(15.0, 113.5)), module, Mixer2::MIX_34_OUPUT)); + + addChild(createLightCentered>(mm2px(Vec(2.5, 23.621)), module, Mixer2::MIX12_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(2.5, 63.4)), module, Mixer2::MIX34_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + Mixer2* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createSubmenuItem("Hardware compatibility", "", + [ = ](Menu * menu) { + menu->addChild(createBoolPtrMenuItem("Clip outputs at ±10V", "", &module->applyClipping)); + })); + } +}; + + +Model* modelMixer2 = createModel("Mixer2"); \ No newline at end of file diff --git a/src/Morphader.cpp b/src/Morphader.cpp index 608c1c4..2418c5d 100644 --- a/src/Morphader.cpp +++ b/src/Morphader.cpp @@ -105,6 +105,9 @@ struct Morphader : Module { configSwitch(MODE + i, AUDIO_MODE, CV_MODE, AUDIO_MODE, string::f("Mode %d", i + 1), {"Audio", "CV"}); configInput(CV_INPUT + i, string::f("CV channel %d", i + 1)); } + for (int i = 0; i < NUM_MIXER_CHANNELS; i++) { + configOutput(OUT + i, string::f("Channel %d", i + 1)); + } configParam(FADER_LAG_PARAM, 2.0f / slewMax, 2.0f / slewMin, 2.0f / slewMax, "Fader lag", "s"); configParam(FADER_PARAM, -1.f, 1.f, 0.f, "Fader"); diff --git a/src/MuDi.cpp b/src/MuDi.cpp new file mode 100644 index 0000000..8bf70fc --- /dev/null +++ b/src/MuDi.cpp @@ -0,0 +1,176 @@ +#include "plugin.hpp" + +using namespace simd; + +struct MuDi : Module { + enum ParamId { + PARAMS_LEN + }; + enum InputId { + CLOCK_INPUT, + RESET_INPUT, + INPUTS_LEN + }; + enum OutputId { + F_1_OUTPUT, + F_2_OUTPUT, + F_4_OUTPUT, + F_8_OUTPUT, + F_16_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(F_1_LIGHT, 3), + ENUMS(F_2_LIGHT, 3), + ENUMS(F_4_LIGHT, 3), + ENUMS(F_8_LIGHT, 3), + ENUMS(F_16_LIGHT, 3), + LIGHTS_LEN + }; + + dsp::TSchmittTrigger clockTrigger_1[4]; + dsp::TSchmittTrigger clockTrigger_2[4]; + dsp::TSchmittTrigger clockTrigger_4[4]; + dsp::TSchmittTrigger clockTrigger_8[4]; + float_4 clockState_1[4] = {}; + float_4 clockState_2[4] = {}; + float_4 clockState_4[4] = {}; + float_4 clockState_8[4] = {}; + float_4 clockState_16[4] = {}; + + dsp::TSchmittTrigger resetTrigger[4]; + dsp::ClockDivider lightDivider; + bool removeClockDC = false; + + MuDi() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configInput(CLOCK_INPUT, "Clock"); + configInput(RESET_INPUT, "Reset"); + + configOutput(F_1_OUTPUT, "F"); + configOutput(F_2_OUTPUT, "1/2 F"); + configOutput(F_4_OUTPUT, "1/4 F"); + configOutput(F_8_OUTPUT, "1/8 F"); + configOutput(F_16_OUTPUT, "1/16 F"); + + lightDivider.setDivision(32); + } + + void process(const ProcessArgs& args) override { + + const int numPolyphonyEngines = inputs[CLOCK_INPUT].getChannels(); + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + // reset + float_4 reset = resetTrigger[c / 4].process(inputs[RESET_INPUT].getPolyVoltageSimd(c)); + clockState_2[c / 4] = ifelse(reset, 0.f, clockState_2[c / 4]); + clockState_4[c / 4] = ifelse(reset, 0.f, clockState_4[c / 4]); + clockState_8[c / 4] = ifelse(reset, 0.f, clockState_8[c / 4]); + clockState_16[c / 4] = ifelse(reset, 0.f, clockState_16[c / 4]); + + // base derived clock + float_4 triggered = clockTrigger_1[c / 4].process(inputs[CLOCK_INPUT].getVoltageSimd(c)); + clockState_1[c / 4] = clockTrigger_1[c / 4].isHigh(); + + // 1/2 derived clock changes state on every rising edge of the base clock + clockState_2[c / 4] = ifelse(triggered, ~clockState_2[c / 4], clockState_2[c / 4]); + float_4 clockTriggered_2 = clockTrigger_2[c / 4].process(ifelse(clockState_2[c / 4], 10.f, 0.f)); + + // 1/4 derived clock changes state on every rising edge of the 1/2 derived clock + clockState_4[c / 4] = ifelse(clockTriggered_2, ~clockState_4[c / 4], clockState_4[c / 4]); + float_4 clockTriggered_4 = clockTrigger_4[c / 4].process(ifelse(clockState_4[c / 4], 10.f, 0.f)); + + // 1/8 derived clock changes state on every rising edge of the 1/4 derived clock + clockState_8[c / 4] = ifelse(clockTriggered_4, ~clockState_8[c / 4], clockState_8[c / 4]); + float_4 clockTriggered_8 = clockTrigger_8[c / 4].process(ifelse(clockState_8[c / 4], 10.f, 0.f)); + + // 1/16 derived clock changes state on every rising edge of the 1/8 derived clock + clockState_16[c / 4] = ifelse(clockTriggered_8, ~clockState_16[c / 4], clockState_16[c / 4]); + + // Set outputs + outputs[F_1_OUTPUT].setVoltageSimd(ifelse(clockState_1[c / 4], 10.f, 0.f) - 5.f * removeClockDC, c); + outputs[F_2_OUTPUT].setVoltageSimd(ifelse(clockState_2[c / 4], 10.f, 0.f) - 5.f * removeClockDC, c); + outputs[F_4_OUTPUT].setVoltageSimd(ifelse(clockState_4[c / 4], 10.f, 0.f) - 5.f * removeClockDC, c); + outputs[F_8_OUTPUT].setVoltageSimd(ifelse(clockState_8[c / 4], 10.f, 0.f) - 5.f * removeClockDC, c); + outputs[F_16_OUTPUT].setVoltageSimd(ifelse(clockState_16[c / 4], 10.f, 0.f) - 5.f * removeClockDC, c); + } + + outputs[F_1_OUTPUT].setChannels(numPolyphonyEngines); + outputs[F_2_OUTPUT].setChannels(numPolyphonyEngines); + outputs[F_4_OUTPUT].setChannels(numPolyphonyEngines); + outputs[F_8_OUTPUT].setChannels(numPolyphonyEngines); + outputs[F_16_OUTPUT].setChannels(numPolyphonyEngines); + + bool anyState[5] = {}; + for (int c = 0; c < numPolyphonyEngines; c++) { + anyState[0] |= ifelse(clockState_1[c / 4], 1.f, 0.f)[c % 4] > 0.f; + anyState[1] |= ifelse(clockState_2[c / 4], 1.f, 0.f)[c % 4] > 0.f; + anyState[2] |= ifelse(clockState_4[c / 4], 1.f, 0.f)[c % 4] > 0.f; + anyState[3] |= ifelse(clockState_8[c / 4], 1.f, 0.f)[c % 4] > 0.f; + anyState[4] |= ifelse(clockState_16[c / 4], 1.f, 0.f)[c % 4] > 0.f; + } + + // Set lights + if (lightDivider.process()) { + float lightTime = args.sampleTime * lightDivider.getDivision(); + + for (int i = 0; i < 5; i++) { + lights[F_1_LIGHT + 3 * i + 0].setBrightnessSmooth(anyState[i] && numPolyphonyEngines == 1, lightTime); + lights[F_1_LIGHT + 3 * i + 1].setBrightness(0.f); + lights[F_1_LIGHT + 3 * i + 2].setBrightnessSmooth(anyState[i] && numPolyphonyEngines > 1, lightTime); + } + } + } + + void dataFromJson(json_t* rootJ) override { + json_t* removeClockDCJ = json_object_get(rootJ, "removeClockDC"); + if (removeClockDCJ) + removeClockDC = json_boolean_value(removeClockDCJ); + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "removeClockDC", json_boolean(removeClockDC)); + return rootJ; + } +}; + + +struct MuDiWidget : ModuleWidget { + MuDiWidget(MuDi* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MuDi.svg"))); + + addChild(createWidget(Vec(box.size.x - RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addInput(createInputCentered(mm2px(Vec(5.0, 15.138)), module, MuDi::CLOCK_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.0, 30.245)), module, MuDi::RESET_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(5.0, 56.695)), module, MuDi::F_1_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(5.0, 70.45)), module, MuDi::F_2_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(5.0, 84.204)), module, MuDi::F_4_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(5.0, 97.959)), module, MuDi::F_8_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(5.0, 111.713)), module, MuDi::F_16_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(1.95, 62.74)), module, MuDi::F_1_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(1.95, 76.325)), module, MuDi::F_2_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(1.95, 90.1)), module, MuDi::F_4_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(1.95, 103.874)), module, MuDi::F_8_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(1.95, 117.648)), module, MuDi::F_16_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + MuDi* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createSubmenuItem("Hardware compatibility", "", + [ = ](Menu * menu) { + menu->addChild(createBoolPtrMenuItem("Remove DC from clock outs", "", &module->removeClockDC)); + })); + } +}; + + +Model* modelMuDi = createModel("MuDi"); \ No newline at end of file diff --git a/src/Muxlicer.cpp b/src/Muxlicer.cpp index cafa397..0345836 100644 --- a/src/Muxlicer.cpp +++ b/src/Muxlicer.cpp @@ -177,8 +177,8 @@ struct MultDivClock { static const std::vector clockOptionsQuadratic = {-16, -8, -4, -2, 1, 2, 4, 8, 16}; static const std::vector clockOptionsAll = {-16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, 1, - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 - }; + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 +}; inline std::string getClockOptionString(const int clockOption) { return (clockOption < 0) ? ("x 1/" + std::to_string(-clockOption)) : ("x " + std::to_string(clockOption)); @@ -887,7 +887,7 @@ struct MuxlicerWidget : ModuleWidget { for (int clockOption : module->quadraticGatesOnly ? clockOptionsQuadratic : clockOptionsAll) { std::string optionString = getClockOptionString(clockOption); OutputClockScalingChildItem* clockItem = createMenuItem(optionString, - CHECKMARK(module->outputClockMultDiv.multDiv == clockOption)); + CHECKMARK(module->outputClockMultDiv.multDiv == clockOption)); clockItem->clockOutMulDiv = clockOption; clockItem->module = module; menu->addChild(clockItem); @@ -1079,6 +1079,9 @@ struct Mex : Module { for (int i = 0; i < 8; ++i) { configSwitch(STEP_PARAM + i, 0.f, 2.f, 0.f, string::f("Step %d", i + 1), {"Gate in/Clock Out", "Muted", "All Gates"}); } + + configInput(GATE_IN_INPUT, "Gate"); + configOutput(OUT_OUTPUT, "Gate"); } Muxlicer* findHostModulePtr(Module* module) { diff --git a/src/Percall.cpp b/src/Percall.cpp index 86c1e56..ba51231 100644 --- a/src/Percall.cpp +++ b/src/Percall.cpp @@ -47,6 +47,7 @@ struct Percall : Module { configInput(CH_INPUTS + i, string::f("Channel %d", i + 1)); configInput(TRIG_INPUTS + i, string::f("Channel %d trigger", i + 1)); configInput(CV_INPUTS + i, string::f("Channel %d CV", i + 1)); + configOutput(CH_OUTPUTS + i, string::f("Channel %d", i + 1)); configOutput(ENV_OUTPUTS + i, string::f("Channel %d envelope", i + 1)); envs[i].attackTime = attackTime; diff --git a/src/SamplingModulator.cpp b/src/SamplingModulator.cpp index 7875b9a..b28bfdb 100644 --- a/src/SamplingModulator.cpp +++ b/src/SamplingModulator.cpp @@ -64,6 +64,14 @@ struct SamplingModulator : Module { configParam(FINE_PARAM, 0.f, 1.f, 0.f, "Fine tune"); configSwitch(INT_EXT_PARAM, 0.f, 1.f, CLOCK_INTERNAL, "Clock", {"External", "Internal"}); + configInput(SYNC_INPUT, "Sync"); + configInput(VOCT_INPUT, "V/Oct"); + configInput(HOLD_INPUT, "Hold"); + configInput(IN_INPUT, "Raw"); + configOutput(CLOCK_OUTPUT, "Clock"); + configOutput(TRIGG_OUTPUT, "Trigger"); + configOutput(OUT_OUTPUT, "Sampled"); + for (int i = 0; i < numSteps; i++) { configSwitch(STEP_PARAM + i, 0.f, 2.f, STATE_ON, string::f("Step %d", i + 1), {"Reset", "Off", "On"}); } diff --git a/src/Slew.cpp b/src/Slew.cpp new file mode 100644 index 0000000..191dab1 --- /dev/null +++ b/src/Slew.cpp @@ -0,0 +1,181 @@ +#include "plugin.hpp" + +using simd::float_4; + +struct Slew : Module { + enum ParamId { + SHAPE_PARAM, + RANGE_PARAM, + RISE_PARAM, + FALL_PARAM, + CV_MODE_PARAM, + PARAMS_LEN + }; + enum InputId { + IN_INPUT, + CV_INPUT, + INPUTS_LEN + }; + enum OutputId { + OUT_OUTPUT, + RISING_OUTPUT, + FALLING_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(FALLING_LIGHT, 3), + ENUMS(RISING_LIGHT, 3), + LIGHTS_LEN + }; + enum CvMode { + CV_MODE_FALL, + CV_MODE_RISE_FALL, + CV_MODE_RISE + }; + + float_4 out[4] = {}; + dsp::ClockDivider lightDivider; + + + Slew() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Shape"); + configSwitch(RANGE_PARAM, 0.f, 2.f, 1.f, "Range", {"Fast", "Medium", "Slow"}); + auto rise = configParam(RISE_PARAM, 0.f, 1.f, 0.f, "Rise"); + rise->description = "Sets the RISE slew time manually, higher is longer slew time.\n" + "Acts as an attenuator of CV in when CV sent to rise."; + auto fall = configParam(FALL_PARAM, 0.f, 1.f, 0.f, "Fall"); + fall->description = "Sets the FALL slew time manually, higher is longer slew time.\n" + "Acts as an attenuator of CV in when CV sent to fall."; + configSwitch(CV_MODE_PARAM, 0.f, 2.f, 1.f, "", {"Fall", "Rise/Fall", "Rise"}); + configInput(IN_INPUT, "In"); + auto cvIn = configInput(CV_INPUT, "CV"); + cvIn->description = "CV input for slew time, 0V to 10V, attenuated by relevant sliders."; + configOutput(OUT_OUTPUT, "Out"); + configOutput(RISING_OUTPUT, "Rising"); + configOutput(FALLING_OUTPUT, "Falling"); + + lightDivider.setDivision(32); + } + + // slew times: + // range slow: 4ms to 4s + // range mid: 40ms to (30) 40s + // range fast: 400ms to 400s + void process(const ProcessArgs& args) override { + + float_4 in[4] = {}; + float_4 riseCV[4] = {}; + float_4 fallCV[4] = {}; + float_4 delta[4] = {}; + + // this is the number of active polyphony engines, defined by the input + const int numPolyphonyEngines = std::max(1, inputs[IN_INPUT].getChannels()); + + // minimum and maximum slopes in volts per second + const int range = (int) params[RANGE_PARAM].getValue(); + const float slewMin = 10 / (4 * pow(10.f, range)); + const float slewMax = 10 / (0.004 * pow(10.f, range)); + // Amount of extra slew per voltage difference + const float shapeScale = 1 / 10.f; + + const float_4 param_rise = params[RISE_PARAM].getValue() * 10.f; + const float_4 param_fall = params[FALL_PARAM].getValue() * 10.f; + const CvMode cvMode = (CvMode)(params[CV_MODE_PARAM].getValue()); + + outputs[OUT_OUTPUT].setChannels(numPolyphonyEngines); + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + in[c / 4] = inputs[IN_INPUT].getVoltageSimd(c); + + if (inputs[CV_INPUT].isConnected() && (cvMode == CV_MODE_RISE_FALL || cvMode == CV_MODE_RISE)) { + riseCV[c / 4] = simd::clamp(inputs[CV_INPUT].getPolyVoltageSimd(c), 0.f, 10.f) * params[RISE_PARAM].getValue(); + } + else { + riseCV[c / 4] = param_rise; + + } + if (inputs[CV_INPUT].isConnected() && (cvMode == CV_MODE_RISE_FALL || cvMode == CV_MODE_FALL)) { + fallCV[c / 4] = simd::clamp(inputs[CV_INPUT].getPolyVoltageSimd(c), 0.f, 10.f) * params[FALL_PARAM].getValue(); + } + else { + fallCV[c / 4] = param_fall; + } + + delta[c / 4] = in[c / 4] - out[c / 4]; + float_4 delta_gt_0 = delta[c / 4] > 0.f; + float_4 delta_lt_0 = delta[c / 4] < 0.f; + + float_4 rateCV = {}; + rateCV = ifelse(delta_gt_0, riseCV[c / 4], 0.f); + rateCV = ifelse(delta_lt_0, fallCV[c / 4], rateCV) * 0.1f; + + float_4 pm_one = simd::sgn(delta[c / 4]); + float_4 slew = slewMax * simd::pow(slewMin / slewMax, rateCV); + + const float shape = params[SHAPE_PARAM].getValue(); + out[c / 4] += slew * simd::crossfade(pm_one, shapeScale * delta[c / 4], shape) * args.sampleTime; + out[c / 4] = ifelse(delta_gt_0 & (out[c / 4] > in[c / 4]), in[c / 4], out[c / 4]); + out[c / 4] = ifelse(delta_lt_0 & (out[c / 4] < in[c / 4]), in[c / 4], out[c / 4]); + + outputs[OUT_OUTPUT].setVoltageSimd(out[c / 4], c); + } + + if (lightDivider.process()) { + const float deltaTime = args.sampleTime * lightDivider.getDivision(); + + if (numPolyphonyEngines == 1) { + lights[RISING_LIGHT + 0].setSmoothBrightness(delta[0][0] > 0 ? 1.f : 0.f, deltaTime); + lights[RISING_LIGHT + 1].setBrightness(0.f); + lights[RISING_LIGHT + 2].setBrightness(0.f); + + lights[FALLING_LIGHT + 0].setSmoothBrightness(delta[0][0] < 0.f ? 1.f : 0.f, deltaTime); + lights[FALLING_LIGHT + 1].setBrightness(0.f); + lights[FALLING_LIGHT + 2].setBrightness(0.f); + } + else { + bool anyRising = false, anyFalling = false; + for (int c = 0; c < numPolyphonyEngines; c++) { + anyRising |= out[c / 4][c % 4] < in[c / 4][c % 4]; + anyFalling |= out[c / 4][c % 4] > in[c / 4][c % 4]; + } + lights[RISING_LIGHT + 0].setBrightness(0.f); + lights[RISING_LIGHT + 1].setBrightness(0.f); + lights[RISING_LIGHT + 2].setSmoothBrightness(anyRising ? 1.f : 0.f, deltaTime); + + lights[FALLING_LIGHT + 0].setBrightness(0.f); + lights[FALLING_LIGHT + 1].setBrightness(0.f); + lights[FALLING_LIGHT + 2].setSmoothBrightness(anyFalling ? 1.f : 0.f, deltaTime); + } + } + } +}; + + +struct SlewWidget : ModuleWidget { + SlewWidget(Slew* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Slew.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(9.835, 30.246)), module, Slew::SHAPE_PARAM)); + addParam(createParam(mm2px(Vec(5.407, 38.103)), module, Slew::RANGE_PARAM)); + addParam(createParam(mm2px(Vec(2.381, 48.289)), module, Slew::RISE_PARAM)); + addParam(createParam(mm2px(Vec(12.7, 48.289)), module, Slew::FALL_PARAM)); + addParam(createParam(mm2px(Vec(13.351, 108.638)), module, Slew::CV_MODE_PARAM)); + + addInput(createInputCentered(mm2px(Vec(4.978, 15.465)), module, Slew::IN_INPUT)); + addInput(createInputCentered(mm2px(Vec(4.978, 112.232)), module, Slew::CV_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(14.843, 15.487)), module, Slew::OUT_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(4.978, 99.399)), module, Slew::RISING_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(15.07, 99.399)), module, Slew::FALLING_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(15.12, 90.397)), module, Slew::FALLING_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(4.978, 90.999)), module, Slew::RISING_LIGHT)); + } +}; + +Model* modelSlew = createModel("Slew"); \ No newline at end of file diff --git a/src/SlewLimiter.cpp b/src/SlewLimiter.cpp index f6c5e08..434183e 100644 --- a/src/SlewLimiter.cpp +++ b/src/SlewLimiter.cpp @@ -31,6 +31,9 @@ struct SlewLimiter : Module { configInput(RISE_INPUT, "Rise CV"); configInput(FALL_INPUT, "Fall CV"); + configInput(IN_INPUT, "In"); + + configOutput(OUT_OUTPUT, "Out"); } void process(const ProcessArgs& args) override { diff --git a/src/SpringReverb.cpp b/src/SpringReverb.cpp index 812ee68..58fb766 100644 --- a/src/SpringReverb.cpp +++ b/src/SpringReverb.cpp @@ -66,6 +66,15 @@ struct SpringReverb : Module { configParam(LEVEL2_PARAM, 0.0, 1.0, 0.0, "In 2 level", "%", 0, 100); configParam(HPF_PARAM, 0.0, 1.0, 0.5, "High pass filter cutoff"); + configInput(CV1_INPUT, "CV 1"); + configInput(CV2_INPUT, "CV 2"); + configInput(IN1_INPUT, "In 1"); + configInput(IN2_INPUT, "In 2"); + configInput(MIX_CV_INPUT, "Mix CV"); + + configOutput(MIX_OUTPUT, "Mix"); + configOutput(WET_OUTPUT, "Wet"); + initIR(); convolver = new dsp::RealTimeConvolver(BLOCK_SIZE); diff --git a/src/StereoStrip.cpp b/src/StereoStrip.cpp index 0c24b15..1e2a89d 100644 --- a/src/StereoStrip.cpp +++ b/src/StereoStrip.cpp @@ -269,11 +269,14 @@ struct StereoStrip : Module { void onSampleRateChange() override { bool forceUpdate = true; updateEQsIfChanged(forceUpdate); + + // at low sample rates (e.g. 24kHz), shelf filter is at Nyquist! + const float shelfSampleRate = std::min(0.4f * APP->engine->getSampleRate(), 12000.0f); for (int side = 0; side < 2; ++side) { for (int c = 0; c < 16; c += 4) { - highpass[side][c / 4].setCutoff(25.0f, 0.8f, AeFilterType::AeHIGHPASS); - highshelf[side][c / 4].setParams(12000.0f, 0.8f, -5.0f, AeEQType::AeHIGHSHELVE); + highpass[side][c / 4].setCutoff(25.0f, 0.8f, AeFilterType::AeHIGHPASS); + highshelf[side][c / 4].setParams(shelfSampleRate, 0.8f, -5.0f, AeEQType::AeHIGHSHELVE); } } } diff --git a/src/Voltio.cpp b/src/Voltio.cpp index bba5c39..469e246 100644 --- a/src/Voltio.cpp +++ b/src/Voltio.cpp @@ -33,7 +33,7 @@ struct Voltio : Module { semitonesParam->snapEnabled = true; configInput(SUM_INPUT, "Sum"); - configOutput(OUT_OUTPUT, ""); + configOutput(OUT_OUTPUT, "Main"); } void process(const ProcessArgs& args) override { diff --git a/src/plugin.cpp b/src/plugin.cpp index 1b48a6e..154a390 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -33,4 +33,9 @@ void init(rack::Plugin *p) { p->addModel(modelOctaves); p->addModel(modelBypass); p->addModel(modelBandit); + p->addModel(modelMixer2); + p->addModel(modelAtte); + p->addModel(modelAxBC); + p->addModel(modelSlew); + p->addModel(modelMuDi); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 70ad26f..5a21ff0 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -34,6 +34,11 @@ extern Model* modelVoltio; extern Model* modelOctaves; extern Model* modelBypass; extern Model* modelBandit; +extern Model* modelMixer2; +extern Model* modelAtte; +extern Model* modelAxBC; +extern Model* modelSlew; +extern Model* modelMuDi; struct Knurlie : SvgScrew { Knurlie() { @@ -273,6 +278,15 @@ T exponentialBipolar80Pade_5_4(T x) { / (T(1.) - T(0.630374) * simd::pow(x, 2) + T(0.166271) * simd::pow(x, 4)); } +template +static T clip(T x) { + // Pade approximant of x/(1 + x^12)^(1/12) + const T limit = 1.16691853009184f; + x = clamp(x * 0.1f, -limit, limit); + return 10.0f * (x + 1.45833f * simd::pow(x, 13) + 0.559028f * simd::pow(x, 25) + 0.0427035f * simd::pow(x, 37)) + / (1.0f + 1.54167f * simd::pow(x, 12) + 0.642361f * simd::pow(x, 24) + 0.0579909f * simd::pow(x, 36)); +} + struct ADEnvelope { enum Stage { STAGE_OFF,