diff --git a/CHANGELOG.md b/CHANGELOG.md index 378b0f2..ce59d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v2.2.0 + + * StereoStrip + * Initial release + ## v2.1.1 * Noise Plethora * Grit quantity knob behaviour updated to match production hardware version diff --git a/README.md b/README.md index 875d962..f66949a 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,6 @@ We have tried to make the VCV implementations as authentic as possible, however * Chopping Kinky hardward is DC coupled, but we add the option (default disabled) to remove this offset. -* The hardware Muxlicer assigns multiple functions to the "Speed Div/Mult" dial, that cannot be reproduced with a single mouse click. Some of these have been moved to the context menu, specifically: quadratic gates, the "All In" normalled voltage, and the input/output clock division/mult. The "Speed Div/Mult" dial remains only for main clock div/mult. \ No newline at end of file +* The hardware Muxlicer assigns multiple functions to the "Speed Div/Mult" dial, that cannot be reproduced with a single mouse click. Some of these have been moved to the context menu, specifically: quadratic gates, the "All In" normalled voltage, and the input/output clock division/mult. The "Speed Div/Mult" dial remains only for main clock div/mult. + +* The Noise Plethora filters self-oscillate on the hardware version but not the software version. \ No newline at end of file diff --git a/plugin.json b/plugin.json index 439c0ac..5019dd9 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "2.1.1", + "version": "2.2.0", "license": "GPL-3.0-or-later", "name": "Befaco", "brand": "Befaco", @@ -241,6 +241,19 @@ "Hardware clone", "Noise" ] + }, + { + "slug": "StereoStrip", + "name": "Stereo Strip", + "description": "Stereo VCA, panning, and EQ", + "tags": [ + "Equalizer", + "Hardware clone", + "Mixer", + "Limiter", + "Panning", + "Polyphonic" + ] } ] } \ No newline at end of file diff --git a/res/components/BefacoSlidePotHandleSmall.svg b/res/components/BefacoSlidePotHandleSmall.svg new file mode 100644 index 0000000..968288a --- /dev/null +++ b/res/components/BefacoSlidePotHandleSmall.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/components/BefacoSlidePotSmall.svg b/res/components/BefacoSlidePotSmall.svg new file mode 100644 index 0000000..b79358b --- /dev/null +++ b/res/components/BefacoSlidePotSmall.svg @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/res/panels/StereoStrip.svg b/res/panels/StereoStrip.svg new file mode 100644 index 0000000..d927422 --- /dev/null +++ b/res/panels/StereoStrip.svg @@ -0,0 +1,1508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NoisePlethora.cpp b/src/NoisePlethora.cpp index 7b1d3bc..e1f408c 100644 --- a/src/NoisePlethora.cpp +++ b/src/NoisePlethora.cpp @@ -10,34 +10,6 @@ enum FilterMode { }; -// Zavalishin 2018, "The Art of VA Filter Design", http://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.0.0a.pdf -// Section 6.7, adopted from BogAudio Saturator https://github.com/bogaudio/BogaudioModules/blob/master/src/dsp/signal.cpp -struct Saturator { - - // saturate input at around ~[-1, +1] with soft clipping - static float process(float sample) { - - if (sample < 0.0f) { - return -saturation(-sample); - } - return saturation(sample); - } -private: - - static float saturation(float sample) { - - const float limit = 1.05f; - const float y1 = 0.98765f; // (2*x - 1)/x**2 where x is 0.9. - // correction so that saturation(0) = 0 - const float offset = 0.0062522; // -0.5f + sqrtf(0.5f * 0.5f) / y1; - - float x = sample / limit; - float x1 = (x + 1.0f) * 0.5f; - - return limit * (offset + x1 - std::sqrt(x1 * x1 - y1 * x) * (1.0f / y1)); - } -}; - // based on Chapter 4 of THE ART OF VA FILTER DESIGN and // Chap 12.4 of "Designing Audio Effect Plugins in C++" Will Pirkle class StateVariableFilter2ndOrder { @@ -392,7 +364,7 @@ struct NoisePlethora : Module { } } - outputs[OUTPUT].setVoltage(Saturator::process(out) * 5.f); + outputs[OUTPUT].setVoltage(Saturator::process(out) * 5.f); } // process section C @@ -423,7 +395,7 @@ struct NoisePlethora : Module { out = svfFilterC.process(toFilter, mode); // assymetric saturator, to get those lovely even harmonics - out = Saturator::process(out + 0.33); + out = Saturator::process(out + 0.33); if (blockDC) { // cascaded Biquad (4th order highpass at ~20Hz) diff --git a/src/StereoStrip.cpp b/src/StereoStrip.cpp new file mode 100644 index 0000000..9a3c780 --- /dev/null +++ b/src/StereoStrip.cpp @@ -0,0 +1,549 @@ +#include "plugin.hpp" + +using simd::float_4; + +// from https://github.com/wiqid/repelzen/blob/master/src/aefilter.hpp (29307df4fd3e713d206f2155dcff0337fc067f1f) +// with permission (GPL-3.0-or-later) +enum AeFilterType { + AeLOWPASS, + AeHIGHPASS +}; + +enum AeEQType { + AeLOWSHELVE, + AeHIGHSHELVE, + AePEAKINGEQ +}; + +template +struct AeFilter { + T x[2] = {}; + T y[2] = {}; + + float a0, a1, a2, b0, b1, b2; + + inline T process(const T& in) noexcept { + T out = b0 * in + b1 * x[0] + b2 * x[1] - a1 * y[0] - a2 * y[1]; + + //shift buffers + x[1] = x[0]; + x[0] = in; + y[1] = y[0]; + y[0] = out; + + return out; + } + + void setCutoff(float f, float q, int type) { + const float w0 = 2 * M_PI * f / APP->engine->getSampleRate(); + const float alpha = std::sin(w0) / (2.0f * q); + const float cs0 = std::cos(w0); + + switch (type) { + case AeLOWPASS: + a0 = 1 + alpha; + b0 = (1 - cs0) / 2 / a0; + b1 = (1 - cs0) / a0; + b2 = (1 - cs0) / 2 / a0; + a1 = (-2 * cs0) / a0; + a2 = (1 - alpha) / a0; + break; + case AeHIGHPASS: + a0 = 1 + alpha; + b0 = (1 + cs0) / 2 / a0; + b1 = -(1 + cs0) / a0; + b2 = (1 + cs0) / 2 / a0; + a1 = -2 * cs0 / a0; + a2 = (1 - alpha) / a0; + } + } +}; + +template +struct AeFilterStereo : AeFilter { + T xl[2] = {}; + T xr[2] = {}; + T yl[2] = {}; + T yr[2] = {}; + + void process(T* inL, T* inR) { + T l = AeFilter::b0 * *inL + AeFilter::b1 * xl[0] + AeFilter::b2 * xl[1] - AeFilter::a1 * yl[0] - AeFilter::a2 * yl[1]; + T r = AeFilter::b0 * *inR + AeFilter::b1 * xr[0] + AeFilter::b2 * xr[1] - AeFilter::a1 * yr[0] - AeFilter::a2 * yr[1]; + + //shift buffers + xl[1] = xl[0]; + xl[0] = *inL; + xr[1] = xr[0]; + xr[0] = *inR; + + yl[1] = yl[0]; + yl[0] = l; + yr[1] = yr[0]; + yr[0] = r; + + *inL = l; + *inR = r; + } +}; + +template +struct AeEqualizer { + T x[2] = {}; + T y[2] = {}; + + float a0, a1, a2, b0, b1, b2; + + T process(T in) { + T out = b0 * in + b1 * x[0] + b2 * x[1] - a1 * y[0] - a2 * y[1]; + //shift buffers + x[1] = x[0]; + x[0] = in; + y[1] = y[0]; + y[0] = out; + return out; + } + + void setParams(float f, float q, float gaindb, AeEQType type) { + + const float w0 = 2 * M_PI * f / APP->engine->getSampleRate(); + const float alpha = sin(w0) / (2.0f * q); + const float cs0 = cos(w0); + const float A = pow(10, gaindb / 40.0f); + + switch (type) { + case AeLOWSHELVE: + a0 = (A + 1.0f) + (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha; + b0 = A * ((A + 1.0f) - (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha) / a0; + b1 = 2.0f * A * ((A - 1.0f) - (A + 1.0f) * cs0) / a0; + b2 = A * ((A + 1.0f) - (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0; + a1 = -2.0f * ((A - 1.0f) + (A + 1.0f) * cs0) / a0; + a2 = ((A + 1.0f) + (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0; + break; + case AeHIGHSHELVE: + a0 = (A + 1.0f) - (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha; + b0 = A * ((A + 1.0f) + (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha) / a0; + b1 = -2.0f * A * ((A - 1.0f) + (A + 1.0f) * cs0) / a0; + b2 = A * ((A + 1.0f) + (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0; + a1 = 2.0f * ((A - 1.0f) - (A + 1.0f) * cs0) / a0; + a2 = ((A + 1.0f) - (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0; + break; + case AePEAKINGEQ: + a0 = 1.0f + alpha / A; + b0 = (1.0f + alpha * A) / a0; + b1 = -2.0f * cs0 / a0; + b2 = (1.0f - alpha * A) / a0; + a1 = -2.0f * cs0 / a0; + a2 = (1.0f - alpha / A) / a0; + } + } +}; + +template +struct AeEqualizerStereo : AeEqualizer { + T xl[2] = {}; + T xr[2] = {}; + T yl[2] = {}; + T yr[2] = {}; + + void process(T* inL, T* inR) { + T l = AeEqualizer::b0 * *inL + AeEqualizer::b1 * xl[0] + AeEqualizer::b2 * xl[1] - AeEqualizer::a1 * yl[0] - AeEqualizer::a2 * yl[1]; + T r = AeEqualizer::b0 * *inR + AeEqualizer::b1 * xr[0] + AeEqualizer::b2 * xr[1] - AeEqualizer::a1 * yr[0] - AeEqualizer::a2 * yr[1]; + + // shift buffers + xl[1] = xl[0]; + xl[0] = *inL; + xr[1] = xr[0]; + xr[0] = *inR; + + yl[1] = yl[0]; + yl[0] = l; + yr[1] = yr[0]; + yr[0] = r; + + *inL = l; + *inR = r; + } +}; + +struct StereoStrip : Module { + enum ParamId { + LOW_PARAM, + MID_PARAM, + HIGH_PARAM, + PAN_PARAM, + MUTE_PARAM, + PAN_CV_PARAM, + LEVEL_PARAM, + IN_BOOST_PARAM, + OUT_CUT_PARAM, + PARAMS_LEN + }; + enum InputId { + LEFT_INPUT, + LEVEL_INPUT, + RIGHT_INPUT, + PAN_INPUT, + INPUTS_LEN + }; + enum OutputId { + LEFT_OUTPUT, + RIGHT_OUTPUT, + OUTPUTS_LEN + }; + enum LightId { + ENUMS(LEFT_LIGHT, 3), + ENUMS(RIGHT_LIGHT, 3), + LIGHTS_LEN + }; + enum MuteStates { + MUTE_OFF_MOMENTARY = -1, + MUTE_ON, + MUTE_OFF + }; + enum MixerSides { + LEFT, + RIGHT + }; + enum PanningLaw { + LINEAR_6dB, + EQUAL_POWER, + LINEAR_CLIPPED + }; + + PanningLaw panningLaw = LINEAR_6dB; + + AeEqualizer eqLow[4][2]; + AeEqualizer eqMid[4][2]; + AeEqualizer eqHigh[4][2]; + + bool applyHighpass = true; + AeFilter highpass[4][2]; + bool applyHighshelf = true; + AeEqualizer highshelf[4][2]; + bool applySoftClipping = true; + + float lastLowGain = -INFINITY; + float lastMidGain = -INFINITY; + float lastHighGain = -INFINITY; + + // for processing mutes + dsp::SlewLimiter clickFilter; + + StereoStrip() { + config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); + configParam(HIGH_PARAM, -15.0f, 15.0f, 0.0f, "High shelf (2000 Hz) gain", " dB"); + configParam(MID_PARAM, -12.5f, 12.5f, 0.0f, "Mid band (1200 Hz) gain", " dB"); + configParam(LOW_PARAM, -20.0f, 20.0f, 0.0f, "Low shelf (125 Hz) gain", " dB"); + configParam(PAN_PARAM, -1.f, 1.f, 0.0f, "Pan"); + configSwitch(MUTE_PARAM, MUTE_OFF_MOMENTARY, MUTE_OFF, MUTE_OFF, "Mute", {"Off (momentary)", "On", "Off"}); + configParam(PAN_CV_PARAM, 0.f, 1.f, 0.f, "Pan CV"); + configParam(LEVEL_PARAM, -60.0f, 0.0f, -60.0f, "Gain", "dB"); + configSwitch(IN_BOOST_PARAM, 0, 1, 0, "In boost", {"0dB", "+6dB"}); + configSwitch(OUT_CUT_PARAM, 0, 1, 0, "Out cut", {"0dB", "-6dB"}); + + configInput(LEFT_INPUT, "Left"); + configInput(LEVEL_INPUT, "Level (10 V normalled)"); + configInput(RIGHT_INPUT, "Right (left normalled)"); + configInput(PAN_INPUT, "Pan CV (-5 V to +5 V)"); + + configOutput(LEFT_OUTPUT, "Left"); + configOutput(RIGHT_OUTPUT, "Right"); + + configLight(LEFT_LIGHT, "Left"); + configLight(RIGHT_LIGHT, "Right"); + + configBypass(LEFT_INPUT, LEFT_OUTPUT); + configBypass(RIGHT_INPUT, RIGHT_OUTPUT); + + onSampleRateChange(); + + clickFilter.rise = 50.f; // Hz + clickFilter.fall = 50.f; // Hz + } + + void onSampleRateChange() override { + bool forceUpdate = true; + updateEQsIfChanged(forceUpdate); + + 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); + } + } + } + + void updateEQsIfChanged(bool forceUpdate = false) { + float highGain = params[HIGH_PARAM].getValue(); + float midGain = params[MID_PARAM].getValue(); + float lowGain = params[LOW_PARAM].getValue(); + + // only calculate coefficients when neccessary + if (highGain != lastHighGain || forceUpdate) { + for (int c = 0; c < 16; c += 4) { + for (int side = 0; side < 2; ++side) { + eqHigh[c / 4][side].setParams(2000.0f, 0.4f, highGain, AeEQType::AeHIGHSHELVE); + } + } + lastHighGain = highGain; + } + + if (midGain != lastMidGain || forceUpdate) { + for (int c = 0; c < 16; c += 4) { + for (int side = 0; side < 2; ++side) { + eqMid[c / 4][side].setParams(1200.0f, 0.52f, midGain, AeEQType::AePEAKINGEQ); + } + } + lastMidGain = midGain; + } + + if (lowGain != lastLowGain || forceUpdate) { + for (int c = 0; c < 16; c += 4) { + for (int side = 0; side < 2; ++side) { + eqLow[c / 4][side].setParams(125.0f, 0.45f, lowGain, AeEQType::AeLOWSHELVE); + } + } + lastLowGain = lowGain; + } + } + + void process(const ProcessArgs& args) override { + + float_4 out[4][2] = {}, in[4][2] = {}; + + const int numPolyphonyEngines = std::max(inputs[LEFT_INPUT].getChannels(), inputs[RIGHT_INPUT].getChannels()); + + // slew mute to avoid clicks + const float muteGain = clickFilter.process(args.sampleTime, params[MUTE_PARAM].getValue() != MUTE_ON); + + if (inputs[LEFT_INPUT].isConnected() || inputs[RIGHT_INPUT].isConnected()) { + + const float switchGains = (params[IN_BOOST_PARAM].getValue() ? 2.0f : 1.0f) * (params[OUT_CUT_PARAM].getValue() ? 0.5f : 1.0f); + const float preVCAGain = switchGains * muteGain * std::pow(10, params[LEVEL_PARAM].getValue() / 20.0f); + + updateEQsIfChanged(); + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + + const float_4 postVCAGain = preVCAGain * clamp(inputs[LEVEL_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f); + + const float_4 panCV = clamp(params[PAN_CV_PARAM].getValue() * inputs[PAN_INPUT].getPolyVoltageSimd(c) / 5.f, -1.f, +1.f); + const float_4 pan = clamp(params[PAN_PARAM].getValue() + panCV, -1.f, +1.f); + + // https://www.desmos.com/calculator/b0lisclikw + float_4 gainForSide[2] = {}; + switch (panningLaw) { + case LINEAR_6dB: { + gainForSide[0] = postVCAGain * (1.f - pan); + gainForSide[1] = postVCAGain * (1.f + pan); + break; + } + case EQUAL_POWER: { + gainForSide[0] = postVCAGain * simd::sqrt(1.f - pan); + gainForSide[1] = postVCAGain * simd::sqrt(1.f + pan); + break; + } + case LINEAR_CLIPPED: { + gainForSide[0] = simd::ifelse(pan < 0, postVCAGain, postVCAGain * (1.f - pan)); + gainForSide[1] = simd::ifelse(pan > 0, postVCAGain, postVCAGain * (1.f + pan)); + break; + } + } + + in[c / 4][LEFT] = inputs[LEFT_INPUT].getPolyVoltageSimd(c); + in[c / 4][RIGHT] = inputs[RIGHT_INPUT].getNormalPolyVoltageSimd(in[c / 4][LEFT], c); + + for (int side = 0; side < 2; ++side) { + + float_4 outForSide = in[c / 4][side]; + + outForSide = eqLow[c / 4][side].process(outForSide); + outForSide = eqMid[c / 4][side].process(outForSide); + outForSide = eqHigh[c / 4][side].process(outForSide); + outForSide = applyHighpass ? highpass[c / 4][side].process(outForSide) : outForSide; + outForSide = applyHighshelf ? highshelf[c / 4][side].process(outForSide) : outForSide; + outForSide = outForSide * gainForSide[side]; + + // soft clipping: the Saturator used elsewhere expects values in range [-1, +1] roughly, so rescale before + // and after (assuming input signals are 10Vpp, clipping will kick in above 12Vpp with the present values) + if (applySoftClipping) { + outForSide = Saturator::process(outForSide / 6.f) * 6.f; + } + + out[c / 4][side] = outForSide; + } + } + } + + if (numPolyphonyEngines <= 1) { + lights[LEFT_LIGHT + 0].setBrightness(0.f); + lights[RIGHT_LIGHT + 0].setBrightness(0.f); + lights[LEFT_LIGHT + 1].setBrightnessSmooth(std::abs(out[0][LEFT][0]), args.sampleTime); + lights[RIGHT_LIGHT + 1].setBrightnessSmooth(std::abs(out[0][RIGHT][0]), args.sampleTime); + lights[LEFT_LIGHT + 2].setBrightness(0.f); + lights[RIGHT_LIGHT + 2].setBrightness(0.f); + } + else { + lights[LEFT_LIGHT + 0].setBrightness(0.f); + lights[RIGHT_LIGHT + 0].setBrightness(0.f); + lights[LEFT_LIGHT + 1].setBrightness(0.f); + lights[RIGHT_LIGHT + 1].setBrightness(0.f); + lights[LEFT_LIGHT + 2].setBrightness(1.f); + lights[RIGHT_LIGHT + 2].setBrightness(1.f); + } + + for (int c = 0; c < numPolyphonyEngines; c += 4) { + outputs[LEFT_OUTPUT].setVoltageSimd(out[c / 4][LEFT], c); + outputs[RIGHT_OUTPUT].setVoltageSimd(out[c / 4][RIGHT], c); + } + + outputs[LEFT_OUTPUT].setChannels(numPolyphonyEngines); + outputs[RIGHT_OUTPUT].setChannels(numPolyphonyEngines); + + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + json_object_set_new(rootJ, "applyHighpass", json_boolean(applyHighpass)); + json_object_set_new(rootJ, "applyHighshelf", json_boolean(applyHighshelf)); + json_object_set_new(rootJ, "panningLaw", json_integer(panningLaw)); + json_object_set_new(rootJ, "applySoftClipping", json_boolean(applySoftClipping)); + + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* applyHighshelfJ = json_object_get(rootJ, "applyHighshelf"); + if (applyHighshelfJ) { + applyHighshelf = json_boolean_value(applyHighshelfJ); + } + + json_t* applyHighpassJ = json_object_get(rootJ, "applyHighpass"); + if (applyHighpassJ) { + applyHighpass = json_boolean_value(applyHighpassJ); + } + + json_t* panningLawJ = json_object_get(rootJ, "panningLaw"); + if (panningLawJ) { + panningLaw = (PanningLaw) json_integer_value(panningLawJ); + } + + json_t* softClippingJ = json_object_get(rootJ, "applySoftClipping"); + if (softClippingJ) { + applySoftClipping = json_boolean_value(softClippingJ); + } + } +}; + +// an implementation of a performable, 3-stage switch, where the bottom state is Momentary +struct ThreeStateBefacoSwitchMomentary : SvgSwitch { + ThreeStateBefacoSwitchMomentary() { + momentary = true; + addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_0.svg"))); + addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_1.svg"))); + addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_2.svg"))); + } + + void onDragStart(const event::DragStart& e) override { + if (e.button == GLFW_MOUSE_BUTTON_LEFT) { + latched = false; + pos = Vec(0, 0); + } + ParamWidget::onDragStart(e); + } + + void onDragMove(const event::DragMove& e) override { + if (e.button == GLFW_MOUSE_BUTTON_LEFT) { + pos += e.mouseDelta; + + // Once the user has dragged the mouse a "threshold" distance, latch + // to disallow further changes of state until the mouse is released. + // We don't just setValue(1) (default/rest state) because this creates a + // jarring UI experience + if (pos.y < -10 && !latched) { + getParamQuantity()->setValue(StereoStrip::MUTE_OFF); + latched = true; + } + if (pos.y > 10 && !latched) { + getParamQuantity()->setValue(StereoStrip::MUTE_OFF_MOMENTARY); + latched = true; + } + } + ParamWidget::onDragMove(e); + } + + void onDragEnd(const event::DragEnd& e) override { + if (e.button == GLFW_MOUSE_BUTTON_LEFT) { + + // not dragged == clicked + if (std::sqrt(pos.square()) < 5) { + // if muted, unmute + if (getParamQuantity()->getValue() == StereoStrip::MUTE_ON) { + getParamQuantity()->setValue(StereoStrip::MUTE_OFF); + } + // if ummuted, mute + else if (getParamQuantity()->getValue() == StereoStrip::MUTE_OFF) { + getParamQuantity()->setValue(StereoStrip::MUTE_ON); + } + } + + // on release, the switch resets to default/neutral/middle position, if was previously down + if (getParamQuantity()->getValue() == StereoStrip::MUTE_OFF_MOMENTARY) { + getParamQuantity()->setValue(StereoStrip::MUTE_ON); + } + latched = false; + } + ParamWidget::onDragEnd(e); + } + + Vec pos; + + bool latched = false; +}; + +struct StereoStripWidget : ModuleWidget { + StereoStripWidget(StereoStrip* module) { + setModule(module); + setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/StereoStrip.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParam(mm2px(Vec(2.763, 35.805)), module, StereoStrip::LOW_PARAM)); + addParam(createParam(mm2px(Vec(12.817, 35.805)), module, StereoStrip::MID_PARAM)); + addParam(createParam(mm2px(Vec(22.861, 35.805)), module, StereoStrip::HIGH_PARAM)); + addParam(createParamCentered(mm2px(Vec(15.042, 74.11)), module, StereoStrip::PAN_PARAM)); + addParam(createParamCentered(mm2px(Vec(7.416, 91.244)), module, StereoStrip::MUTE_PARAM)); + addParam(createParamCentered(mm2px(Vec(22.842, 91.244)), module, StereoStrip::PAN_CV_PARAM)); + addParam(createParamCentered(mm2px(Vec(15.054, 111.333)), module, StereoStrip::LEVEL_PARAM)); + addParam(createParam(mm2px(Vec(2.372, 72.298)), module, StereoStrip::IN_BOOST_PARAM)); + addParam(createParam(mm2px(Vec(24.253, 72.298)), module, StereoStrip::OUT_CUT_PARAM)); + + + addInput(createInputCentered(mm2px(Vec(5.038, 14.852)), module, StereoStrip::LEFT_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.023, 14.852)), module, StereoStrip::LEVEL_INPUT)); + addInput(createInputCentered(mm2px(Vec(5.038, 26.304)), module, StereoStrip::RIGHT_INPUT)); + addInput(createInputCentered(mm2px(Vec(15.023, 26.304)), module, StereoStrip::PAN_INPUT)); + + addOutput(createOutputCentered(mm2px(Vec(25.069, 14.882)), module, StereoStrip::LEFT_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(25.069, 26.317)), module, StereoStrip::RIGHT_OUTPUT)); + + addChild(createLightCentered>(mm2px(Vec(4.05, 69.906)), module, StereoStrip::LEFT_LIGHT)); + addChild(createLightCentered>(mm2px(Vec(26.05, 69.906)), module, StereoStrip::RIGHT_LIGHT)); + } + + void appendContextMenu(Menu* menu) override { + StereoStrip* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator()); + menu->addChild(createBoolPtrMenuItem("Apply Highpass (25Hz)", "", &module->applyHighpass)); + menu->addChild(createBoolPtrMenuItem("Apply Highshelf (12kHz)", "", &module->applyHighshelf)); + menu->addChild(createBoolPtrMenuItem("Apply soft-clipping", "", &module->applySoftClipping)); + menu->addChild(new MenuSeparator()); + menu->addChild(createIndexPtrSubmenuItem("Panning law", {"Linear (+6dB)", "Equal power (+3dB)", "Linear clipped"}, &module->panningLaw)); + } +}; + + +Model* modelChannelStrip = createModel("StereoStrip"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 03f4dba..95321b0 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -24,4 +24,5 @@ void init(rack::Plugin *p) { p->addModel(modelMuxlicer); p->addModel(modelMex); p->addModel(modelNoisePlethora); + p->addModel(modelChannelStrip); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 50dac69..648f9e3 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -25,6 +25,7 @@ extern Model* modelSTMix; extern Model* modelMuxlicer; extern Model* modelMex; extern Model* modelNoisePlethora; +extern Model* modelChannelStrip; struct Knurlie : SvgScrew { Knurlie() { @@ -153,6 +154,18 @@ struct Davies1900hLargeLightGreyKnob : Davies1900hKnob { } }; +struct BefacoSlidePotSmall : app::SvgSlider { + BefacoSlidePotSmall() { + math::Vec margin = math::Vec(3.5, 3.5); + maxHandlePos = math::Vec(-2, -2).plus(margin); + minHandlePos = math::Vec(-2, 60).plus(margin); + setBackgroundSvg(Svg::load(asset::plugin(pluginInstance, "res/components/BefacoSlidePotSmall.svg"))); + setHandleSvg(Svg::load(asset::plugin(pluginInstance, "res/components/BefacoSlidePotHandleSmall.svg"))); + background->box.pos = margin; + box.size = background->box.size.plus(margin.mult(2)); + } +}; + inline int unsigned_modulo(int a, int b) { return ((a % b) + b) % b; } @@ -291,4 +304,29 @@ struct PulseGenerator_4 { // Keep the previous pulse if the existing pulse will be held longer than the currently requested one. remaining = ifelse(mask & (duration > remaining), duration, remaining); } +}; + +// Zavalishin 2018, "The Art of VA Filter Design", http://www.native-instruments.com/fileadmin/ni_media/downloads/pdf/VAFilterDesign_2.0.0a.pdf +// Section 6.7, adopted from BogAudio Saturator https://github.com/bogaudio/BogaudioModules/blob/master/src/dsp/signal.cpp +template +struct Saturator { + + // saturate input at around ~[-1, +1] V with soft clipping + static T process(T sample) { + return simd::ifelse(sample < 0.f, -saturation(-sample), saturation(sample)); + } +private: + + static T saturation(T sample) { + + const float limit = 1.05f; + const float y1 = 0.98765f; // (2*x - 1)/x**2 where x is 0.9. + // correction so that saturation(0) = 0 + const float offset = 0.0062522; // -0.5f + sqrtf(0.5f * 0.5f) / y1; + + T x = sample / limit; + T x1 = (x + 1.0f) * 0.5f; + + return limit * (offset + x1 - simd::sqrt(x1 * x1 - y1 * x) * (1.0f / y1)); + } }; \ No newline at end of file