diff --git a/src/LFO.cpp b/src/LFO.cpp index b591485..6fe490b 100644 --- a/src/LFO.cpp +++ b/src/LFO.cpp @@ -4,100 +4,24 @@ using simd::float_4; -template -struct LowFrequencyOscillator { - T phase = 0.f; - T pw = 0.5f; - T freq = 1.f; - bool invert = false; - bool bipolar = false; - T resetState = T::mask(); - - void setPitch(T pitch) { - pitch = simd::fmin(pitch, 10.f); - freq = dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f); - } - void setPulseWidth(T pw) { - const T pwMin = 0.01f; - this->pw = clamp(pw, pwMin, 1.f - pwMin); - } - void setReset(T reset) { - reset = simd::rescale(reset, 0.1f, 2.f, 0.f, 1.f); - T on = (reset >= 1.f); - T off = (reset <= 0.f); - T triggered = ~resetState & on; - resetState = simd::ifelse(off, 0.f, resetState); - resetState = simd::ifelse(on, T::mask(), resetState); - phase = simd::ifelse(triggered, 0.f, phase); - } - void step(float dt) { - T deltaPhase = simd::fmin(freq * dt, 0.5f); - phase += deltaPhase; - phase -= (phase >= 1.f) & 1.f; - } - T sin() { - T p = phase; - if (!bipolar) - p -= 0.25f; - T v = simd::sin(2 * M_PI * p); - if (invert) - v *= -1.f; - if (!bipolar) - v += 1.f; - return v; - } - T tri() { - T p = phase; - if (bipolar) - p += 0.25f; - T v = 4.f * simd::fabs(p - simd::round(p)) - 1.f; - if (invert) - v *= -1.f; - if (!bipolar) - v += 1.f; - return v; - } - T saw() { - T p = phase; - if (!bipolar) - p -= 0.5f; - T v = 2.f * (p - simd::round(p)); - if (invert) - v *= -1.f; - if (!bipolar) - v += 1.f; - return v; - } - T sqr() { - T v = simd::ifelse(phase < pw, 1.f, -1.f); - if (invert) - v *= -1.f; - if (!bipolar) - v += 1.f; - return v; - } - T light() { - return simd::sin(2 * T(M_PI) * phase); - } -}; - - struct LFO : Module { enum ParamIds { OFFSET_PARAM, INVERT_PARAM, FREQ_PARAM, - FM1_PARAM, + FM_PARAM, FM2_PARAM, // removed PW_PARAM, PWM_PARAM, NUM_PARAMS }; enum InputIds { - FM1_INPUT, + FM_INPUT, FM2_INPUT, // removed RESET_INPUT, PW_INPUT, + // added in 2.0 + CLOCK_INPUT, NUM_INPUTS }; enum OutputIds { @@ -109,72 +33,161 @@ struct LFO : Module { }; enum LightIds { ENUMS(PHASE_LIGHT, 3), + INVERT_LIGHT, + OFFSET_LIGHT, NUM_LIGHTS }; - LowFrequencyOscillator oscillators[4]; + bool offset = false; + bool invert = false; + + float_4 phases[4]; + dsp::TSchmittTrigger clockTriggers[4]; + dsp::TSchmittTrigger resetTriggers[4]; + dsp::SchmittTrigger clockTrigger; + float clockFreq = 1.f; + dsp::Timer clockTimer; + + dsp::BooleanTrigger offsetTrigger; + dsp::BooleanTrigger invertTrigger; dsp::ClockDivider lightDivider; LFO() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configSwitch(OFFSET_PARAM, 0.f, 1.f, 1.f, "Offset", {"Bipolar", "Unipolar"}); - configSwitch(INVERT_PARAM, 0.f, 1.f, 1.f, "Orientation", {"Inverted", "Normal"}); + configButton(OFFSET_PARAM, "Offset 0-10V"); + configButton(INVERT_PARAM, "Invert"); configParam(FREQ_PARAM, -8.f, 10.f, 1.f, "Frequency", " Hz", 2, 1); - configParam(FM1_PARAM, 0.f, 1.f, 0.f, "Frequency modulation 1", "%", 0.f, 100.f); + 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(FM2_PARAM, 0.f, 1.f, 0.f, "Frequency modulation 2", "%", 0.f, 100.f); - configParam(PWM_PARAM, 0.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f); - configInput(FM1_INPUT, "Frequency modulation 1"); - configInput(FM2_INPUT, "Frequency modulation 2"); + configParam(PWM_PARAM, -1.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f); + getParamQuantity(PWM_PARAM)->randomizeEnabled = false; + + configInput(FM_INPUT, "Frequency modulation"); + configInput(CLOCK_INPUT, "Clock"); configInput(RESET_INPUT, "Reset"); configInput(PW_INPUT, "Pulse width modulation"); + configOutput(SIN_OUTPUT, "Sine"); configOutput(TRI_OUTPUT, "Triangle"); configOutput(SAW_OUTPUT, "Sawtooth"); configOutput(SQR_OUTPUT, "Square"); + configLight(PHASE_LIGHT, "Phase"); - lightInfos[PHASE_LIGHT]->description = "Tracks the sine output.\nGreen if positive, red if negative, blue if polyphonic."; lightDivider.setDivision(16); + onReset(); + } + + void onReset() override { + offset = false; + invert = false; + for (int c = 0; c < 16; c += 4) { + phases[c / 4] = 0.f; + } + clockFreq = 1.f; + clockTimer.reset(); } void process(const ProcessArgs& args) override { float freqParam = params[FREQ_PARAM].getValue(); - float fm1Param = params[FM1_PARAM].getValue(); - float fm2Param = params[FM2_PARAM].getValue(); + float fmParam = params[FM_PARAM].getValue(); float pwParam = params[PW_PARAM].getValue(); float pwmParam = params[PWM_PARAM].getValue(); - int channels = std::max(1, inputs[FM1_INPUT].getChannels()); + // Buttons + if (offsetTrigger.process(params[OFFSET_PARAM].getValue() > 0.f)) { + offset ^= true; + } + if (invertTrigger.process(params[INVERT_PARAM].getValue() > 0.f)) { + invert ^= true; + } - for (int c = 0; c < channels; c += 4) { - auto* oscillator = &oscillators[c / 4]; - oscillator->invert = (params[INVERT_PARAM].getValue() == 0.f); - oscillator->bipolar = (params[OFFSET_PARAM].getValue() == 0.f); + // Clock + if (inputs[CLOCK_INPUT].isConnected()) { + clockTimer.process(args.sampleTime); + if (clockTrigger.process(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 2.f)) { + float clockFreq = 1.f / clockTimer.getTime(); + clockTimer.reset(); + if (0.001f <= clockFreq && clockFreq <= 1000.f) { + this->clockFreq = clockFreq; + } + } + } + else { + // Default frequency when clock is unpatched + clockFreq = 2.f; + } + + int channels = std::max(1, inputs[FM_INPUT].getChannels()); + + for (int c = 0; c < channels; c += 4) { + // Pitch and frequency float_4 pitch = freqParam; - // FM1, polyphonic - pitch += inputs[FM1_INPUT].getVoltageSimd(c) * fm1Param; - // FM2, polyphonic or monophonic - pitch += inputs[FM2_INPUT].getPolyVoltageSimd(c) * fm2Param; - oscillator->setPitch(pitch); + pitch += inputs[FM_INPUT].getVoltageSimd(c) * fmParam; + float_4 freq = clockFreq / 2.f * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f); // Pulse width - float_4 pw = pwParam + inputs[PW_INPUT].getPolyVoltageSimd(c) / 10.f * pwmParam; - oscillator->setPulseWidth(pw); - - oscillator->step(args.sampleTime); - oscillator->setReset(inputs[RESET_INPUT].getPolyVoltageSimd(c)); - - // Outputs - 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); + float_4 pw = pwParam; + pw += inputs[PW_INPUT].getPolyVoltageSimd(c) / 10.f * pwmParam; + pw = clamp(pw, 0.01f, 0.99f); + + // Advance phase + float_4 deltaPhase = simd::fmin(freq * args.sampleTime, 0.5f); + phases[c / 4] += deltaPhase; + phases[c / 4] -= simd::trunc(phases[c / 4]); + + // Reset + float_4 reset = inputs[RESET_INPUT].getPolyVoltageSimd(c); + float_4 resetTriggered = resetTriggers[c / 4].process(reset, 0.1f, 2.f); + phases[c / 4] = simd::ifelse(resetTriggered, 0.f, phases[c / 4]); + + // Sine + if (outputs[SIN_OUTPUT].isConnected()) { + float_4 p = phases[c / 4]; + if (offset) + p -= 0.25f; + float_4 v = simd::sin(2 * M_PI * p); + if (invert) + v *= -1.f; + if (offset) + v += 1.f; + outputs[SIN_OUTPUT].setVoltageSimd(5.f * v, c); + } + // Triangle + if (outputs[TRI_OUTPUT].isConnected()) { + float_4 p = phases[c / 4]; + if (!offset) + p += 0.25f; + float_4 v = 4.f * simd::fabs(p - simd::round(p)) - 1.f; + if (invert) + v *= -1.f; + if (offset) + v += 1.f; + outputs[TRI_OUTPUT].setVoltageSimd(5.f * v, c); + } + // Sawtooth + if (outputs[SAW_OUTPUT].isConnected()) { + float_4 p = phases[c / 4]; + if (offset) + p -= 0.5f; + float_4 v = 2.f * (p - simd::round(p)); + if (invert) + v *= -1.f; + if (offset) + v += 1.f; + outputs[SAW_OUTPUT].setVoltageSimd(5.f * v, c); + } + // Square + if (outputs[SQR_OUTPUT].isConnected()) { + float_4 v = simd::ifelse(phases[c / 4] < pw, 1.f, -1.f); + if (invert) + v *= -1.f; + if (offset) + v += 1.f; + outputs[SQR_OUTPUT].setVoltageSimd(5.f * v, c); + } } outputs[SIN_OUTPUT].setChannels(channels); @@ -185,9 +198,9 @@ struct LFO : Module { // Light if (lightDivider.process()) { if (channels == 1) { - float lightValue = oscillators[0].light().s[0]; - lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision()); - lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision()); + float b = 1.f - phases[0][0]; + lights[PHASE_LIGHT + 0].setSmoothBrightness(b, args.sampleTime * lightDivider.getDivision()); + lights[PHASE_LIGHT + 1].setSmoothBrightness(b, args.sampleTime * lightDivider.getDivision()); lights[PHASE_LIGHT + 2].setBrightness(0.f); } else { @@ -195,6 +208,41 @@ struct LFO : Module { lights[PHASE_LIGHT + 1].setBrightness(0.f); lights[PHASE_LIGHT + 2].setBrightness(1.f); } + lights[OFFSET_LIGHT].setBrightness(offset); + lights[INVERT_LIGHT].setBrightness(invert); + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + // offset + json_object_set_new(rootJ, "offset", json_boolean(offset)); + // invert + json_object_set_new(rootJ, "invert", json_boolean(invert)); + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + // offset + json_t* offsetJ = json_object_get(rootJ, "offset"); + if (offsetJ) + offset = json_boolean_value(offsetJ); + // invert + json_t* invertJ = json_object_get(rootJ, "invert"); + if (invertJ) + invert = json_boolean_value(invertJ); + } + + void paramsFromJson(json_t* rootJ) override { + Module::paramsFromJson(rootJ); + // In <2.0, OFFSET_PARAM and INVERT_PARAM were toggle switches instead of momentary buttons, so if params are on after deserializing, set boolean states instead. + if (params[OFFSET_PARAM].getValue() > 0.f) { + offset = true; + params[OFFSET_PARAM].setValue(0.f); + } + if (params[INVERT_PARAM].getValue() > 0.f) { + invert = true; + params[INVERT_PARAM].setValue(0.f); } } }; @@ -212,13 +260,13 @@ struct LFOWidget : ModuleWidget { addParam(createParamCentered(mm2px(Vec(22.902, 29.803)), module, LFO::FREQ_PARAM)); addParam(createParamCentered(mm2px(Vec(22.861, 56.388)), module, LFO::PW_PARAM)); - addParam(createParamCentered(mm2px(Vec(6.604, 80.603)), module, LFO::FM1_PARAM)); - // addParam(createParamCentered(mm2px(Vec(17.441, 80.603)), module, LFO::INV_PARAM)); - // addParam(createParamCentered(mm2px(Vec(28.279, 80.603)), module, LFO::OFST_PARAM)); + addParam(createParamCentered(mm2px(Vec(6.604, 80.603)), module, LFO::FM_PARAM)); + addParam(createLightParamCentered>>(mm2px(Vec(17.441, 80.603)), module, LFO::INVERT_PARAM, LFO::INVERT_LIGHT)); + addParam(createLightParamCentered>>(mm2px(Vec(28.279, 80.603)), module, LFO::OFFSET_PARAM, LFO::OFFSET_LIGHT)); addParam(createParamCentered(mm2px(Vec(39.116, 80.603)), module, LFO::PWM_PARAM)); - addInput(createInputCentered(mm2px(Vec(6.604, 96.859)), module, LFO::FM1_INPUT)); - // addInput(createInputCentered(mm2px(Vec(17.441, 96.859)), module, LFO::CLK_INPUT)); + addInput(createInputCentered(mm2px(Vec(6.604, 96.859)), module, LFO::FM_INPUT)); + addInput(createInputCentered(mm2px(Vec(17.441, 96.859)), module, LFO::CLOCK_INPUT)); addInput(createInputCentered(mm2px(Vec(28.279, 96.819)), module, LFO::RESET_INPUT)); addInput(createInputCentered(mm2px(Vec(39.116, 96.819)), module, LFO::PW_INPUT)); @@ -233,130 +281,3 @@ struct LFOWidget : ModuleWidget { Model* modelLFO = createModel("LFO"); - - -#if 0 -struct LFO2 : Module { - enum ParamIds { - OFFSET_PARAM, - INVERT_PARAM, - FREQ_PARAM, - WAVE_PARAM, - FM_PARAM, - NUM_PARAMS - }; - enum InputIds { - FM_INPUT, - RESET_INPUT, - WAVE_INPUT, - NUM_INPUTS - }; - enum OutputIds { - INTERP_OUTPUT, - NUM_OUTPUTS - }; - enum LightIds { - ENUMS(PHASE_LIGHT, 3), - NUM_LIGHTS - }; - - LowFrequencyOscillator oscillators[4]; - dsp::ClockDivider lightDivider; - - LFO2() { - config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configSwitch(OFFSET_PARAM, 0.f, 1.f, 1.f, "Offset", {"Bipolar", "Unipolar"}); - configSwitch(INVERT_PARAM, 0.f, 1.f, 1.f, "Orientation", {"Inverted", "Normal"}); - configParam(FREQ_PARAM, -8.f, 10.f, 1.f, "Frequency", " Hz", 2, 1); - configParam(WAVE_PARAM, 0.f, 3.f, 1.5f, "Wave"); - configParam(FM_PARAM, 0.f, 1.f, 1.f, "Frequency modulation", "%", 0.f, 100.f); - configInput(FM_INPUT, "Frequency modulation"); - configInput(RESET_INPUT, "Reset"); - configInput(WAVE_INPUT, "Wave type"); - configOutput(INTERP_OUTPUT, "Audio"); - configLight(PHASE_LIGHT, "Phase"); - lightInfos[PHASE_LIGHT]->description = "Tracks the sine output.\nGreen if positive, red if negative, blue if polyphonic."; - - lightDivider.setDivision(16); - } - - void process(const ProcessArgs& args) override { - float freqParam = params[FREQ_PARAM].getValue(); - float fmParam = params[FM_PARAM].getValue(); - float waveParam = params[WAVE_PARAM].getValue(); - - int channels = std::max(1, inputs[FM_INPUT].getChannels()); - - for (int c = 0; c < channels; c += 4) { - auto* oscillator = &oscillators[c / 4]; - oscillator->invert = (params[INVERT_PARAM].getValue() == 0.f); - oscillator->bipolar = (params[OFFSET_PARAM].getValue() == 0.f); - - float_4 pitch = freqParam + inputs[FM_INPUT].getVoltageSimd(c) * fmParam; - oscillator->setPitch(pitch); - - oscillator->step(args.sampleTime); - oscillator->setReset(inputs[RESET_INPUT].getPolyVoltageSimd(c)); - - // Outputs - if (outputs[INTERP_OUTPUT].isConnected()) { - 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[INTERP_OUTPUT].setVoltageSimd(5.f * v, c); - } - } - - outputs[INTERP_OUTPUT].setChannels(channels); - - // Light - if (lightDivider.process()) { - if (channels == 1) { - float lightValue = oscillators[0].light().s[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 LFO2Widget : ModuleWidget { - LFO2Widget(LFO2* module) { - setModule(module); - setPanel(createPanel(asset::plugin(pluginInstance, "res/WTLFO.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, LFO2::OFFSET_PARAM)); - addParam(createParam(Vec(62, 215), module, LFO2::INVERT_PARAM)); - - addParam(createParam(Vec(18, 60), module, LFO2::FREQ_PARAM)); - addParam(createParam(Vec(11, 142), module, LFO2::WAVE_PARAM)); - addParam(createParam(Vec(11, 207), module, LFO2::FM_PARAM)); - - addInput(createInput(Vec(11, 276), module, LFO2::FM_INPUT)); - addInput(createInput(Vec(54, 276), module, LFO2::RESET_INPUT)); - addInput(createInput(Vec(11, 319), module, LFO2::WAVE_INPUT)); - - addOutput(createOutput(Vec(54, 319), module, LFO2::INTERP_OUTPUT)); - - addChild(createLight>(Vec(68, 42.5f), module, LFO2::PHASE_LIGHT)); - } -}; - - -Model* modelLFO2 = createModel("LFO2"); -#endif \ No newline at end of file diff --git a/src/WTLFO.cpp b/src/WTLFO.cpp index fc45f49..d5ca482 100644 --- a/src/WTLFO.cpp +++ b/src/WTLFO.cpp @@ -96,6 +96,8 @@ struct WTLFO : Module { for (int c = 0; c < 16; c += 4) { phases[c / 4] = 0.f; } + clockFreq = 1.f; + clockTimer.reset(); } void onRandomize(const RandomizeEvent& e) override { @@ -137,7 +139,7 @@ struct WTLFO : Module { if (inputs[CLOCK_INPUT].isConnected()) { clockTimer.process(args.sampleTime); - if (clockTrigger.process(rescale(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 2.f, 0.f, 1.f))) { + if (clockTrigger.process(inputs[CLOCK_INPUT].getVoltage(), 0.1f, 2.f)) { float clockFreq = 1.f / clockTimer.getTime(); clockTimer.reset(); if (0.001f <= clockFreq && clockFreq <= 1000.f) { @@ -170,7 +172,7 @@ struct WTLFO : Module { for (int c = 0; c < channels; c += 4) { // Calculate frequency in Hz float_4 pitch = freqParam + inputs[FM_INPUT].getVoltageSimd(c) * fmParam; - float_4 freq = clockFreq / 2.f * simd::pow(2.f, pitch); + float_4 freq = clockFreq / 2.f * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f); freq = simd::fmin(freq, 1024.f); // Accumulate phase