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.

543 lines
16KB

  1. #include "plugin.hpp"
  2. using simd::float_4;
  3. // Accurate only on [0, 1]
  4. template <typename T>
  5. T sin2pi_pade_05_7_6(T x) {
  6. x -= 0.5f;
  7. return (T(-6.28319) * x + T(35.353) * simd::pow(x, 3) - T(44.9043) * simd::pow(x, 5) + T(16.0951) * simd::pow(x, 7))
  8. / (1 + T(0.953136) * simd::pow(x, 2) + T(0.430238) * simd::pow(x, 4) + T(0.0981408) * simd::pow(x, 6));
  9. }
  10. template <typename T>
  11. T sin2pi_pade_05_5_4(T x) {
  12. x -= 0.5f;
  13. return (T(-6.283185307) * x + T(33.19863968) * simd::pow(x, 3) - T(32.44191367) * simd::pow(x, 5))
  14. / (1 + T(1.296008659) * simd::pow(x, 2) + T(0.7028072946) * simd::pow(x, 4));
  15. }
  16. template <typename T>
  17. T expCurve(T x) {
  18. return (3 + x * (-13 + 5 * x)) / (3 + 2 * x);
  19. }
  20. enum WaveIds {
  21. SIN_WAVE = 1 << 0,
  22. TRI_WAVE = 1 << 1,
  23. SAW_WAVE = 1 << 2,
  24. SQR_WAVE = 1 << 3,
  25. ALL_WAVE = 0xffffffff
  26. };
  27. template <int OVERSAMPLE, int QUALITY, typename T>
  28. struct VoltageControlledOscillator {
  29. bool analog = false;
  30. bool soft = false;
  31. bool syncEnabled = false;
  32. // For optimizing in serial code
  33. int channels = 0;
  34. T lastSyncValue = 0.f;
  35. T phase = 0.f;
  36. T freq;
  37. T pulseWidth = 0.5f;
  38. T syncDirection = 1.f;
  39. dsp::TRCFilter<T> sqrFilter;
  40. dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> sqrMinBlep;
  41. dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> sawMinBlep;
  42. dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> triMinBlep;
  43. dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> sinMinBlep;
  44. T sqrValue = 0.f;
  45. T sawValue = 0.f;
  46. T triValue = 0.f;
  47. T sinValue = 0.f;
  48. void setPitch(T pitch) {
  49. freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30) / 1073741824;
  50. }
  51. void setPulseWidth(T pulseWidth) {
  52. const float pwMin = 0.01f;
  53. this->pulseWidth = simd::clamp(pulseWidth, pwMin, 1.f - pwMin);
  54. }
  55. void process(float deltaTime, T syncValue, int enabledWaves) {
  56. // Do nothing if there aren't any waves to process
  57. if (!enabledWaves) return;
  58. // Advance phase
  59. T deltaPhase = simd::clamp(freq * deltaTime, 1e-6f, 0.35f);
  60. if (soft) {
  61. // Reverse direction
  62. deltaPhase *= syncDirection;
  63. }
  64. else {
  65. // Reset back to forward
  66. syncDirection = 1.f;
  67. }
  68. phase += deltaPhase;
  69. // Wrap phase
  70. phase -= simd::floor(phase);
  71. if (enabledWaves & SQR_WAVE) {
  72. // Jump sqr when crossing 0, or 1 if backwards
  73. T wrapPhase = (syncDirection == -1.f) & 1.f;
  74. T wrapCrossing = (wrapPhase - (phase - deltaPhase)) / deltaPhase;
  75. int wrapMask = simd::movemask((0 < wrapCrossing) & (wrapCrossing <= 1.f));
  76. if (wrapMask) {
  77. for (int i = 0; i < channels; i++) {
  78. if (wrapMask & (1 << i)) {
  79. T mask = simd::movemaskInverse<T>(1 << i);
  80. float p = wrapCrossing[i] - 1.f;
  81. T x = mask & (2.f * syncDirection);
  82. sqrMinBlep.insertDiscontinuity(p, x);
  83. }
  84. }
  85. }
  86. // Jump sqr when crossing `pulseWidth`
  87. T pulseCrossing = (pulseWidth - (phase - deltaPhase)) / deltaPhase;
  88. int pulseMask = simd::movemask((0 < pulseCrossing) & (pulseCrossing <= 1.f));
  89. if (pulseMask) {
  90. for (int i = 0; i < channels; i++) {
  91. if (pulseMask & (1 << i)) {
  92. T mask = simd::movemaskInverse<T>(1 << i);
  93. float p = pulseCrossing[i] - 1.f;
  94. T x = mask & (-2.f * syncDirection);
  95. sqrMinBlep.insertDiscontinuity(p, x);
  96. }
  97. }
  98. }
  99. }
  100. if (enabledWaves & SAW_WAVE) {
  101. // Jump saw when crossing 0.5
  102. T halfCrossing = (0.5f - (phase - deltaPhase)) / deltaPhase;
  103. int halfMask = simd::movemask((0 < halfCrossing) & (halfCrossing <= 1.f));
  104. if (halfMask) {
  105. for (int i = 0; i < channels; i++) {
  106. if (halfMask & (1 << i)) {
  107. T mask = simd::movemaskInverse<T>(1 << i);
  108. float p = halfCrossing[i] - 1.f;
  109. T x = mask & (-2.f * syncDirection);
  110. sawMinBlep.insertDiscontinuity(p, x);
  111. }
  112. }
  113. }
  114. }
  115. // Detect sync
  116. // Might be NAN or outside of [0, 1) range
  117. if (syncEnabled) {
  118. T deltaSync = syncValue - lastSyncValue;
  119. T syncCrossing = -lastSyncValue / deltaSync;
  120. lastSyncValue = syncValue;
  121. T sync = (0.f < syncCrossing) & (syncCrossing <= 1.f) & (syncValue >= 0.f);
  122. int syncMask = simd::movemask(sync);
  123. if (syncMask) {
  124. if (soft) {
  125. syncDirection = simd::ifelse(sync, -syncDirection, syncDirection);
  126. }
  127. else {
  128. T newPhase = simd::ifelse(sync, (1.f - syncCrossing) * deltaPhase, phase);
  129. // Insert minBLEP for sync
  130. for (int i = 0; i < channels; i++) {
  131. if (syncMask & (1 << i)) {
  132. T mask = simd::movemaskInverse<T>(1 << i);
  133. float p = syncCrossing[i] - 1.f;
  134. T x;
  135. x = mask & (sqr(newPhase) - sqr(phase));
  136. sqrMinBlep.insertDiscontinuity(p, x);
  137. x = mask & (saw(newPhase) - saw(phase));
  138. sawMinBlep.insertDiscontinuity(p, x);
  139. x = mask & (tri(newPhase) - tri(phase));
  140. triMinBlep.insertDiscontinuity(p, x);
  141. x = mask & (sin(newPhase) - sin(phase));
  142. sinMinBlep.insertDiscontinuity(p, x);
  143. }
  144. }
  145. phase = newPhase;
  146. }
  147. }
  148. }
  149. // Square
  150. if (enabledWaves & SQR_WAVE) {
  151. sqrValue = sqr(phase);
  152. sqrValue += sqrMinBlep.process();
  153. if (analog) {
  154. sqrFilter.setCutoffFreq(20.f * deltaTime);
  155. sqrFilter.process(sqrValue);
  156. sqrValue = sqrFilter.highpass() * 0.95f;
  157. }
  158. }
  159. // Saw
  160. if (enabledWaves & SAW_WAVE) {
  161. sawValue = saw(phase);
  162. sawValue += sawMinBlep.process();
  163. }
  164. // Tri
  165. if (enabledWaves & TRI_WAVE) {
  166. triValue = tri(phase);
  167. triValue += triMinBlep.process();
  168. }
  169. // Sin
  170. if (enabledWaves & SIN_WAVE) {
  171. sinValue = sin(phase);
  172. sinValue += sinMinBlep.process();
  173. }
  174. }
  175. T sin(T phase) {
  176. T v;
  177. if (analog) {
  178. // Quadratic approximation of sine, slightly richer harmonics
  179. T halfPhase = (phase < 0.5f);
  180. T x = phase - simd::ifelse(halfPhase, 0.25f, 0.75f);
  181. v = 1.f - 16.f * simd::pow(x, 2);
  182. v *= simd::ifelse(halfPhase, 1.f, -1.f);
  183. }
  184. else {
  185. v = sin2pi_pade_05_5_4(phase);
  186. // v = sin2pi_pade_05_7_6(phase);
  187. // v = simd::sin(2 * T(M_PI) * phase);
  188. }
  189. return v;
  190. }
  191. T sin() {
  192. return sinValue;
  193. }
  194. T tri(T phase) {
  195. T v;
  196. if (analog) {
  197. T x = phase + 0.25f;
  198. x -= simd::trunc(x);
  199. T halfX = (x >= 0.5f);
  200. x *= 2;
  201. x -= simd::trunc(x);
  202. v = expCurve(x) * simd::ifelse(halfX, 1.f, -1.f);
  203. }
  204. else {
  205. v = 1 - 4 * simd::fmin(simd::fabs(phase - 0.25f), simd::fabs(phase - 1.25f));
  206. }
  207. return v;
  208. }
  209. T tri() {
  210. return triValue;
  211. }
  212. T saw(T phase) {
  213. T v;
  214. T x = phase + 0.5f;
  215. x -= simd::trunc(x);
  216. if (analog) {
  217. v = -expCurve(x);
  218. }
  219. else {
  220. v = 2 * x - 1;
  221. }
  222. return v;
  223. }
  224. T saw() {
  225. return sawValue;
  226. }
  227. T sqr(T phase) {
  228. T v = simd::ifelse(phase < pulseWidth, 1.f, -1.f);
  229. return v;
  230. }
  231. T sqr() {
  232. return sqrValue;
  233. }
  234. T light() {
  235. return simd::sin(2 * T(M_PI) * phase);
  236. }
  237. };
  238. struct VCO : Module {
  239. enum ParamIds {
  240. MODE_PARAM,
  241. SYNC_PARAM,
  242. FREQ_PARAM,
  243. FINE_PARAM,
  244. FM_PARAM,
  245. PW_PARAM,
  246. PWM_PARAM,
  247. NUM_PARAMS
  248. };
  249. enum InputIds {
  250. PITCH_INPUT,
  251. FM_INPUT,
  252. SYNC_INPUT,
  253. PW_INPUT,
  254. NUM_INPUTS
  255. };
  256. enum OutputIds {
  257. SIN_OUTPUT,
  258. TRI_OUTPUT,
  259. SAW_OUTPUT,
  260. SQR_OUTPUT,
  261. NUM_OUTPUTS
  262. };
  263. enum LightIds {
  264. ENUMS(PHASE_LIGHT, 3),
  265. NUM_LIGHTS
  266. };
  267. VoltageControlledOscillator<16, 16, float_4> oscillators[4];
  268. dsp::ClockDivider lightDivider;
  269. VCO() {
  270. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  271. configParam(MODE_PARAM, 0.f, 1.f, 1.f, "Analog mode");
  272. configParam(SYNC_PARAM, 0.f, 1.f, 1.f, "Hard sync");
  273. configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
  274. configParam(FINE_PARAM, -1.f, 1.f, 0.f, "Fine frequency");
  275. configParam(FM_PARAM, 0.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f);
  276. configParam(PW_PARAM, 0.01f, 0.99f, 0.5f, "Pulse width", "%", 0.f, 100.f);
  277. configParam(PWM_PARAM, 0.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f);
  278. lightDivider.setDivision(16);
  279. }
  280. void process(const ProcessArgs& args) override {
  281. int enabledWaves = 0; // Need to use int here becuase C++
  282. if (outputs[SIN_OUTPUT].isConnected())
  283. enabledWaves |= SIN_WAVE;
  284. if (outputs[TRI_OUTPUT].isConnected())
  285. enabledWaves |= TRI_WAVE;
  286. if (outputs[SAW_OUTPUT].isConnected())
  287. enabledWaves |= SAW_WAVE;
  288. if (outputs[SQR_OUTPUT].isConnected())
  289. enabledWaves |= SQR_WAVE;
  290. if (!enabledWaves) return;
  291. float freqParam = params[FREQ_PARAM].getValue() / 12.f;
  292. freqParam += dsp::quadraticBipolar(params[FINE_PARAM].getValue()) * 3.f / 12.f;
  293. float fmParam = dsp::quadraticBipolar(params[FM_PARAM].getValue());
  294. int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1);
  295. for (int c = 0; c < channels; c += 4) {
  296. auto* oscillator = &oscillators[c / 4];
  297. oscillator->channels = std::min(channels - c, 4);
  298. oscillator->analog = params[MODE_PARAM].getValue() > 0.f;
  299. oscillator->soft = params[SYNC_PARAM].getValue() <= 0.f;
  300. float_4 pitch = freqParam;
  301. pitch += inputs[PITCH_INPUT].getVoltageSimd<float_4>(c);
  302. if (inputs[FM_INPUT].isConnected()) {
  303. pitch += fmParam * inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c);
  304. }
  305. oscillator->setPitch(pitch);
  306. oscillator->setPulseWidth(params[PW_PARAM].getValue() + params[PWM_PARAM].getValue() * inputs[PW_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f);
  307. oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected();
  308. oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c), enabledWaves);
  309. // Set output
  310. if (outputs[SIN_OUTPUT].isConnected())
  311. outputs[SIN_OUTPUT].setVoltageSimd(5.f * oscillator->sin(), c);
  312. if (outputs[TRI_OUTPUT].isConnected())
  313. outputs[TRI_OUTPUT].setVoltageSimd(5.f * oscillator->tri(), c);
  314. if (outputs[SAW_OUTPUT].isConnected())
  315. outputs[SAW_OUTPUT].setVoltageSimd(5.f * oscillator->saw(), c);
  316. if (outputs[SQR_OUTPUT].isConnected())
  317. outputs[SQR_OUTPUT].setVoltageSimd(5.f * oscillator->sqr(), c);
  318. }
  319. outputs[SIN_OUTPUT].setChannels(channels);
  320. outputs[TRI_OUTPUT].setChannels(channels);
  321. outputs[SAW_OUTPUT].setChannels(channels);
  322. outputs[SQR_OUTPUT].setChannels(channels);
  323. // Light
  324. if (lightDivider.process()) {
  325. if (channels == 1) {
  326. float lightValue = oscillators[0].light()[0];
  327. lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision());
  328. lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision());
  329. lights[PHASE_LIGHT + 2].setBrightness(0.f);
  330. }
  331. else {
  332. lights[PHASE_LIGHT + 0].setBrightness(0.f);
  333. lights[PHASE_LIGHT + 1].setBrightness(0.f);
  334. lights[PHASE_LIGHT + 2].setBrightness(1.f);
  335. }
  336. }
  337. }
  338. };
  339. struct VCOWidget : ModuleWidget {
  340. VCOWidget(VCO* module) {
  341. setModule(module);
  342. setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/VCO-1.svg")));
  343. addChild(createWidget<ScrewSilver>(Vec(15, 0)));
  344. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 0)));
  345. addChild(createWidget<ScrewSilver>(Vec(15, 365)));
  346. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 365)));
  347. addParam(createParam<CKSS>(Vec(15, 77), module, VCO::MODE_PARAM));
  348. addParam(createParam<CKSS>(Vec(119, 77), module, VCO::SYNC_PARAM));
  349. addParam(createParam<RoundHugeBlackKnob>(Vec(47, 61), module, VCO::FREQ_PARAM));
  350. addParam(createParam<RoundLargeBlackKnob>(Vec(23, 143), module, VCO::FINE_PARAM));
  351. addParam(createParam<RoundLargeBlackKnob>(Vec(91, 143), module, VCO::PW_PARAM));
  352. addParam(createParam<RoundLargeBlackKnob>(Vec(23, 208), module, VCO::FM_PARAM));
  353. addParam(createParam<RoundLargeBlackKnob>(Vec(91, 208), module, VCO::PWM_PARAM));
  354. addInput(createInput<PJ301MPort>(Vec(11, 276), module, VCO::PITCH_INPUT));
  355. addInput(createInput<PJ301MPort>(Vec(45, 276), module, VCO::FM_INPUT));
  356. addInput(createInput<PJ301MPort>(Vec(80, 276), module, VCO::SYNC_INPUT));
  357. addInput(createInput<PJ301MPort>(Vec(114, 276), module, VCO::PW_INPUT));
  358. addOutput(createOutput<PJ301MPort>(Vec(11, 320), module, VCO::SIN_OUTPUT));
  359. addOutput(createOutput<PJ301MPort>(Vec(45, 320), module, VCO::TRI_OUTPUT));
  360. addOutput(createOutput<PJ301MPort>(Vec(80, 320), module, VCO::SAW_OUTPUT));
  361. addOutput(createOutput<PJ301MPort>(Vec(114, 320), module, VCO::SQR_OUTPUT));
  362. addChild(createLight<SmallLight<RedGreenBlueLight>>(Vec(99, 42.5f), module, VCO::PHASE_LIGHT));
  363. }
  364. };
  365. Model* modelVCO = createModel<VCO, VCOWidget>("VCO");
  366. struct VCO2 : Module {
  367. enum ParamIds {
  368. MODE_PARAM,
  369. SYNC_PARAM,
  370. FREQ_PARAM,
  371. WAVE_PARAM,
  372. FM_PARAM,
  373. NUM_PARAMS
  374. };
  375. enum InputIds {
  376. FM_INPUT,
  377. SYNC_INPUT,
  378. WAVE_INPUT,
  379. NUM_INPUTS
  380. };
  381. enum OutputIds {
  382. OUT_OUTPUT,
  383. NUM_OUTPUTS
  384. };
  385. enum LightIds {
  386. ENUMS(PHASE_LIGHT, 3),
  387. NUM_LIGHTS
  388. };
  389. VoltageControlledOscillator<8, 8, float_4> oscillators[4];
  390. dsp::ClockDivider lightDivider;
  391. VCO2() {
  392. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  393. configParam(MODE_PARAM, 0.f, 1.f, 1.f, "Analog mode");
  394. configParam(SYNC_PARAM, 0.f, 1.f, 1.f, "Hard sync");
  395. configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
  396. configParam(WAVE_PARAM, 0.f, 3.f, 1.5f, "Wave");
  397. configParam(FM_PARAM, 0.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f);
  398. lightDivider.setDivision(16);
  399. }
  400. void process(const ProcessArgs& args) override {
  401. if (!outputs[OUT_OUTPUT].isConnected()) return;
  402. float freqParam = params[FREQ_PARAM].getValue() / 12.f;
  403. float fmParam = dsp::quadraticBipolar(params[FM_PARAM].getValue());
  404. float waveParam = params[WAVE_PARAM].getValue();
  405. int channels = std::max(inputs[FM_INPUT].getChannels(), 1);
  406. for (int c = 0; c < channels; c += 4) {
  407. auto* oscillator = &oscillators[c / 4];
  408. oscillator->channels = std::min(channels - c, 4);
  409. oscillator->analog = (params[MODE_PARAM].getValue() > 0.f);
  410. oscillator->soft = (params[SYNC_PARAM].getValue() <= 0.f);
  411. float_4 pitch = freqParam;
  412. pitch += fmParam * inputs[FM_INPUT].getVoltageSimd<float_4>(c);
  413. oscillator->setPitch(pitch);
  414. oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected();
  415. oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c), ALL_WAVE);
  416. // Outputs
  417. float_4 wave = simd::clamp(waveParam + inputs[WAVE_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f * 3.f, 0.f, 3.f);
  418. float_4 v = 0.f;
  419. v += oscillator->sin() * simd::fmax(0.f, 1.f - simd::fabs(wave - 0.f));
  420. v += oscillator->tri() * simd::fmax(0.f, 1.f - simd::fabs(wave - 1.f));
  421. v += oscillator->saw() * simd::fmax(0.f, 1.f - simd::fabs(wave - 2.f));
  422. v += oscillator->sqr() * simd::fmax(0.f, 1.f - simd::fabs(wave - 3.f));
  423. outputs[OUT_OUTPUT].setVoltageSimd(5.f * v, c);
  424. }
  425. outputs[OUT_OUTPUT].setChannels(channels);
  426. // Light
  427. if (lightDivider.process()) {
  428. if (channels == 1) {
  429. float lightValue = oscillators[0].light()[0];
  430. lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision());
  431. lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision());
  432. lights[PHASE_LIGHT + 2].setBrightness(0.f);
  433. }
  434. else {
  435. lights[PHASE_LIGHT + 0].setBrightness(0.f);
  436. lights[PHASE_LIGHT + 1].setBrightness(0.f);
  437. lights[PHASE_LIGHT + 2].setBrightness(1.f);
  438. }
  439. }
  440. }
  441. };
  442. struct VCO2Widget : ModuleWidget {
  443. VCO2Widget(VCO2* module) {
  444. setModule(module);
  445. setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/VCO-2.svg")));
  446. addChild(createWidget<ScrewSilver>(Vec(15, 0)));
  447. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 0)));
  448. addChild(createWidget<ScrewSilver>(Vec(15, 365)));
  449. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 365)));
  450. addParam(createParam<CKSS>(Vec(62, 150), module, VCO2::MODE_PARAM));
  451. addParam(createParam<CKSS>(Vec(62, 215), module, VCO2::SYNC_PARAM));
  452. addParam(createParam<RoundHugeBlackKnob>(Vec(17, 60), module, VCO2::FREQ_PARAM));
  453. addParam(createParam<RoundLargeBlackKnob>(Vec(12, 143), module, VCO2::WAVE_PARAM));
  454. addParam(createParam<RoundLargeBlackKnob>(Vec(12, 208), module, VCO2::FM_PARAM));
  455. addInput(createInput<PJ301MPort>(Vec(11, 276), module, VCO2::FM_INPUT));
  456. addInput(createInput<PJ301MPort>(Vec(54, 276), module, VCO2::SYNC_INPUT));
  457. addInput(createInput<PJ301MPort>(Vec(11, 320), module, VCO2::WAVE_INPUT));
  458. addOutput(createOutput<PJ301MPort>(Vec(54, 320), module, VCO2::OUT_OUTPUT));
  459. addChild(createLight<SmallLight<RedGreenBlueLight>>(Vec(68, 42.5f), module, VCO2::PHASE_LIGHT));
  460. }
  461. };
  462. Model* modelVCO2 = createModel<VCO2, VCO2Widget>("VCO2");