From 79a8a1f47ebcd0fff82062f4e1b62c2ae95de1b0 Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:55:46 +0000 Subject: [PATCH 1/5] Update Oneiroi.md to add changelog --- docs/Oneiroi.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Oneiroi.md b/docs/Oneiroi.md index 999ef51..2144b3b 100644 --- a/docs/Oneiroi.md +++ b/docs/Oneiroi.md @@ -14,3 +14,9 @@ Based on [Befaco Oneiroi](http://www.befaco.org/oneiroi) Eurorack module. For th * As yet, slew of parameter values on randomize is not supported ![Oneiroi](img/Oneiroi.png) + + +## Changelog + +### v2.0.0 + * Initial release From 92373bf9efb1c5d642330fa16920dc139f6da94b Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:30:39 +0000 Subject: [PATCH 2/5] Update Oneiroi.md --- docs/Oneiroi.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Oneiroi.md b/docs/Oneiroi.md index 2144b3b..50eb16f 100644 --- a/docs/Oneiroi.md +++ b/docs/Oneiroi.md @@ -2,6 +2,10 @@ Based on [Befaco Oneiroi](http://www.befaco.org/oneiroi) Eurorack module. For the official manual, see [here](https://befaco.org/docs/Oneiroi/Oneiroi_User_Manual.pdf). +## Changelog + +### v2.0.0 + * Initial release ## Differences with hardware @@ -16,7 +20,3 @@ Based on [Befaco Oneiroi](http://www.befaco.org/oneiroi) Eurorack module. For th ![Oneiroi](img/Oneiroi.png) -## Changelog - -### v2.0.0 - * Initial release From 44a898c3fd934586d74c2dcc6fbead05835bf71f Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:22:04 +0000 Subject: [PATCH 3/5] Update Oneiroi changelog --- docs/Oneiroi.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/Oneiroi.md b/docs/Oneiroi.md index 50eb16f..491df55 100644 --- a/docs/Oneiroi.md +++ b/docs/Oneiroi.md @@ -4,6 +4,11 @@ Based on [Befaco Oneiroi](http://www.befaco.org/oneiroi) Eurorack module. For th ## Changelog +### v2.0.1 + * Fixed issues where looper buffer wasn't restored on Windows + * Add option to never randomise pitch + * Random button led brightness now reflects length of the slewing applied + ### v2.0.0 * Initial release From 5914634cd91cf11cc32394616252beb19c5c934c Mon Sep 17 00:00:00 2001 From: Ewan <915048+hemmer@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:31:38 +0000 Subject: [PATCH 4/5] v2.9.0 (#61) * Add Mixer2, AxBC, MuDi, Atte, Slew * Fix StereoStrip issues at very low sample rates --- CHANGELOG.md | 5 + plugin.json | 61 +- res/panels/Atte.svg | 610 +++++++++++++++++++ res/panels/AxBC.svg | 1173 +++++++++++++++++++++++++++++++++++++ res/panels/Mixer2.svg | 723 +++++++++++++++++++++++ res/panels/MuDi.svg | 576 ++++++++++++++++++ res/panels/Slew.svg | 674 +++++++++++++++++++++ src/ABC.cpp | 13 +- src/Atte.cpp | 188 ++++++ src/AxBC.cpp | 233 ++++++++ src/Burst.cpp | 4 + src/DualAtenuverter.cpp | 10 +- src/Mixer.cpp | 4 + src/Mixer2.cpp | 175 ++++++ src/Morphader.cpp | 3 + src/MuDi.cpp | 176 ++++++ src/Muxlicer.cpp | 9 +- src/Percall.cpp | 1 + src/SamplingModulator.cpp | 8 + src/Slew.cpp | 181 ++++++ src/SlewLimiter.cpp | 3 + src/SpringReverb.cpp | 9 + src/StereoStrip.cpp | 7 +- src/Voltio.cpp | 2 +- src/plugin.cpp | 5 + src/plugin.hpp | 14 + 26 files changed, 4847 insertions(+), 20 deletions(-) create mode 100644 res/panels/Atte.svg create mode 100644 res/panels/AxBC.svg create mode 100644 res/panels/Mixer2.svg create mode 100644 res/panels/MuDi.svg create mode 100644 res/panels/Slew.svg create mode 100644 src/Atte.cpp create mode 100644 src/AxBC.cpp create mode 100644 src/Mixer2.cpp create mode 100644 src/MuDi.cpp create mode 100644 src/Slew.cpp 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, From c5a1791e6c9935bcb6dc2e389558fd2e213166e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Wed, 12 Mar 2025 11:09:31 -0300 Subject: [PATCH 5/5] Fixed duplicated word plus minor typos (#62) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2caa49b..956d9e6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ We have tried to make the VCV implementations as authentic as possible, however * The hardware version of Morphader accepts 0-8V CV for the crossfade control, here we widen this to accept 0-10V. -* Chopping Kinky hardward is DC coupled, but we add the option (default disabled) to remove this offset. +* Chopping Kinky hardware is DC coupled, but we add the option (default disabled) to remove this offset. * See [docs/Muxlicer.md](docs/Muxlicer.md) @@ -28,6 +28,6 @@ We have tried to make the VCV implementations as authentic as possible, however * to limit the pulsewidth from 5% to 95% (hardware is full range) * to remove DC from the pulse waveform output (hardware contains DC for non-50% duty cycles) -* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid acidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop. +* MotionMTR optionally doesn't use the 10V normalling on inputs if in audio mode to avoid accidentally adding unwanted DC to audio signals, see context menu. E.g. if you temporarily unpatch an audio source whilst using it it mixer mode, you get 10V DC suddenly and a nasty pop. -* Burst hardware version version can also set the tempo by tapping the encoder, this is not possible in the VCV version. \ No newline at end of file +* Burst hardware version can also set the tempo by tapping the encoder, this is not possible in the VCV version.