#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); } enum WaveIds { SIN_WAVE = 1 << 0, TRI_WAVE = 1 << 1, SAW_WAVE = 1 << 2, SQR_WAVE = 1 << 3, ALL_WAVE = 0xffffffff }; 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; 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 setPitch(T pitch) { freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30) / 1073741824; } void setPulseWidth(T pulseWidth) { const float pwMin = 0.01f; this->pulseWidth = simd::clamp(pulseWidth, pwMin, 1.f - pwMin); } void process(float deltaTime, T syncValue, int enabledWaves) { // Do nothing if there aren't any waves to process if (!enabledWaves) return; // Advance phase T deltaPhase = simd::clamp(freq * deltaTime, 1e-6f, 0.35f); if (soft) { // Reverse direction deltaPhase *= syncDirection; } else { // Reset back to forward syncDirection = 1.f; } phase += deltaPhase; // Wrap phase phase -= simd::floor(phase); if (enabledWaves & SQR_WAVE) { // 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); } } } } if (enabledWaves & SAW_WAVE) { // 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 if (enabledWaves & SQR_WAVE) { sqrValue = sqr(phase); sqrValue += sqrMinBlep.process(); if (analog) { sqrFilter.setCutoffFreq(20.f * deltaTime); sqrFilter.process(sqrValue); sqrValue = sqrFilter.highpass() * 0.95f; } } // Saw if (enabledWaves & SAW_WAVE) { sawValue = saw(phase); sawValue += sawMinBlep.process(); } // Tri if (enabledWaves & TRI_WAVE) { triValue = tri(phase); triValue += triMinBlep.process(); } // Sin if (enabledWaves & SIN_WAVE) { 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, SYNC_PARAM, FREQ_PARAM, FINE_PARAM, FM_PARAM, PW_PARAM, PWM_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), NUM_LIGHTS }; VoltageControlledOscillator<16, 16, float_4> oscillators[4]; dsp::ClockDivider lightDivider; VCO() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configParam(MODE_PARAM, 0.f, 1.f, 1.f, "Analog mode"); configParam(SYNC_PARAM, 0.f, 1.f, 1.f, "Hard sync"); configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); configParam(FINE_PARAM, -1.f, 1.f, 0.f, "Fine frequency"); configParam(FM_PARAM, 0.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f); configParam(PW_PARAM, 0.01f, 0.99f, 0.5f, "Pulse width", "%", 0.f, 100.f); configParam(PWM_PARAM, 0.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f); lightDivider.setDivision(16); } void process(const ProcessArgs& args) override { int enabledWaves = 0; // Need to use int here becuase C++ if (outputs[SIN_OUTPUT].isConnected()) enabledWaves |= SIN_WAVE; if (outputs[TRI_OUTPUT].isConnected()) enabledWaves |= TRI_WAVE; if (outputs[SAW_OUTPUT].isConnected()) enabledWaves |= SAW_WAVE; if (outputs[SQR_OUTPUT].isConnected()) enabledWaves |= SQR_WAVE; if (!enabledWaves) return; float freqParam = params[FREQ_PARAM].getValue() / 12.f; freqParam += dsp::quadraticBipolar(params[FINE_PARAM].getValue()) * 3.f / 12.f; float fmParam = dsp::quadraticBipolar(params[FM_PARAM].getValue()); 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); oscillator->analog = params[MODE_PARAM].getValue() > 0.f; oscillator->soft = params[SYNC_PARAM].getValue() <= 0.f; float_4 pitch = freqParam; pitch += inputs[PITCH_INPUT].getVoltageSimd(c); if (inputs[FM_INPUT].isConnected()) { pitch += fmParam * inputs[FM_INPUT].getPolyVoltageSimd(c); } oscillator->setPitch(pitch); oscillator->setPulseWidth(params[PW_PARAM].getValue() + params[PWM_PARAM].getValue() * inputs[PW_INPUT].getPolyVoltageSimd(c) / 10.f); oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected(); oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd(c), enabledWaves); // 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); } } } }; struct VCOWidget : ModuleWidget { VCOWidget(VCO* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/VCO-1.svg"))); addChild(createWidget(Vec(15, 0))); addChild(createWidget(Vec(box.size.x - 30, 0))); addChild(createWidget(Vec(15, 365))); addChild(createWidget(Vec(box.size.x - 30, 365))); addParam(createParam(Vec(15, 77), module, VCO::MODE_PARAM)); addParam(createParam(Vec(119, 77), module, VCO::SYNC_PARAM)); addParam(createParam(Vec(47, 61), module, VCO::FREQ_PARAM)); addParam(createParam(Vec(23, 143), module, VCO::FINE_PARAM)); addParam(createParam(Vec(91, 143), module, VCO::PW_PARAM)); addParam(createParam(Vec(23, 208), module, VCO::FM_PARAM)); addParam(createParam(Vec(91, 208), module, VCO::PWM_PARAM)); addInput(createInput(Vec(11, 276), module, VCO::PITCH_INPUT)); addInput(createInput(Vec(45, 276), module, VCO::FM_INPUT)); addInput(createInput(Vec(80, 276), module, VCO::SYNC_INPUT)); addInput(createInput(Vec(114, 276), module, VCO::PW_INPUT)); addOutput(createOutput(Vec(11, 320), module, VCO::SIN_OUTPUT)); addOutput(createOutput(Vec(45, 320), module, VCO::TRI_OUTPUT)); addOutput(createOutput(Vec(80, 320), module, VCO::SAW_OUTPUT)); addOutput(createOutput(Vec(114, 320), module, VCO::SQR_OUTPUT)); addChild(createLight>(Vec(99, 42.5f), module, VCO::PHASE_LIGHT)); } }; Model* modelVCO = createModel("VCO"); struct VCO2 : Module { enum ParamIds { MODE_PARAM, SYNC_PARAM, FREQ_PARAM, WAVE_PARAM, FM_PARAM, NUM_PARAMS }; enum InputIds { FM_INPUT, SYNC_INPUT, WAVE_INPUT, NUM_INPUTS }; enum OutputIds { OUT_OUTPUT, NUM_OUTPUTS }; enum LightIds { ENUMS(PHASE_LIGHT, 3), NUM_LIGHTS }; VoltageControlledOscillator<8, 8, float_4> oscillators[4]; dsp::ClockDivider lightDivider; VCO2() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configParam(MODE_PARAM, 0.f, 1.f, 1.f, "Analog mode"); configParam(SYNC_PARAM, 0.f, 1.f, 1.f, "Hard sync"); configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); configParam(WAVE_PARAM, 0.f, 3.f, 1.5f, "Wave"); configParam(FM_PARAM, 0.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f); lightDivider.setDivision(16); } void process(const ProcessArgs& args) override { if (!outputs[OUT_OUTPUT].isConnected()) return; float freqParam = params[FREQ_PARAM].getValue() / 12.f; float fmParam = dsp::quadraticBipolar(params[FM_PARAM].getValue()); float waveParam = params[WAVE_PARAM].getValue(); int channels = std::max(inputs[FM_INPUT].getChannels(), 1); for (int c = 0; c < channels; c += 4) { auto* oscillator = &oscillators[c / 4]; oscillator->channels = std::min(channels - c, 4); oscillator->analog = (params[MODE_PARAM].getValue() > 0.f); oscillator->soft = (params[SYNC_PARAM].getValue() <= 0.f); float_4 pitch = freqParam; pitch += fmParam * inputs[FM_INPUT].getVoltageSimd(c); oscillator->setPitch(pitch); oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected(); oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd(c), ALL_WAVE); // Outputs float_4 wave = simd::clamp(waveParam + inputs[WAVE_INPUT].getPolyVoltageSimd(c) / 10.f * 3.f, 0.f, 3.f); float_4 v = 0.f; v += oscillator->sin() * simd::fmax(0.f, 1.f - simd::fabs(wave - 0.f)); v += oscillator->tri() * simd::fmax(0.f, 1.f - simd::fabs(wave - 1.f)); v += oscillator->saw() * simd::fmax(0.f, 1.f - simd::fabs(wave - 2.f)); v += oscillator->sqr() * simd::fmax(0.f, 1.f - simd::fabs(wave - 3.f)); outputs[OUT_OUTPUT].setVoltageSimd(5.f * v, c); } outputs[OUT_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); } } } }; struct VCO2Widget : ModuleWidget { VCO2Widget(VCO2* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/VCO-2.svg"))); addChild(createWidget(Vec(15, 0))); addChild(createWidget(Vec(box.size.x - 30, 0))); addChild(createWidget(Vec(15, 365))); addChild(createWidget(Vec(box.size.x - 30, 365))); addParam(createParam(Vec(62, 150), module, VCO2::MODE_PARAM)); addParam(createParam(Vec(62, 215), module, VCO2::SYNC_PARAM)); addParam(createParam(Vec(17, 60), module, VCO2::FREQ_PARAM)); addParam(createParam(Vec(12, 143), module, VCO2::WAVE_PARAM)); addParam(createParam(Vec(12, 208), module, VCO2::FM_PARAM)); addInput(createInput(Vec(11, 276), module, VCO2::FM_INPUT)); addInput(createInput(Vec(54, 276), module, VCO2::SYNC_INPUT)); addInput(createInput(Vec(11, 320), module, VCO2::WAVE_INPUT)); addOutput(createOutput(Vec(54, 320), module, VCO2::OUT_OUTPUT)); addChild(createLight>(Vec(68, 42.5f), module, VCO2::PHASE_LIGHT)); } }; Model* modelVCO2 = createModel("VCO2");