Browse Source

Add StereoStrip

* addressing feedback from Andrew
* add optional saturation to outputs
* added linear clipped panning law
tags/v2.2.0^2
hemmer 3 years ago
parent
commit
15f183e7f7
9 changed files with 2201 additions and 32 deletions
  1. +3
    -1
      README.md
  2. +13
    -1
      plugin.json
  3. +36
    -0
      res/components/BefacoSlidePotHandleSmall.svg
  4. +51
    -0
      res/components/BefacoSlidePotSmall.svg
  5. +1508
    -0
      res/panels/StereoStrip.svg
  6. +2
    -30
      src/NoisePlethora.cpp
  7. +549
    -0
      src/StereoStrip.cpp
  8. +1
    -0
      src/plugin.cpp
  9. +38
    -0
      src/plugin.hpp

+ 3
- 1
README.md View File

@@ -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.
* 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.

+ 13
- 1
plugin.json View File

@@ -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,18 @@
"Hardware clone",
"Noise"
]
},
{
"slug": "StereoStrip",
"name": "Stereo Strip",
"description": "Stereo VCA, panning, and EQ",
"tags": [
"Equalizer",
"Hardware clone",
"Mixer",
"Panning",
"Polyphonic"
]
}
]
}

+ 36
- 0
res/components/BefacoSlidePotHandleSmall.svg View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="11.7px" height="19.27px" viewBox="0 0 11.7 19.27" enable-background="new 0 0 11.7 19.27" xml:space="preserve">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5.85" y1="0.00002" x2="5.85" y2="19.26998">
<stop offset="0" style="stop-color:#C40D0D"/>
<stop offset="1" style="stop-color:#730000"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M5.99089,0.00002c-4.54181,0-5.99048,4.39062-5.99048,9.72565
c0,5.42615,1.62805,9.56366,6.08649,9.54425c4.35376-0.01935,5.54059-4.25189,5.61041-9.63348V9.63356
C11.76616,4.30532,10.27874,0.00002,5.99089,0.00002z"/>
<linearGradient id="SVGID_00000005981792839563570410000005624999559760348816_" gradientUnits="userSpaceOnUse" x1="5.85" y1="0.31135" x2="5.85" y2="18.60765">
<stop offset="0" style="stop-color:#ED0000"/>
<stop offset="1" style="stop-color:#A10000"/>
</linearGradient>
<path fill="url(#SVGID_00000005981792839563570410000005624999559760348816_)" d="M5.98196,0.31135
c-4.25385,0-5.61068,4.16878-5.61068,9.23423c0,5.15198,1.52483,9.08043,5.7006,9.062
c4.07773-0.01837,5.18931-4.03705,5.25471-9.14672V9.45813C11.39107,4.39911,9.99795,0.31135,5.98196,0.31135z"/>
<linearGradient id="SVGID_00000166637737070165468880000001400135779107130813_" gradientUnits="userSpaceOnUse" x1="51.79285" y1="150.06172" x2="51.79285" y2="156.50661" gradientTransform="matrix(0 -1 -0.50871 0 87.35653 61.24197)">
<stop offset="0" style="stop-color:#FDFDFF"/>
<stop offset="1" style="stop-color:#FCFEFF;stop-opacity:0"/>
</linearGradient>
<ellipse opacity="0.29" fill="url(#SVGID_00000166637737070165468880000001400135779107130813_)" cx="9.37883" cy="9.44912" rx="1.6393" ry="5.64087"/>
<linearGradient id="SVGID_00000065062046531620287420000014186053846948254592_" gradientUnits="userSpaceOnUse" x1="103.38602" y1="-136.56932" x2="103.38602" y2="-130.93109" gradientTransform="matrix(0 1 0.50871 0 70.1237 -93.8822)">
<stop offset="0" style="stop-color:#FDFDFF"/>
<stop offset="1" style="stop-color:#FCFEFF;stop-opacity:0"/>
</linearGradient>
<ellipse opacity="0.29" fill="url(#SVGID_00000065062046531620287420000014186053846948254592_)" cx="2.08318" cy="9.50382" rx="1.43412" ry="5.64087"/>
</g>
</svg>

+ 51
- 0
res/components/BefacoSlidePotSmall.svg View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
version="1.0"
id="svg1579"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
x="0px"
y="0px"
width="2.2731254mm"
height="26.011mm"
viewBox="0 0 8.5913398 98.309291"
enable-background="new 0 0 8.59132 104"
xml:space="preserve"
sodipodi:docname="BefacoSlidePotSmall.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1115" />
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1.0"
fit-margin-bottom="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-top="0"
id="base"
inkscape:current-layer="svg1579"
inkscape:cx="-2.8571429"
inkscape:cy="35.625"
inkscape:document-units="mm"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="1081"
inkscape:window-maximized="0"
inkscape:window-width="1296"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:zoom="5.6"
pagecolor="#ffffff"
showgrid="false"
units="mm"
inkscape:pagecheckerboard="0">
</sodipodi:namedview>
<path
d="M 4.29567,98.309289 C 2.2706741,98.309289 0,95.683298 0,92.443966 V 5.8653233 C 0,2.6259913 1.92323,0 4.29566,0 h 1e-5 C 6.6681,0 8.59134,2.6259913 8.59134,5.8653233 V 92.443966 c 0,3.239332 -2.2706741,5.865323 -4.29567,5.865323 z"
id="path1112"
sodipodi:nodetypes="zssssssz"
style="stroke-width:1.16851" />
</svg>

