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.

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