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