+ 1508
- 0
res/panels/StereoStrip.svg
File diff suppressed because it is too large
View File


+ 2
- 30
src/NoisePlethora.cpp View File

@@ -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<float>::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<float>::process(out + 0.33);

if (blockDC) {
// cascaded Biquad (4th order highpass at ~20Hz)


+ 549
- 0
src/StereoStrip.cpp View File

@@ -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 <typename T>
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 <typename T>
struct AeFilterStereo : AeFilter<T> {
T xl[2] = {};
T xr[2] = {};
T yl[2] = {};
T yr[2] = {};

void process(T* inL, T* inR) {
T l = AeFilter<T>::b0 * *inL + AeFilter<T>::b1 * xl[0] + AeFilter<T>::b2 * xl[1] - AeFilter<T>::a1 * yl[0] - AeFilter<T>::a2 * yl[1];
T r = AeFilter<T>::b0 * *inR + AeFilter<T>::b1 * xr[0] + AeFilter<T>::b2 * xr[1] - AeFilter<T>::a1 * yr[0] - AeFilter<T>::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 <typename T>
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 <typename T>
struct AeEqualizerStereo : AeEqualizer<T> {
T xl[2] = {};
T xr[2] = {};
T yl[2] = {};
T yr[2] = {};

void process(T* inL, T* inR) {
T l = AeEqualizer<T>::b0 * *inL + AeEqualizer<T>::b1 * xl[0] + AeEqualizer<T>::b2 * xl[1] - AeEqualizer<T>::a1 * yl[0] - AeEqualizer<T>::a2 * yl[1];
T r = AeEqualizer<T>::b0 * *inR + AeEqualizer<T>::b1 * xr[0] + AeEqualizer<T>::b2 * xr[1] - AeEqualizer<T>::a1 * yr[0] - AeEqualizer<T>::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<float_4> eqLow[4][2];
AeEqualizer<float_4> eqMid[4][2];
AeEqualizer<float_4> eqHigh[4][2];

bool applyHighpass = true;
AeFilter<float_4> highpass[4][2];
bool applyHighshelf = true;
AeEqualizer<float_4> 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<float_4>(10.f, c) / 10.f, 0.f, 1.f);

const float_4 panCV = clamp(params[PAN_CV_PARAM].getValue() * inputs[PAN_INPUT].getPolyVoltageSimd<float_4>(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<float_4>(c);
in[c / 4][RIGHT] = inputs[RIGHT_INPUT].getNormalPolyVoltageSimd<float_4>(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<float_4>::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<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.763, 35.805)), module, StereoStrip::LOW_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.817, 35.805)), module, StereoStrip::MID_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(22.861, 35.805)), module, StereoStrip::HIGH_PARAM));
addParam(createParamCentered<Davies1900hDarkGreyKnob>(mm2px(Vec(15.042, 74.11)), module, StereoStrip::PAN_PARAM));
addParam(createParamCentered<ThreeStateBefacoSwitchMomentary>(mm2px(Vec(7.416, 91.244)), module, StereoStrip::MUTE_PARAM));
addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(22.842, 91.244)), module, StereoStrip::PAN_CV_PARAM));
addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(15.054, 111.333)), module, StereoStrip::LEVEL_PARAM));
addParam(createParam<CKSSNarrow>(mm2px(Vec(2.372, 72.298)), module, StereoStrip::IN_BOOST_PARAM));
addParam(createParam<CKSSNarrow>(mm2px(Vec(24.253, 72.298)), module, StereoStrip::OUT_CUT_PARAM));


addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 14.852)), module, StereoStrip::LEFT_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.023, 14.852)), module, StereoStrip::LEVEL_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 26.304)), module, StereoStrip::RIGHT_INPUT));
addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.023, 26.304)), module, StereoStrip::PAN_INPUT));

addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.069, 14.882)), module, StereoStrip::LEFT_OUTPUT));
addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.069, 26.317)), module, StereoStrip::RIGHT_OUTPUT));

addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(4.05, 69.906)), module, StereoStrip::LEFT_LIGHT));
addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(26.05, 69.906)), module, StereoStrip::RIGHT_LIGHT));
}

void appendContextMenu(Menu* menu) override {
StereoStrip* module = dynamic_cast<StereoStrip*>(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, StereoStripWidget>("StereoStrip");

+ 1
- 0
src/plugin.cpp View File

@@ -24,4 +24,5 @@ void init(rack::Plugin *p) {
p->addModel(modelMuxlicer);
p->addModel(modelMex);
p->addModel(modelNoisePlethora);
p->addModel(modelChannelStrip);
}

+ 38
- 0
src/plugin.hpp View File

@@ -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 <class T>
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));
}
};

Loading…
Cancel
Save