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 @@
+
+
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 @@
+
+
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 @@
+
+
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,