You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

556 lines
17KB

  1. #include "plugin.hpp"
  2. using simd::float_4;
  3. // from https://github.com/wiqid/repelzen/blob/master/src/aefilter.hpp (29307df4fd3e713d206f2155dcff0337fc067f1f)
  4. // with permission (GPL-3.0-or-later)
  5. enum AeFilterType {
  6. AeLOWPASS,
  7. AeHIGHPASS
  8. };
  9. enum AeEQType {
  10. AeLOWSHELVE,
  11. AeHIGHSHELVE,
  12. AePEAKINGEQ
  13. };
  14. template <typename T>
  15. struct AeFilter {
  16. T x[2] = {};
  17. T y[2] = {};
  18. float a0, a1, a2, b0, b1, b2;
  19. inline T process(const T& in) noexcept {
  20. T out = b0 * in + b1 * x[0] + b2 * x[1] - a1 * y[0] - a2 * y[1];
  21. //shift buffers
  22. x[1] = x[0];
  23. x[0] = in;
  24. y[1] = y[0];
  25. y[0] = out;
  26. return out;
  27. }
  28. void setCutoff(float f, float q, int type) {
  29. const float w0 = 2 * M_PI * f / APP->engine->getSampleRate();
  30. const float alpha = std::sin(w0) / (2.0f * q);
  31. const float cs0 = std::cos(w0);
  32. switch (type) {
  33. case AeLOWPASS:
  34. a0 = 1 + alpha;
  35. b0 = (1 - cs0) / 2 / a0;
  36. b1 = (1 - cs0) / a0;
  37. b2 = (1 - cs0) / 2 / a0;
  38. a1 = (-2 * cs0) / a0;
  39. a2 = (1 - alpha) / a0;
  40. break;
  41. case AeHIGHPASS:
  42. a0 = 1 + alpha;
  43. b0 = (1 + cs0) / 2 / a0;
  44. b1 = -(1 + cs0) / a0;
  45. b2 = (1 + cs0) / 2 / a0;
  46. a1 = -2 * cs0 / a0;
  47. a2 = (1 - alpha) / a0;
  48. }
  49. }
  50. };
  51. template <typename T>
  52. struct AeFilterStereo : AeFilter<T> {
  53. T xl[2] = {};
  54. T xr[2] = {};
  55. T yl[2] = {};
  56. T yr[2] = {};
  57. void process(T* inL, T* inR) {
  58. 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];
  59. 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];
  60. //shift buffers
  61. xl[1] = xl[0];
  62. xl[0] = *inL;
  63. xr[1] = xr[0];
  64. xr[0] = *inR;
  65. yl[1] = yl[0];
  66. yl[0] = l;
  67. yr[1] = yr[0];
  68. yr[0] = r;
  69. *inL = l;
  70. *inR = r;
  71. }
  72. };
  73. template <typename T>
  74. struct AeEqualizer {
  75. T x[2] = {};
  76. T y[2] = {};
  77. float a0, a1, a2, b0, b1, b2;
  78. T process(T in) {
  79. T out = b0 * in + b1 * x[0] + b2 * x[1] - a1 * y[0] - a2 * y[1];
  80. //shift buffers
  81. x[1] = x[0];
  82. x[0] = in;
  83. y[1] = y[0];
  84. y[0] = out;
  85. return out;
  86. }
  87. void setParams(float f, float q, float gaindb, AeEQType type) {
  88. const float w0 = 2 * M_PI * f / APP->engine->getSampleRate();
  89. const float alpha = sin(w0) / (2.0f * q);
  90. const float cs0 = cos(w0);
  91. const float A = pow(10, gaindb / 40.0f);
  92. switch (type) {
  93. case AeLOWSHELVE:
  94. a0 = (A + 1.0f) + (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha;
  95. b0 = A * ((A + 1.0f) - (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha) / a0;
  96. b1 = 2.0f * A * ((A - 1.0f) - (A + 1.0f) * cs0) / a0;
  97. b2 = A * ((A + 1.0f) - (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0;
  98. a1 = -2.0f * ((A - 1.0f) + (A + 1.0f) * cs0) / a0;
  99. a2 = ((A + 1.0f) + (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0;
  100. break;
  101. case AeHIGHSHELVE:
  102. a0 = (A + 1.0f) - (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha;
  103. b0 = A * ((A + 1.0f) + (A - 1.0f) * cs0 + 2 * sqrt(A) * alpha) / a0;
  104. b1 = -2.0f * A * ((A - 1.0f) + (A + 1.0f) * cs0) / a0;
  105. b2 = A * ((A + 1.0f) + (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0;
  106. a1 = 2.0f * ((A - 1.0f) - (A + 1.0f) * cs0) / a0;
  107. a2 = ((A + 1.0f) - (A - 1.0f) * cs0 - 2.0f * sqrt(A) * alpha) / a0;
  108. break;
  109. case AePEAKINGEQ:
  110. a0 = 1.0f + alpha / A;
  111. b0 = (1.0f + alpha * A) / a0;
  112. b1 = -2.0f * cs0 / a0;
  113. b2 = (1.0f - alpha * A) / a0;
  114. a1 = -2.0f * cs0 / a0;
  115. a2 = (1.0f - alpha / A) / a0;
  116. }
  117. }
  118. };
  119. template <typename T>
  120. struct AeEqualizerStereo : AeEqualizer<T> {
  121. T xl[2] = {};
  122. T xr[2] = {};
  123. T yl[2] = {};
  124. T yr[2] = {};
  125. void process(T* inL, T* inR) {
  126. 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];
  127. 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];
  128. // shift buffers
  129. xl[1] = xl[0];
  130. xl[0] = *inL;
  131. xr[1] = xr[0];
  132. xr[0] = *inR;
  133. yl[1] = yl[0];
  134. yl[0] = l;
  135. yr[1] = yr[0];
  136. yr[0] = r;
  137. *inL = l;
  138. *inR = r;
  139. }
  140. };
  141. struct StereoStrip : Module {
  142. enum ParamId {
  143. LOW_PARAM,
  144. MID_PARAM,
  145. HIGH_PARAM,
  146. PAN_PARAM,
  147. MUTE_PARAM,
  148. PAN_CV_PARAM,
  149. LEVEL_PARAM,
  150. IN_BOOST_PARAM,
  151. OUT_CUT_PARAM,
  152. PARAMS_LEN
  153. };
  154. enum InputId {
  155. LEFT_INPUT,
  156. LEVEL_INPUT,
  157. RIGHT_INPUT,
  158. PAN_INPUT,
  159. INPUTS_LEN
  160. };
  161. enum OutputId {
  162. LEFT_OUTPUT,
  163. RIGHT_OUTPUT,
  164. OUTPUTS_LEN
  165. };
  166. enum LightId {
  167. ENUMS(LEFT_LIGHT, 3),
  168. ENUMS(RIGHT_LIGHT, 3),
  169. LIGHTS_LEN
  170. };
  171. enum MuteStates {
  172. MUTE_OFF_MOMENTARY = -1,
  173. MUTE_ON,
  174. MUTE_OFF
  175. };
  176. enum MixerSides {
  177. LEFT,
  178. RIGHT
  179. };
  180. enum PanningLaw {
  181. LINEAR_6dB,
  182. EQUAL_POWER,
  183. LINEAR_CLIPPED
  184. };
  185. PanningLaw panningLaw = LINEAR_6dB;
  186. AeEqualizer<float_4> eqLow[4][2];
  187. AeEqualizer<float_4> eqMid[4][2];
  188. AeEqualizer<float_4> eqHigh[4][2];
  189. bool applyHighpass = true;
  190. AeFilter<float_4> highpass[4][2];
  191. bool applyHighshelf = true;
  192. AeEqualizer<float_4> highshelf[4][2];
  193. bool applySoftClipping = true;
  194. float lastLowGain = -INFINITY;
  195. float lastMidGain = -INFINITY;
  196. float lastHighGain = -INFINITY;
  197. // for processing mutes
  198. dsp::SlewLimiter clickFilter;
  199. dsp::ClockDivider sliderUpdate;
  200. StereoStrip() {
  201. config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
  202. configParam(HIGH_PARAM, -15.0f, 15.0f, 0.0f, "High shelf (2000 Hz) gain", " dB");
  203. configParam(MID_PARAM, -12.5f, 12.5f, 0.0f, "Mid band (1200 Hz) gain", " dB");
  204. configParam(LOW_PARAM, -20.0f, 20.0f, 0.0f, "Low shelf (125 Hz) gain", " dB");
  205. configParam(PAN_PARAM, -1.f, 1.f, 0.0f, "Pan");
  206. configSwitch(MUTE_PARAM, MUTE_OFF_MOMENTARY, MUTE_OFF, MUTE_OFF, "Mute", {"Off (momentary)", "On", "Off"});
  207. configParam(PAN_CV_PARAM, 0.f, 1.f, 0.f, "Pan CV");
  208. configParam(LEVEL_PARAM, -60.0f, 0.0f, -60.0f, "Gain", "dB");
  209. configSwitch(IN_BOOST_PARAM, 0, 1, 0, "In boost", {"0dB", "+6dB"});
  210. configSwitch(OUT_CUT_PARAM, 0, 1, 0, "Out cut", {"0dB", "-6dB"});
  211. configInput(LEFT_INPUT, "Left");
  212. configInput(LEVEL_INPUT, "Level (10 V normalled)");
  213. configInput(RIGHT_INPUT, "Right (left normalled)");
  214. configInput(PAN_INPUT, "Pan CV (-5 V to +5 V)");
  215. configOutput(LEFT_OUTPUT, "Left");
  216. configOutput(RIGHT_OUTPUT, "Right");
  217. configLight(LEFT_LIGHT, "Left");
  218. configLight(RIGHT_LIGHT, "Right");
  219. configBypass(LEFT_INPUT, LEFT_OUTPUT);
  220. configBypass(RIGHT_INPUT, RIGHT_OUTPUT);
  221. onSampleRateChange();
  222. clickFilter.rise = 50.f; // Hz
  223. clickFilter.fall = 50.f; // Hz
  224. // only poll EQ sliders every 16 samples
  225. sliderUpdate.setDivision(16);
  226. }
  227. void onSampleRateChange() override {
  228. bool forceUpdate = true;
  229. updateEQsIfChanged(forceUpdate);
  230. for (int side = 0; side < 2; ++side) {
  231. for (int c = 0; c < 16; c += 4) {
  232. highpass[side][c / 4].setCutoff(25.0f, 0.8f, AeFilterType::AeHIGHPASS);
  233. highshelf[side][c / 4].setParams(12000.0f, 0.8f, -5.0f, AeEQType::AeHIGHSHELVE);
  234. }
  235. }
  236. }
  237. void updateEQsIfChanged(bool forceUpdate = false) {
  238. float highGain = params[HIGH_PARAM].getValue();
  239. float midGain = params[MID_PARAM].getValue();
  240. float lowGain = params[LOW_PARAM].getValue();
  241. // only calculate coefficients when neccessary
  242. if (highGain != lastHighGain || forceUpdate) {
  243. for (int c = 0; c < 16; c += 4) {
  244. for (int side = 0; side < 2; ++side) {
  245. eqHigh[c / 4][side].setParams(2000.0f, 0.4f, highGain, AeEQType::AeHIGHSHELVE);
  246. }
  247. }
  248. lastHighGain = highGain;
  249. }
  250. if (midGain != lastMidGain || forceUpdate) {
  251. for (int c = 0; c < 16; c += 4) {
  252. for (int side = 0; side < 2; ++side) {
  253. eqMid[c / 4][side].setParams(1200.0f, 0.52f, midGain, AeEQType::AePEAKINGEQ);
  254. }
  255. }
  256. lastMidGain = midGain;
  257. }
  258. if (lowGain != lastLowGain || forceUpdate) {
  259. for (int c = 0; c < 16; c += 4) {
  260. for (int side = 0; side < 2; ++side) {
  261. eqLow[c / 4][side].setParams(125.0f, 0.45f, lowGain, AeEQType::AeLOWSHELVE);
  262. }
  263. }
  264. lastLowGain = lowGain;
  265. }
  266. }
  267. void process(const ProcessArgs& args) override {
  268. float_4 out[4][2] = {}, in[4][2] = {};
  269. const int numPolyphonyEngines = std::max(inputs[LEFT_INPUT].getChannels(), inputs[RIGHT_INPUT].getChannels());
  270. // slew mute to avoid clicks
  271. const float muteGain = clickFilter.process(args.sampleTime, params[MUTE_PARAM].getValue() != MUTE_ON);
  272. if (inputs[LEFT_INPUT].isConnected() || inputs[RIGHT_INPUT].isConnected()) {
  273. const float switchGains = (params[IN_BOOST_PARAM].getValue() ? 2.0f : 1.0f) * (params[OUT_CUT_PARAM].getValue() ? 0.5f : 1.0f);
  274. const float preVCAGain = switchGains * muteGain * std::pow(10, params[LEVEL_PARAM].getValue() / 20.0f);
  275. if (sliderUpdate.process()) {
  276. updateEQsIfChanged();
  277. }
  278. for (int c = 0; c < numPolyphonyEngines; c += 4) {
  279. const float_4 postVCAGain = preVCAGain * clamp(inputs[LEVEL_INPUT].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.f);
  280. const float_4 panCV = clamp(params[PAN_CV_PARAM].getValue() * inputs[PAN_INPUT].getPolyVoltageSimd<float_4>(c) / 5.f, -1.f, +1.f);
  281. const float_4 pan = clamp(params[PAN_PARAM].getValue() + panCV, -1.f, +1.f);
  282. // https://www.desmos.com/calculator/b0lisclikw
  283. float_4 gainForSide[2] = {};
  284. switch (panningLaw) {
  285. case LINEAR_6dB: {
  286. gainForSide[0] = postVCAGain * (1.f - pan);
  287. gainForSide[1] = postVCAGain * (1.f + pan);
  288. break;
  289. }
  290. case EQUAL_POWER: {
  291. gainForSide[0] = postVCAGain * simd::sqrt(1.f - pan);
  292. gainForSide[1] = postVCAGain * simd::sqrt(1.f + pan);
  293. break;
  294. }
  295. case LINEAR_CLIPPED: {
  296. gainForSide[0] = simd::ifelse(pan < 0, postVCAGain, postVCAGain * (1.f - pan));
  297. gainForSide[1] = simd::ifelse(pan > 0, postVCAGain, postVCAGain * (1.f + pan));
  298. break;
  299. }
  300. }
  301. in[c / 4][LEFT] = inputs[LEFT_INPUT].getPolyVoltageSimd<float_4>(c);
  302. in[c / 4][RIGHT] = inputs[RIGHT_INPUT].getNormalPolyVoltageSimd<float_4>(in[c / 4][LEFT], c);
  303. for (int side = 0; side < 2; ++side) {
  304. float_4 outForSide = in[c / 4][side];
  305. outForSide = eqLow[c / 4][side].process(outForSide);
  306. outForSide = eqMid[c / 4][side].process(outForSide);
  307. outForSide = eqHigh[c / 4][side].process(outForSide);
  308. outForSide = applyHighpass ? highpass[c / 4][side].process(outForSide) : outForSide;
  309. outForSide = applyHighshelf ? highshelf[c / 4][side].process(outForSide) : outForSide;
  310. outForSide = outForSide * gainForSide[side];
  311. // soft clipping: the Saturator used elsewhere expects values in range [-1, +1] roughly, so rescale before
  312. // and after (assuming input signals are 10Vpp, clipping will kick in above 12Vpp with the present values)
  313. if (applySoftClipping) {
  314. outForSide = Saturator<float_4>::process(outForSide / 6.f) * 6.f;
  315. }
  316. out[c / 4][side] = outForSide;
  317. }
  318. }
  319. }
  320. if (numPolyphonyEngines <= 1) {
  321. lights[LEFT_LIGHT + 0].setBrightness(0.f);
  322. lights[RIGHT_LIGHT + 0].setBrightness(0.f);
  323. lights[LEFT_LIGHT + 1].setBrightnessSmooth(std::abs(out[0][LEFT][0]), args.sampleTime);
  324. lights[RIGHT_LIGHT + 1].setBrightnessSmooth(std::abs(out[0][RIGHT][0]), args.sampleTime);
  325. lights[LEFT_LIGHT + 2].setBrightness(0.f);
  326. lights[RIGHT_LIGHT + 2].setBrightness(0.f);
  327. }
  328. else {
  329. lights[LEFT_LIGHT + 0].setBrightness(0.f);
  330. lights[RIGHT_LIGHT + 0].setBrightness(0.f);
  331. lights[LEFT_LIGHT + 1].setBrightness(0.f);
  332. lights[RIGHT_LIGHT + 1].setBrightness(0.f);
  333. lights[LEFT_LIGHT + 2].setBrightness(1.f);
  334. lights[RIGHT_LIGHT + 2].setBrightness(1.f);
  335. }
  336. for (int c = 0; c < numPolyphonyEngines; c += 4) {
  337. outputs[LEFT_OUTPUT].setVoltageSimd(out[c / 4][LEFT], c);
  338. outputs[RIGHT_OUTPUT].setVoltageSimd(out[c / 4][RIGHT], c);
  339. }
  340. outputs[LEFT_OUTPUT].setChannels(numPolyphonyEngines);
  341. outputs[RIGHT_OUTPUT].setChannels(numPolyphonyEngines);
  342. }
  343. json_t* dataToJson() override {
  344. json_t* rootJ = json_object();
  345. json_object_set_new(rootJ, "applyHighpass", json_boolean(applyHighpass));
  346. json_object_set_new(rootJ, "applyHighshelf", json_boolean(applyHighshelf));
  347. json_object_set_new(rootJ, "panningLaw", json_integer(panningLaw));
  348. json_object_set_new(rootJ, "applySoftClipping", json_boolean(applySoftClipping));
  349. return rootJ;
  350. }
  351. void dataFromJson(json_t* rootJ) override {
  352. json_t* applyHighshelfJ = json_object_get(rootJ, "applyHighshelf");
  353. if (applyHighshelfJ) {
  354. applyHighshelf = json_boolean_value(applyHighshelfJ);
  355. }
  356. json_t* applyHighpassJ = json_object_get(rootJ, "applyHighpass");
  357. if (applyHighpassJ) {
  358. applyHighpass = json_boolean_value(applyHighpassJ);
  359. }
  360. json_t* panningLawJ = json_object_get(rootJ, "panningLaw");
  361. if (panningLawJ) {
  362. panningLaw = (PanningLaw) json_integer_value(panningLawJ);
  363. }
  364. json_t* softClippingJ = json_object_get(rootJ, "applySoftClipping");
  365. if (softClippingJ) {
  366. applySoftClipping = json_boolean_value(softClippingJ);
  367. }
  368. }
  369. };
  370. // an implementation of a performable, 3-stage switch, where the bottom state is Momentary
  371. struct ThreeStateBefacoSwitchMomentary : SvgSwitch {
  372. ThreeStateBefacoSwitchMomentary() {
  373. momentary = true;
  374. addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_0.svg")));
  375. addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_1.svg")));
  376. addFrame(Svg::load(asset::system("res/ComponentLibrary/BefacoSwitch_2.svg")));
  377. }
  378. void onDragStart(const event::DragStart& e) override {
  379. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  380. latched = false;
  381. pos = Vec(0, 0);
  382. }
  383. ParamWidget::onDragStart(e);
  384. }
  385. void onDragMove(const event::DragMove& e) override {
  386. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  387. pos += e.mouseDelta;
  388. // Once the user has dragged the mouse a "threshold" distance, latch
  389. // to disallow further changes of state until the mouse is released.
  390. // We don't just setValue(1) (default/rest state) because this creates a
  391. // jarring UI experience
  392. if (pos.y < -10 && !latched) {
  393. getParamQuantity()->setValue(StereoStrip::MUTE_OFF);
  394. latched = true;
  395. }
  396. if (pos.y > 10 && !latched) {
  397. getParamQuantity()->setValue(StereoStrip::MUTE_OFF_MOMENTARY);
  398. latched = true;
  399. }
  400. }
  401. ParamWidget::onDragMove(e);
  402. }
  403. void onDragEnd(const event::DragEnd& e) override {
  404. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  405. // not dragged == clicked
  406. if (std::sqrt(pos.square()) < 5) {
  407. // if muted, unmute
  408. if (getParamQuantity()->getValue() == StereoStrip::MUTE_ON) {
  409. getParamQuantity()->setValue(StereoStrip::MUTE_OFF);
  410. }
  411. // if ummuted, mute
  412. else if (getParamQuantity()->getValue() == StereoStrip::MUTE_OFF) {
  413. getParamQuantity()->setValue(StereoStrip::MUTE_ON);
  414. }
  415. }
  416. // on release, the switch resets to default/neutral/middle position, if was previously down
  417. if (getParamQuantity()->getValue() == StereoStrip::MUTE_OFF_MOMENTARY) {
  418. getParamQuantity()->setValue(StereoStrip::MUTE_ON);
  419. }
  420. latched = false;
  421. }
  422. ParamWidget::onDragEnd(e);
  423. }
  424. Vec pos;
  425. bool latched = false;
  426. };
  427. struct StereoStripWidget : ModuleWidget {
  428. StereoStripWidget(StereoStrip* module) {
  429. setModule(module);
  430. setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/StereoStrip.svg")));
  431. addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
  432. addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  433. addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.763, 35.805)), module, StereoStrip::LOW_PARAM));
  434. addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.817, 35.805)), module, StereoStrip::MID_PARAM));
  435. addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(22.861, 35.805)), module, StereoStrip::HIGH_PARAM));
  436. addParam(createParamCentered<Davies1900hDarkGreyKnob>(mm2px(Vec(15.042, 74.11)), module, StereoStrip::PAN_PARAM));
  437. addParam(createParamCentered<ThreeStateBefacoSwitchMomentary>(mm2px(Vec(7.416, 91.244)), module, StereoStrip::MUTE_PARAM));
  438. addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(22.842, 91.244)), module, StereoStrip::PAN_CV_PARAM));
  439. addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(15.054, 111.333)), module, StereoStrip::LEVEL_PARAM));
  440. addParam(createParam<CKSSNarrow>(mm2px(Vec(2.372, 72.298)), module, StereoStrip::IN_BOOST_PARAM));
  441. addParam(createParam<CKSSNarrow>(mm2px(Vec(24.253, 72.298)), module, StereoStrip::OUT_CUT_PARAM));
  442. addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 14.852)), module, StereoStrip::LEFT_INPUT));
  443. addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.023, 14.852)), module, StereoStrip::LEVEL_INPUT));
  444. addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.038, 26.304)), module, StereoStrip::RIGHT_INPUT));
  445. addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.023, 26.304)), module, StereoStrip::PAN_INPUT));
  446. addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.069, 14.882)), module, StereoStrip::LEFT_OUTPUT));
  447. addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(25.069, 26.317)), module, StereoStrip::RIGHT_OUTPUT));
  448. addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(4.05, 69.906)), module, StereoStrip::LEFT_LIGHT));
  449. addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(26.05, 69.906)), module, StereoStrip::RIGHT_LIGHT));
  450. }
  451. void appendContextMenu(Menu* menu) override {
  452. StereoStrip* module = dynamic_cast<StereoStrip*>(this->module);
  453. assert(module);
  454. menu->addChild(new MenuSeparator());
  455. menu->addChild(createBoolPtrMenuItem("Apply Highpass (25Hz)", "", &module->applyHighpass));
  456. menu->addChild(createBoolPtrMenuItem("Apply Highshelf (12kHz)", "", &module->applyHighshelf));
  457. menu->addChild(createBoolPtrMenuItem("Apply soft-clipping", "", &module->applySoftClipping));
  458. menu->addChild(new MenuSeparator());
  459. menu->addChild(createIndexPtrSubmenuItem("Panning law", {"Linear (+6dB)", "Equal power (+3dB)", "Linear clipped"}, &module->panningLaw));
  460. }
  461. };
  462. Model* modelChannelStrip = createModel<StereoStrip, StereoStripWidget>("StereoStrip");