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.

622 lines
20KB

  1. #include "plugin.hpp"
  2. // TODO: Remove these DSP classes after released in Rack SDK
  3. /** Evaluates sin(pi x) for x in [-1, 1].
  4. 9th order polynomial with roots at 0, -1, and 1.
  5. Optimized coefficients for best THD (-103.7 dB) and max absolute error (6.68e-06).
  6. */
  7. template <typename T>
  8. inline T sin_pi_9(T x) {
  9. T x2 = x * x;
  10. return x * (T(1) - x2) * (T(3.141521108990) + x2 * (T(-2.024773594411) + x2 * (T(0.517493161073) + x2 * T(-0.063691343423))));
  11. }
  12. /** Catmull-Rom cubic interpolation
  13. t is the fractional position between y1 and y2.
  14. */
  15. template <typename T>
  16. T catmullRomInterpolate(T y0, T y1, T y2, T y3, T t) {
  17. T t2 = t * t;
  18. T t3 = t2 * t;
  19. return y1 + T(0.5) * t * (y2 - y0)
  20. + t2 * (y0 - T(2.5)*y1 + T(2)*y2 - T(0.5)*y3)
  21. + t3 * (T(-0.5)*y0 + T(1.5)*y1 - T(1.5)*y2 + T(0.5)*y3);
  22. }
  23. /** One-pole low-pass filter, exponential moving average.
  24. -6 dB/octave slope.
  25. Useful for leaky integrators and smoothing control signals.
  26. Has a pole at (1 - alpha) and a zero at 0.
  27. */
  28. struct OnePoleLowpass {
  29. float alpha = 0.f;
  30. /** Sets the cutoff frequency where gain is -3 dB.
  31. f = f_c / f_s, the normalized frequency in the range [0, 0.5].
  32. */
  33. void setCutoff(float f) {
  34. alpha = 1.f - std::exp(-2.f * M_PI * f);
  35. }
  36. template <typename T = float>
  37. struct State {
  38. T y = 0.f;
  39. };
  40. /** Advances the state with input x. Returns the output. */
  41. template <typename T>
  42. T process(State<T>& s, T x) const {
  43. s.y += alpha * (x - s.y);
  44. return s.y;
  45. }
  46. /** Computes the frequency response at normalized frequency f. */
  47. std::complex<float> getResponse(float f) const {
  48. float omega = 2.f * M_PI * f;
  49. std::complex<float> z = std::exp(std::complex<float>(0.f, omega));
  50. return alpha * z / (z - (1.f - alpha));
  51. }
  52. float getMagnitude(float f) const {
  53. return std::abs(getResponse(f));
  54. }
  55. float getPhase(float f) const {
  56. return std::arg(getResponse(f));
  57. }
  58. };
  59. /** One-pole high-pass filter.
  60. 6 dB/octave slope.
  61. Useful for DC-blocking.
  62. Has a pole at (1 - alpha) and a zero at 1.
  63. */
  64. struct OnePoleHighpass : OnePoleLowpass {
  65. template <typename T>
  66. T process(State<T>& s, T x) const {
  67. return x - OnePoleLowpass::process(s, x);
  68. }
  69. std::complex<float> getResponse(float f) const {
  70. return 1.f - OnePoleLowpass::getResponse(f);
  71. }
  72. float getMagnitude(float f) const {
  73. return std::abs(getResponse(f));
  74. }
  75. float getPhase(float f) const {
  76. return std::arg(getResponse(f));
  77. }
  78. };
  79. /** Holds a precomputed minBLEP impulse response, reordered to efficiently insert into an audio buffer.
  80. */
  81. template <int Z, int O>
  82. struct MinBlep {
  83. /** Reordered impulse response for cubic interpolation, minus 1.0.
  84. Z dimension has +1 padding at start (for z-1 wrap) and +4 at end (for SIMD + z+1 wrap).
  85. Access via getImpulse() which handles O wrapping and Z offset.
  86. */
  87. float impulseReordered[O][1 + 2 * Z + 4] = {};
  88. MinBlep() {
  89. float impulse[2 * Z][O];
  90. dsp::minBlepImpulse(Z, O, &impulse[0][0]);
  91. // Fill impulseReordered with transposed and pre-subtracted data
  92. // Storage index = z + 1 (to allow z = -1 access at index 0)
  93. for (int o = 0; o < O; o++) {
  94. // z = -1: before impulse starts
  95. impulseReordered[o][0] = -1.f;
  96. // z = 0 to 2*Z-1: actual impulse data
  97. for (int z = 0; z < 2 * Z; z++) {
  98. impulseReordered[o][1 + z] = impulse[z][o] - 1.f;
  99. }
  100. // z = 2*Z to 2*Z+3: after impulse ends (for SIMD padding)
  101. for (int z = 2 * Z; z < 2 * Z + 4; z++) {
  102. impulseReordered[o][1 + z] = 0.f;
  103. }
  104. }
  105. }
  106. /** Get pointer to impulse data, handling o wrapping and z offset. */
  107. const float* getImpulse(int o, int z) const {
  108. z += o / O;
  109. o = o % O;
  110. if (o < 0) {
  111. z -= 1;
  112. o += O;
  113. }
  114. // assert(0 <= o && o < O);
  115. // assert(-1 <= z && z < 2 * Z + 4);
  116. return &impulseReordered[o][z + 1];
  117. }
  118. /** Places a discontinuity at 0 < p <= 1 relative to the current frame.
  119. `out` must have enough space to write 2*Z floats, spaced by `stride` floats.
  120. `p` is the subsample position to insert a discontinuity of `magnitude`.
  121. For example if a square wave will jump from 1 to -1 in 0.1 frames, use insertDiscontinuity(out, 1, 0.1f, -2.f).
  122. Note: In the deprecated MinBlepGenerator, `p` was in the range (-1, 0], so add 1 to p if updating to MinBlep.
  123. */
  124. void insertDiscontinuity(float* out, int stride, float p, float magnitude) const {
  125. if (!(0 < p && p <= 1))
  126. return;
  127. // Calculate impulse array index and fractional part
  128. float subsample = (1 - p) * O;
  129. int o = (int) subsample;
  130. float t = subsample - o;
  131. // For each zero crossing, interpolate impulse response from oversample points
  132. using simd::float_4;
  133. for (int z = 0; z < 2 * Z; z += 4) {
  134. float_4 y0 = float_4::load(getImpulse(o - 1, z));
  135. float_4 y1 = float_4::load(getImpulse(o, z));
  136. float_4 y2 = float_4::load(getImpulse(o + 1, z));
  137. float_4 y3 = float_4::load(getImpulse(o + 2, z));
  138. float_4 y = catmullRomInterpolate(y0, y1, y2, y3, float_4(t));
  139. y *= magnitude;
  140. // Write all 4 samples to buffer
  141. for (int zi = 0; zi < 4; zi++) {
  142. out[(z + zi) * stride] += y[zi];
  143. }
  144. }
  145. }
  146. };
  147. /** Buffer that allows reading/writing up to N future elements contiguously.
  148. */
  149. template <int N, typename T>
  150. struct MinBlepBuffer {
  151. T buffer[2 * N] = {};
  152. int32_t bufferIndex = 0;
  153. T* startData() {
  154. return &buffer[bufferIndex];
  155. }
  156. /** Returns the current element and advances the buffer.
  157. */
  158. T shift() {
  159. T v = buffer[bufferIndex];
  160. bufferIndex++;
  161. if (bufferIndex >= N) {
  162. // Move second half of buffer to beginning
  163. std::memcpy(buffer, buffer + N, N * sizeof(T));
  164. std::memset(buffer + N, 0, N * sizeof(T));
  165. bufferIndex = 0;
  166. }
  167. return v;
  168. }
  169. /** Copies `n` elements to `out` and advances the buffer.
  170. */
  171. void shiftBuffer(T* out, size_t n) {
  172. std::memcpy(out, buffer + bufferIndex, n * sizeof(T));
  173. bufferIndex += n;
  174. if (bufferIndex >= N) {
  175. std::memcpy(buffer, buffer + N, N * sizeof(T));
  176. std::memset(buffer + N, 0, N * sizeof(T));
  177. bufferIndex = 0;
  178. }
  179. }
  180. };
  181. static MinBlep<16, 16> minBlep;
  182. template <typename T>
  183. struct VCOProcessor {
  184. T phase = 0.f;
  185. /** 1 for forward, -1 for backward. */
  186. T syncDirection = 1.f;
  187. T lastSync = 0.f;
  188. /** 1 or -1 */
  189. T lastSqrState = 1.f;
  190. /** Leaky integrator result. */
  191. T triFilterState = 0.f;
  192. OnePoleHighpass dcFilter;
  193. OnePoleHighpass::State<T> dcFilterStateSqr;
  194. OnePoleHighpass::State<T> dcFilterStateSaw;
  195. OnePoleHighpass::State<T> dcFilterStateTri;
  196. OnePoleHighpass::State<T> dcFilterStateSin;
  197. MinBlepBuffer<16*2, T> sqrMinBlep;
  198. MinBlepBuffer<16*2, T> sawMinBlep;
  199. MinBlepBuffer<16*2, T> sinMinBlep;
  200. void setSampleTime(float sampleTime) {
  201. dcFilter.setCutoff(std::min(0.4f, 20.f * sampleTime));
  202. }
  203. struct Frame {
  204. /** Number of channels valid in SIMD type
  205. For optimizing serial operations.
  206. */
  207. uint8_t channels = 0;
  208. bool soft = false;
  209. bool syncEnabled = false;
  210. bool sqrEnabled = false;
  211. bool sawEnabled = false;
  212. bool triEnabled = false;
  213. bool sinEnabled = false;
  214. T pulseWidth = 0.5f;
  215. T sync = 0.f;
  216. T freq = 0.f;
  217. // Outputs
  218. T sqr = 0.f;
  219. T saw = 0.f;
  220. T tri = 0.f;
  221. T sin = 0.f;
  222. };
  223. void process(Frame& frame, float sampleTime) {
  224. // Compute deltaPhase for full frame
  225. T deltaPhase = simd::clamp(frame.freq * sampleTime, 0.f, 0.49f);
  226. if (frame.soft) {
  227. deltaPhase *= syncDirection;
  228. }
  229. else {
  230. syncDirection = 1.f;
  231. }
  232. T prevPhase = phase;
  233. const float pwMin = 0.01f;
  234. T pulseWidth = simd::clamp(frame.pulseWidth, pwMin, 1.f - pwMin);
  235. // Inserts minBLEP for each channel where mask is true.
  236. // Serial but does nothing if mask is all zero.
  237. auto insertMinBlep = [&](T mask, T p, T magnitude, MinBlepBuffer<16*2, T>& minBlepBuffer) {
  238. int m = simd::movemask(mask);
  239. if (!m)
  240. return;
  241. float* buffer = (float*) minBlepBuffer.startData();
  242. for (int i = 0; i < frame.channels; i++) {
  243. if (m & (1 << i)) {
  244. minBlep.insertDiscontinuity(&buffer[i], 4, p[i], magnitude[i]);
  245. }
  246. }
  247. };
  248. // Computes subsample time where phase crosses threshold.
  249. auto getCrossing = [](T thresholdPhase, T startPhase, T endPhase, T startSubsample, T endSubsample) -> T {
  250. T delta = endPhase - startPhase;
  251. T diff = thresholdPhase - startPhase;
  252. // Forward: wrap thresholdPhase to (startPhase, startPhase+1]
  253. // Backward: wrap thresholdPhase to [startPhase-1, startPhase)
  254. thresholdPhase -= simd::ifelse(delta >= 0.f, simd::floor(diff), simd::ceil(diff));
  255. T p = (thresholdPhase - startPhase) / delta;
  256. return startSubsample + p * (endSubsample - startSubsample);
  257. };
  258. // Processes wrap/pulse/saw crossings between startPhase and endPhase.
  259. // startSubsample and endSubsample define the time range within the frame.
  260. // channelMask limits processing to specific channels.
  261. auto processCrossings = [&](T startPhase, T endPhase, T startSubsample, T endSubsample, T channelMask) {
  262. if (frame.sqrEnabled || frame.triEnabled) {
  263. // Insert minBLEP to square when phase crosses 0 (mod 1)
  264. T wrapSubsample = getCrossing(1.f, startPhase, endPhase, startSubsample, endSubsample);
  265. T wrapMask = channelMask & (startSubsample < wrapSubsample) & (wrapSubsample <= endSubsample);
  266. insertMinBlep(wrapMask, wrapSubsample, 2.f * syncDirection, sqrMinBlep);
  267. // Insert minBLEP to square when phase crosses pulse width
  268. T pulseSubsample = getCrossing(pulseWidth, startPhase, endPhase, startSubsample, endSubsample);
  269. T pulseMask = channelMask & (startSubsample < pulseSubsample) & (pulseSubsample <= endSubsample);
  270. insertMinBlep(pulseMask, pulseSubsample, -2.f * syncDirection, sqrMinBlep);
  271. }
  272. if (frame.sawEnabled) {
  273. // Insert minBLEP to saw when crossing 0.5
  274. T sawSubsample = getCrossing(0.5f, startPhase, endPhase, startSubsample, endSubsample);
  275. T sawMask = channelMask & (startSubsample < sawSubsample) & (sawSubsample <= endSubsample);
  276. insertMinBlep(sawMask, sawSubsample, -2.f * syncDirection, sawMinBlep);
  277. }
  278. };
  279. // Check if square value changed due to pulseWidth changing since last frame
  280. if (frame.sqrEnabled || frame.triEnabled) {
  281. T sqrState = simd::ifelse(prevPhase < pulseWidth, 1.f, -1.f);
  282. T changed = (sqrState != lastSqrState);
  283. T magnitude = sqrState - lastSqrState;
  284. insertMinBlep(changed, 1e-6f, magnitude, sqrMinBlep);
  285. }
  286. if (!frame.syncEnabled) {
  287. // No sync. Process full frame
  288. T endPhase = prevPhase + deltaPhase;
  289. processCrossings(prevPhase, endPhase, 0.f, 1.f, T::mask());
  290. phase = endPhase;
  291. }
  292. else {
  293. // Compute sync subsample position
  294. T deltaSync = frame.sync - lastSync;
  295. T syncSubsample = -lastSync / deltaSync;
  296. lastSync = frame.sync;
  297. // Check if sync rises through 0
  298. T syncOccurred = (0.f < syncSubsample) & (syncSubsample <= 1.f) & (deltaSync >= 0.f);
  299. T noSync = ~syncOccurred;
  300. if (simd::movemask(noSync)) {
  301. // No sync for these channels. Process full frame
  302. T endPhase = prevPhase + deltaPhase;
  303. processCrossings(prevPhase, endPhase, 0.f, 1.f, noSync);
  304. phase = simd::ifelse(noSync, endPhase, phase);
  305. }
  306. if (simd::movemask(syncOccurred)) {
  307. // Process crossings before sync
  308. T syncPhase = prevPhase + deltaPhase * syncSubsample;
  309. processCrossings(prevPhase, syncPhase, 0.f, syncSubsample, syncOccurred);
  310. // Wrap sync phase
  311. syncPhase -= simd::floor(syncPhase);
  312. if (frame.soft) {
  313. // Soft sync: Reverse direction, continue from syncPhase
  314. syncDirection = simd::ifelse(syncOccurred, -syncDirection, syncDirection);
  315. T endPhase = syncPhase + (-deltaPhase) * (1.f - syncSubsample);
  316. processCrossings(syncPhase, endPhase, syncSubsample, 1.f, syncOccurred);
  317. phase = simd::ifelse(syncOccurred, endPhase, phase);
  318. }
  319. else {
  320. // Hard sync: Reset phase from syncPhase to 0 at syncSubsample, insert discontinuities
  321. if (frame.sqrEnabled || frame.triEnabled) {
  322. // Check if square jumps from -1 to +1
  323. T sqrJump = (syncPhase >= pulseWidth);
  324. insertMinBlep(syncOccurred & sqrJump, syncSubsample, 2.f, sqrMinBlep);
  325. }
  326. if (frame.triEnabled) {
  327. // TODO Insert minBLEP to Tri
  328. }
  329. if (frame.sawEnabled) {
  330. // Saw jumps from saw(syncPhase) to saw(0) = 0
  331. insertMinBlep(syncOccurred, syncSubsample, -saw(syncPhase), sawMinBlep);
  332. }
  333. if (frame.sinEnabled) {
  334. // sin jumps from sin(syncPhase) to sin(0) = 0
  335. insertMinBlep(syncOccurred, syncSubsample, -sin(syncPhase), sinMinBlep);
  336. }
  337. // Process crossings after sync (starting from phase 0)
  338. T endPhase = deltaPhase * (1.f - syncSubsample);
  339. processCrossings(0.f, endPhase, syncSubsample, 1.f, syncOccurred);
  340. phase = simd::ifelse(syncOccurred, endPhase, phase);
  341. }
  342. }
  343. }
  344. // Wrap phase to [0, 1)
  345. phase -= simd::floor(phase);
  346. // Generate outputs
  347. if (frame.sawEnabled) {
  348. frame.saw = saw(phase);
  349. frame.saw += sawMinBlep.shift();
  350. frame.saw = dcFilter.process(dcFilterStateSaw, frame.saw);
  351. }
  352. if (frame.sqrEnabled || frame.triEnabled) {
  353. frame.sqr = simd::ifelse(phase < pulseWidth, 1.f, -1.f);
  354. lastSqrState = frame.sqr;
  355. frame.sqr += sqrMinBlep.shift();
  356. T triSqr = frame.sqr;
  357. frame.sqr = dcFilter.process(dcFilterStateSqr, frame.sqr);
  358. if (frame.triEnabled) {
  359. // Integrate square wave
  360. const float triShape = 0.2f;
  361. T triFreq = sampleTime * triShape * frame.freq;
  362. // Use bilinear transform to derive alpha
  363. T alpha = 1 / (1 + 1 / (M_PI * triFreq));
  364. // T alpha = 1.f - simd::exp(-2.f * M_PI * triFreq);
  365. triFilterState += alpha * (triSqr - triFilterState);
  366. // Apply gain to roughly have unit amplitude at 0.5 pulseWidth. Depends on triShape.
  367. frame.tri = triFilterState * 6.6f;
  368. frame.tri = dcFilter.process(dcFilterStateTri, frame.tri);
  369. }
  370. }
  371. if (frame.sinEnabled) {
  372. frame.sin = sin(phase);
  373. frame.sin += sinMinBlep.shift();
  374. frame.sin = dcFilter.process(dcFilterStateSin, frame.sin);
  375. }
  376. }
  377. T light() const {
  378. return sin(phase);
  379. }
  380. static T saw(T phase) {
  381. T x = phase + 0.5f;
  382. x -= simd::trunc(x);
  383. return 2 * x - 1;
  384. }
  385. static T tri(T phase) {
  386. return 1 - 4 * simd::fmin(simd::fabs(phase - 0.25f), simd::fabs(phase - 1.25f));
  387. }
  388. static T sin(T phase) {
  389. return sin_pi_9(1 - 2 * phase);
  390. }
  391. };
  392. using simd::float_4;
  393. struct VCO : Module {
  394. enum ParamIds {
  395. MODE_PARAM, // removed
  396. SYNC_PARAM,
  397. FREQ_PARAM,
  398. FINE_PARAM, // removed
  399. FM_PARAM,
  400. PW_PARAM,
  401. PW_CV_PARAM,
  402. // new in 2.0
  403. LINEAR_PARAM,
  404. NUM_PARAMS
  405. };
  406. enum InputIds {
  407. PITCH_INPUT,
  408. FM_INPUT,
  409. SYNC_INPUT,
  410. PW_INPUT,
  411. NUM_INPUTS
  412. };
  413. enum OutputIds {
  414. SIN_OUTPUT,
  415. TRI_OUTPUT,
  416. SAW_OUTPUT,
  417. SQR_OUTPUT,
  418. NUM_OUTPUTS
  419. };
  420. enum LightIds {
  421. ENUMS(PHASE_LIGHT, 3),
  422. LINEAR_LIGHT,
  423. SOFT_LIGHT,
  424. NUM_LIGHTS
  425. };
  426. VCOProcessor<float_4> processors[4];
  427. dsp::ClockDivider lightDivider;
  428. VCO() {
  429. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  430. configSwitch(LINEAR_PARAM, 0.f, 1.f, 0.f, "FM mode", {"1V/octave", "Linear"});
  431. configSwitch(SYNC_PARAM, 0.f, 1.f, 1.f, "Sync mode", {"Soft", "Hard"});
  432. configParam(FREQ_PARAM, -75.f, 75.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
  433. configParam(FM_PARAM, -1.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f);
  434. getParamQuantity(FM_PARAM)->randomizeEnabled = false;
  435. configParam(PW_PARAM, 0.01f, 0.99f, 0.5f, "Pulse width", "%", 0.f, 100.f);
  436. configParam(PW_CV_PARAM, -1.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f);
  437. getParamQuantity(PW_CV_PARAM)->randomizeEnabled = false;
  438. configInput(PITCH_INPUT, "1V/octave pitch");
  439. configInput(FM_INPUT, "Frequency modulation");
  440. configInput(SYNC_INPUT, "Sync");
  441. configInput(PW_INPUT, "Pulse width modulation");
  442. configOutput(SIN_OUTPUT, "Sine");
  443. configOutput(TRI_OUTPUT, "Triangle");
  444. configOutput(SAW_OUTPUT, "Sawtooth");
  445. configOutput(SQR_OUTPUT, "Square");
  446. lightDivider.setDivision(16);
  447. }
  448. void onSampleRateChange(const SampleRateChangeEvent& e) override {
  449. for (int c = 0; c < 16; c += 4) {
  450. processors[c / 4].setSampleTime(e.sampleTime);
  451. }
  452. }
  453. void process(const ProcessArgs& args) override {
  454. VCOProcessor<float_4>::Frame frame;
  455. float freqParam = params[FREQ_PARAM].getValue() / 12.f;
  456. float fmParam = params[FM_PARAM].getValue();
  457. float pwParam = params[PW_PARAM].getValue();
  458. float pwCvParam = params[PW_CV_PARAM].getValue();
  459. bool linear = params[LINEAR_PARAM].getValue() > 0.f;
  460. frame.soft = params[SYNC_PARAM].getValue() <= 0.f;
  461. frame.syncEnabled = inputs[SYNC_INPUT].isConnected();
  462. frame.sqrEnabled = outputs[SQR_OUTPUT].isConnected();
  463. frame.sawEnabled = outputs[SAW_OUTPUT].isConnected();
  464. frame.triEnabled = outputs[TRI_OUTPUT].isConnected();
  465. frame.sinEnabled = outputs[SIN_OUTPUT].isConnected();
  466. int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1);
  467. for (int c = 0; c < channels; c += 4) {
  468. frame.channels = std::min(channels - c, 4);
  469. // Get frequency
  470. float_4 pitch = freqParam + inputs[PITCH_INPUT].getPolyVoltageSimd<float_4>(c);
  471. float_4 freq;
  472. if (!linear) {
  473. pitch += inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
  474. freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
  475. }
  476. else {
  477. freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch);
  478. freq += dsp::FREQ_C4 * inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
  479. }
  480. frame.freq = clamp(freq, 0.f, args.sampleRate / 2.f);
  481. // Get pulse width
  482. frame.pulseWidth = pwParam + inputs[PW_INPUT].getPolyVoltageSimd<float_4>(c) / 10.f * pwCvParam;
  483. frame.sync = inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c);
  484. processors[c / 4].process(frame, args.sampleTime);
  485. // Set output
  486. outputs[SQR_OUTPUT].setVoltageSimd(5.f * frame.sqr, c);
  487. outputs[SAW_OUTPUT].setVoltageSimd(5.f * frame.saw, c);
  488. outputs[TRI_OUTPUT].setVoltageSimd(5.f * frame.tri, c);
  489. outputs[SIN_OUTPUT].setVoltageSimd(5.f * frame.sin, c);
  490. }
  491. outputs[SIN_OUTPUT].setChannels(channels);
  492. outputs[TRI_OUTPUT].setChannels(channels);
  493. outputs[SAW_OUTPUT].setChannels(channels);
  494. outputs[SQR_OUTPUT].setChannels(channels);
  495. // Light
  496. if (lightDivider.process()) {
  497. if (channels == 1) {
  498. float lightValue = processors[0].light()[0];
  499. lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision());
  500. lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision());
  501. lights[PHASE_LIGHT + 2].setBrightness(0.f);
  502. }
  503. else {
  504. lights[PHASE_LIGHT + 0].setBrightness(0.f);
  505. lights[PHASE_LIGHT + 1].setBrightness(0.f);
  506. lights[PHASE_LIGHT + 2].setBrightness(1.f);
  507. }
  508. lights[LINEAR_LIGHT].setBrightness(linear);
  509. lights[SOFT_LIGHT].setBrightness(frame.soft);
  510. }
  511. }
  512. };
  513. struct VCOWidget : ModuleWidget {
  514. VCOWidget(VCO* module) {
  515. setModule(module);
  516. setPanel(createPanel(asset::plugin(pluginInstance, "res/VCO.svg"), asset::plugin(pluginInstance, "res/VCO-dark.svg")));
  517. addChild(createWidget<ThemedScrew>(Vec(RACK_GRID_WIDTH, 0)));
  518. addChild(createWidget<ThemedScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  519. addChild(createWidget<ThemedScrew>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  520. addChild(createWidget<ThemedScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  521. addParam(createParamCentered<RoundHugeBlackKnob>(mm2px(Vec(22.905, 29.808)), module, VCO::FREQ_PARAM));
  522. addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(22.862, 56.388)), module, VCO::PW_PARAM));
  523. addParam(createParamCentered<Trimpot>(mm2px(Vec(6.607, 80.603)), module, VCO::FM_PARAM));
  524. addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM, VCO::LINEAR_LIGHT));
  525. addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM, VCO::SOFT_LIGHT));
  526. addParam(createParamCentered<Trimpot>(mm2px(Vec(39.118, 80.603)), module, VCO::PW_CV_PARAM));
  527. addInput(createInputCentered<ThemedPJ301MPort>(mm2px(Vec(6.607, 96.859)), module, VCO::FM_INPUT));
  528. addInput(createInputCentered<ThemedPJ301MPort>(mm2px(Vec(17.444, 96.859)), module, VCO::PITCH_INPUT));
  529. addInput(createInputCentered<ThemedPJ301MPort>(mm2px(Vec(28.282, 96.859)), module, VCO::SYNC_INPUT));
  530. addInput(createInputCentered<ThemedPJ301MPort>(mm2px(Vec(39.15, 96.859)), module, VCO::PW_INPUT));
  531. addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(6.607, 113.115)), module, VCO::SIN_OUTPUT));
  532. addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(17.444, 113.115)), module, VCO::TRI_OUTPUT));
  533. addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(28.282, 113.115)), module, VCO::SAW_OUTPUT));
  534. addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(39.119, 113.115)), module, VCO::SQR_OUTPUT));
  535. addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(31.089, 16.428)), module, VCO::PHASE_LIGHT));
  536. }
  537. };
  538. Model* modelVCO = createModel<VCO, VCOWidget>("VCO");