#include "plugin.hpp" using simd::float_4; // Accurate only on [0, 1] template T sin2pi_pade_05_7_6(T x) { x -= 0.5f; 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)) / (1 + T(0.953136) * simd::pow(x, 2) + T(0.430238) * simd::pow(x, 4) + T(0.0981408) * simd::pow(x, 6)); } template T sin2pi_pade_05_5_4(T x) { x -= 0.5f; return (T(-6.283185307) * x + T(33.19863968) * simd::pow(x, 3) - T(32.44191367) * simd::pow(x, 5)) / (1 + T(1.296008659) * simd::pow(x, 2) + T(0.7028072946) * simd::pow(x, 4)); } template T expCurve(T x) { return (3 + x * (-13 + 5 * x)) / (3 + 2 * x); } template struct VoltageControlledOscillator { bool analog = false; bool soft = false; bool syncEnabled = false; // For optimizing in serial code int channels = 0; T lastSyncValue = 0.f; T phase = 0.f; T freq = 0.f; T pulseWidth = 0.5f; T syncDirection = 1.f; dsp::TRCFilter sqrFilter; dsp::MinBlepGenerator sqrMinBlep; dsp::MinBlepGenerator sawMinBlep; dsp::MinBlepGenerator triMinBlep; dsp::MinBlepGenerator sinMinBlep; T sqrValue = 0.f; T sawValue = 0.f; T triValue = 0.f; T sinValue = 0.f; void setPulseWidth(T pulseWidth) { const float pwMin = 0.01f; this->pulseWidth = simd::clamp(pulseWidth, pwMin, 1.f - pwMin); } void process(float deltaTime, T syncValue) { // Advance phase T deltaPhase = simd::clamp(freq * deltaTime, 0.f, 0.35f); if (soft) { // Reverse direction deltaPhase *= syncDirection; } else { // Reset back to forward syncDirection = 1.f; } phase += deltaPhase; // Wrap phase phase -= simd::floor(phase); // Jump sqr when crossing 0, or 1 if backwards T wrapPhase = (syncDirection == -1.f) & 1.f; T wrapCrossing = (wrapPhase - (phase - deltaPhase)) / deltaPhase; int wrapMask = simd::movemask((0 < wrapCrossing) & (wrapCrossing <= 1.f)); if (wrapMask) { for (int i = 0; i < channels; i++) { if (wrapMask & (1 << i)) { T mask = simd::movemaskInverse(1 << i); float p = wrapCrossing[i] - 1.f; T x = mask & (2.f * syncDirection); sqrMinBlep.insertDiscontinuity(p, x); } } } // Jump sqr when crossing `pulseWidth` T pulseCrossing = (pulseWidth - (phase - deltaPhase)) / deltaPhase; int pulseMask = simd::movemask((0 < pulseCrossing) & (pulseCrossing <= 1.f)); if (pulseMask) { for (int i = 0; i < channels; i++) { if (pulseMask & (1 << i)) { T mask = simd::movemaskInverse(1 << i); float p = pulseCrossing[i] - 1.f; T x = mask & (-2.f * syncDirection); sqrMinBlep.insertDiscontinuity(p, x); } } } // Jump saw when crossing 0.5 T halfCrossing = (0.5f - (phase - deltaPhase)) / deltaPhase; int halfMask = simd::movemask((0 < halfCrossing) & (halfCrossing <= 1.f)); if (halfMask) { for (int i = 0; i < channels; i++) { if (halfMask & (1 << i)) { T mask = simd::movemaskInverse(1 << i); float p = halfCrossing[i] - 1.f; T x = mask & (-2.f * syncDirection); sawMinBlep.insertDiscontinuity(p, x); } } } // Detect sync // Might be NAN or outside of [0, 1) range if (syncEnabled) { T deltaSync = syncValue - lastSyncValue; T syncCrossing = -lastSyncValue / deltaSync; lastSyncValue = syncValue; T sync = (0.f < syncCrossing) & (syncCrossing <= 1.f) & (syncValue >= 0.f); int syncMask = simd::movemask(sync); if (syncMask) { if (soft) { syncDirection = simd::ifelse(sync, -syncDirection, syncDirection); } else { T newPhase = simd::ifelse(sync, (1.f - syncCrossing) * deltaPhase, phase); // Insert minBLEP for sync for (int i = 0; i < channels; i++) { if (syncMask & (1 << i)) { T mask = simd::movemaskInverse(1 << i); float p = syncCrossing[i] - 1.f; T x; x = mask & (sqr(newPhase) - sqr(phase)); sqrMinBlep.insertDiscontinuity(p, x); x = mask & (saw(newPhase) - saw(phase)); sawMinBlep.insertDiscontinuity(p, x); x = mask & (tri(newPhase) - tri(phase)); triMinBlep.insertDiscontinuity(p, x); x = mask & (sin(newPhase) - sin(phase)); sinMinBlep.insertDiscontinuity(p, x); } } phase = newPhase; } } } // Square sqrValue = sqr(phase); sqrValue += sqrMinBlep.process(); if (analog) { sqrFilter.setCutoffFreq(20.f * deltaTime); sqrFilter.process(sqrValue); sqrValue = sqrFilter.highpass() * 0.95f; } // Saw sawValue = saw(phase); sawValue += sawMinBlep.process(); // Tri triValue = tri(phase); triValue += triMinBlep.process(); // Sin sinValue = sin(phase); sinValue += sinMinBlep.process(); } T sin(T phase) { T v; if (analog) { // Quadratic approximation of sine, slightly richer harmonics T halfPhase = (phase < 0.5f); T x = phase - simd::ifelse(halfPhase, 0.25f, 0.75f); v = 1.f - 16.f * simd::pow(x, 2); v *= simd::ifelse(halfPhase, 1.f, -1.f); } else { v = sin2pi_pade_05_5_4(phase); // v = sin2pi_pade_05_7_6(phase); // v = simd::sin(2 * T(M_PI) * phase); } return v; } T sin() { return sinValue; } T tri(T phase) { T v; if (analog) { T x = phase + 0.25f; x -= simd::trunc(x); T halfX = (x >= 0.5f); x *= 2; x -= simd::trunc(x); v = expCurve(x) * simd::ifelse(halfX, 1.f, -1.f); } else { v = 1 - 4 * simd::fmin(simd::fabs(phase - 0.25f), simd::fabs(phase - 1.25f)); } return v; } T tri() { return triValue; } T saw(T phase) { T v; T x = phase + 0.5f; x -= simd::trunc(x); if (analog) { v = -expCurve(x); } else { v = 2 * x - 1; } return v; } T saw() { return sawValue; } T sqr(T phase) { T v = simd::ifelse(phase < pulseWidth, 1.f, -1.f); return v; } T sqr() { return sqrValue; } T light() { return simd::sin(2 * T(M_PI) * phase); } }; struct VCO : Module { enum ParamIds { MODE_PARAM, // removed SYNC_PARAM, FREQ_PARAM, FINE_PARAM, // removed FM_PARAM, PW_PARAM, PW_CV_PARAM, // new in 2.0 LINEAR_PARAM, NUM_PARAMS }; enum InputIds { PITCH_INPUT, FM_INPUT, SYNC_INPUT, PW_INPUT, NUM_INPUTS }; enum OutputIds { SIN_OUTPUT, TRI_OUTPUT, SAW_OUTPUT, SQR_OUTPUT, NUM_OUTPUTS }; enum LightIds { ENUMS(PHASE_LIGHT, 3), LINEAR_LIGHT, SOFT_LIGHT, NUM_LIGHTS }; VoltageControlledOscillator<16, 16, float_4> oscillators[4]; dsp::ClockDivider lightDivider; VCO() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configSwitch(LINEAR_PARAM, 0.f, 1.f, 0.f, "FM mode", {"1V/octave", "Linear"}); configSwitch(SYNC_PARAM, 0.f, 1.f, 1.f, "Sync mode", {"Soft", "Hard"}); configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); configParam(FM_PARAM, -1.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f); getParamQuantity(FM_PARAM)->randomizeEnabled = false; configParam(PW_PARAM, 0.01f, 0.99f, 0.5f, "Pulse width", "%", 0.f, 100.f); configParam(PW_CV_PARAM, -1.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f); getParamQuantity(PW_CV_PARAM)->randomizeEnabled = false; configInput(PITCH_INPUT, "1V/octave pitch"); configInput(FM_INPUT, "Frequency modulation"); configInput(SYNC_INPUT, "Sync"); configInput(PW_INPUT, "Pulse width modulation"); configOutput(SIN_OUTPUT, "Sine"); configOutput(TRI_OUTPUT, "Triangle"); configOutput(SAW_OUTPUT, "Sawtooth"); configOutput(SQR_OUTPUT, "Square"); lightDivider.setDivision(16); } void process(const ProcessArgs& args) override { float freqParam = params[FREQ_PARAM].getValue() / 12.f; float fmParam = params[FM_PARAM].getValue(); float pwParam = params[PW_PARAM].getValue(); float pwCvParam = params[PW_CV_PARAM].getValue(); bool linear = params[LINEAR_PARAM].getValue() > 0.f; bool soft = params[SYNC_PARAM].getValue() <= 0.f; int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1); for (int c = 0; c < channels; c += 4) { auto& oscillator = oscillators[c / 4]; oscillator.channels = std::min(channels - c, 4); // removed oscillator.analog = true; oscillator.soft = soft; // Get frequency float_4 pitch = freqParam + inputs[PITCH_INPUT].getPolyVoltageSimd(c); float_4 freq; if (!linear) { pitch += inputs[FM_INPUT].getPolyVoltageSimd(c) * fmParam; freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); } else { freq = dsp::FREQ_C4 * dsp::exp2_taylor5(pitch); freq += dsp::FREQ_C4 * inputs[FM_INPUT].getPolyVoltageSimd(c) * fmParam; } freq = clamp(freq, 0.f, args.sampleRate / 2.f); oscillator.freq = freq; // Get pulse width float_4 pw = pwParam + inputs[PW_INPUT].getPolyVoltageSimd(c) / 10.f * pwCvParam; oscillator.setPulseWidth(pw); oscillator.syncEnabled = inputs[SYNC_INPUT].isConnected(); float_4 sync = inputs[SYNC_INPUT].getPolyVoltageSimd(c); oscillator.process(args.sampleTime, sync); // Set output if (outputs[SIN_OUTPUT].isConnected()) outputs[SIN_OUTPUT].setVoltageSimd(5.f * oscillator.sin(), c); if (outputs[TRI_OUTPUT].isConnected()) outputs[TRI_OUTPUT].setVoltageSimd(5.f * oscillator.tri(), c); if (outputs[SAW_OUTPUT].isConnected()) outputs[SAW_OUTPUT].setVoltageSimd(5.f * oscillator.saw(), c); if (outputs[SQR_OUTPUT].isConnected()) outputs[SQR_OUTPUT].setVoltageSimd(5.f * oscillator.sqr(), c); } outputs[SIN_OUTPUT].setChannels(channels); outputs[TRI_OUTPUT].setChannels(channels); outputs[SAW_OUTPUT].setChannels(channels); outputs[SQR_OUTPUT].setChannels(channels); // Light if (lightDivider.process()) { if (channels == 1) { float lightValue = oscillators[0].light()[0]; lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision()); lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision()); lights[PHASE_LIGHT + 2].setBrightness(0.f); } else { lights[PHASE_LIGHT + 0].setBrightness(0.f); lights[PHASE_LIGHT + 1].setBrightness(0.f); lights[PHASE_LIGHT + 2].setBrightness(1.f); } lights[LINEAR_LIGHT].setBrightness(linear); lights[SOFT_LIGHT].setBrightness(soft); } } }; struct VCOWidget : ModuleWidget { VCOWidget(VCO* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/VCO.svg"), asset::plugin(pluginInstance, "res/VCO-dark.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(22.905, 29.808)), module, VCO::FREQ_PARAM)); addParam(createParamCentered(mm2px(Vec(22.862, 56.388)), module, VCO::PW_PARAM)); addParam(createParamCentered(mm2px(Vec(6.607, 80.603)), module, VCO::FM_PARAM)); addParam(createLightParamCentered>>(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM, VCO::LINEAR_LIGHT)); addParam(createLightParamCentered>>(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM, VCO::SOFT_LIGHT)); addParam(createParamCentered(mm2px(Vec(39.118, 80.603)), module, VCO::PW_CV_PARAM)); addInput(createInputCentered(mm2px(Vec(6.607, 96.859)), module, VCO::FM_INPUT)); addInput(createInputCentered(mm2px(Vec(17.444, 96.859)), module, VCO::PITCH_INPUT)); addInput(createInputCentered(mm2px(Vec(28.282, 96.859)), module, VCO::SYNC_INPUT)); addInput(createInputCentered(mm2px(Vec(39.15, 96.859)), module, VCO::PW_INPUT)); addOutput(createOutputCentered(mm2px(Vec(6.607, 113.115)), module, VCO::SIN_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(17.444, 113.115)), module, VCO::TRI_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(28.282, 113.115)), module, VCO::SAW_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(39.119, 113.115)), module, VCO::SQR_OUTPUT)); addChild(createLightCentered>(mm2px(Vec(31.089, 16.428)), module, VCO::PHASE_LIGHT)); } }; Model* modelVCO = createModel("VCO");