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.

728 lines
22KB

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