|
|
@@ -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"); |