zynaddsubfx

ZynAddSubFX open source synthesizer
Log | Files | Refs | Submodules | LICENSE

commit 779305c4b2864f2eba119e95330ea3427d4ca238
parent aff608a91f62a5e4c17417d66e35fef97b2c162f
Author: friedolino78 <34608315+friedolino78@users.noreply.github.com>
Date:   Thu,  5 Aug 2021 23:43:53 +0200

Merge pull request #85 from friedolino78/ladder

Ladder
Diffstat:
Msrc/DSP/CMakeLists.txt | 1+
Msrc/DSP/Filter.cpp | 5+++++
Asrc/DSP/MoogFilter.cpp | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/DSP/MoogFilter.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Params/FilterParams.cpp | 19+++++++++++++++++--
Msrc/Synth/ModFilter.cpp | 11+++++++++++
Msrc/Synth/ModFilter.h | 1+
Msrc/UI/FilterUI.fl | 69++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/globals.h | 2++
9 files changed, 309 insertions(+), 19 deletions(-)

diff --git a/src/DSP/CMakeLists.txt b/src/DSP/CMakeLists.txt @@ -4,6 +4,7 @@ set(zynaddsubfx_dsp_SRCS DSP/Filter.cpp DSP/FormantFilter.cpp DSP/SVFilter.cpp + DSP/MoogFilter.cpp DSP/Unison.cpp DSP/Value_Smoothing_Filter.cpp PARENT_SCOPE diff --git a/src/DSP/Filter.cpp b/src/DSP/Filter.cpp @@ -19,6 +19,7 @@ #include "AnalogFilter.h" #include "FormantFilter.h" #include "SVFilter.h" +#include "MoogFilter.h" #include "../Params/FilterParams.h" #include "../Misc/Allocator.h" @@ -52,6 +53,10 @@ Filter *Filter::generate(Allocator &memory, const FilterParams *pars, if(filter->outgain > 1.0f) filter->outgain = sqrt(filter->outgain); break; + case 3: + filter = memory.alloc<MoogFilter>(Ftype, 1000.0f, pars->getq(), srate, bufsize); + filter->setgain(pars->getgain()); + break; default: filter = memory.alloc<AnalogFilter>(Ftype, 1000.0f, pars->getq(), Fstages, srate, bufsize); if((Ftype >= 6) && (Ftype <= 8)) diff --git a/src/DSP/MoogFilter.cpp b/src/DSP/MoogFilter.cpp @@ -0,0 +1,168 @@ +#include <cassert> +#include <cstdio> +#include <cmath> +#include <stdio.h> + +#include "../Misc/Util.h" +#include "MoogFilter.h" + +// theory from "THE ART OF VA FILTER DESIGN" +// by Vadim Zavalishin + +namespace zyn{ + +MoogFilter::MoogFilter(unsigned char Ftype, float Ffreq, float Fq, + unsigned int srate, int bufsize) + :Filter(srate, bufsize), sr(srate), gain(1.0f) +{ + setfreq_and_q(Ffreq/srate, Fq); + settype(Ftype); // q must be set before + + // initialize state but not to 0, in case of being used as denominator + for (unsigned int i = 0; i<(sizeof(state)/sizeof(*state)); i++) + { + state[i] = 0.0001f; + } +} + +MoogFilter::~MoogFilter(void) +{ + +} + +inline float MoogFilter::tanhX(const float x) const +{ + // Pade approximation of tanh(x) bound to [-1 .. +1] + // https://mathr.co.uk/blog/2017-09-06_approximating_hyperbolic_tangent.html + float x2 = x*x; + return (x*(105.0f+10.0f*x2)/(105.0f+(45.0f+x2)*x2)); +} + + +inline float MoogFilter::tanhXdivX(float x) const +{ + + //add DC offset to raise even harmonics (like transistor bias current) + x+= 0.1f; + // pre calc often used term + float x2 = x*x; + // Pade approximation for tanh(x)/x used in filter stages (5x per sample) + //~ return ((15.0+x2)/(15.0+6.0*x2)); + // faster approximation without division + // tuned for more distortion in self oscillation + return (1.0f-(0.35f*x2)+(0.06f*x2*x2)); +} + +inline float MoogFilter::step(float input) +{ + // transconductance + // gM(vIn) = tanh( vIn ) / vIn + float gm0 = tanhXdivX(state[0]); + // to ease calculations the others are 1.0 (so daturation) + // denominators + float d0 = 1.0f / (1.0f + c*gm0); + float dp1 = 1.0f / (1.0f + c); + float dp2 = dp1*dp1; + float dp3 = dp2*dp1; + + // instantaneous response estimate + float y3Estimate = + cp4 * d0 * gm0 * dp3 * input + + cp3 * gm0 * dp3 * d0 * state[0] + + cp2 * dp3 * state[1] + + c * dp2 * state[2] + + dp1 * state[3]; + + // mix input and gained feedback estimate for + // cheaper feedback gain compensation. Idea from + // Antti Huovilainen and Vesa Välimäki, "NEW APPROACHES TO DIGITAL SUBTRACTIVE SYNTHESIS" + float u = input - tanhX(feedbackGain * (y3Estimate - 0.5f*input)); + // output of all stages + float y0 = gm0 * d0 * (state[0] + c * u); + float y1 = dp1 * (state[1] + c * y0); + float y2 = dp1 * (state[2] + c * y1); + float y3 = dp1 * (state[3] + c * y2); + + // update state + state[0] += ct2 * (u - y0); + state[1] += ct2 * (y0 - y1); + state[2] += ct2 * (y1 - y2); + state[3] += ct2 * (y2 - y3); + + // calculate multimode filter output + return (a0 * u + + a1 * y0 + + a2 * y1 + + a3 * y2 + + a4 * y3); +} + +void MoogFilter::filterout(float *smp) +{ + for (int i = 0; i < buffersize; i ++) + { + smp[i] = step(tanhX(smp[i]*gain)); + smp[i] *= outgain; + } +} + +void MoogFilter::setfreq_and_q(float frequency, float q_) +{ + setfreq(frequency/sr); + setq(q_); +} + +inline float MoogFilter::tan_2(const float x) const +{ + //Pade approximation tan(x) hand tuned to map fCutoff + float x2 = x*x; + //~ return ((9.54f*x*((11.08f - x2)))/(105.0f - x2*(45.0f + x2))); // more accurate but instable at high frequencies + return (x+0.15f*x2+0.3f*x2*x2); // faster, no division (by zero) +} + +void MoogFilter::setfreq(float ff) +{ + // pre warp cutoff to map to reality + c = tan_2(PI * ff); + // limit cutoff to prevent overflow + c = limit(c,0.0006f,1.5f); + // pre calculate some stuff outside the hot zone + ct2 = c * 2.0f; + cp2 = c * c; + cp3 = cp2 * c; + cp4 = cp2 * cp2; +} + +void MoogFilter::setq(float q) +{ + // flattening the Q input + // self oscillation begins around 4.0 + // mapped to match the ANALOG filter class + feedbackGain = cbrtf(q/1000.0f)*4.0f + 0.3f; + // compensation factor for passband reduction by the negative feedback + passbandCompensation = 1.0f + limit(feedbackGain, 0.0f, 1.0f); +} + +void MoogFilter::setgain(float dBgain) +{ + gain = dB2rap(dBgain); +} + +void MoogFilter::settype(unsigned char ftype) +{ + switch (ftype) + { + case 0: + a0 = 1.0f; a1 =-4.0f; a2 = 6.0f; a3 =-4.0f; a4 = 1.0f; + break; + case 1: + a0 = 0.0f; a1 = 0.0f; a2 = 4.0f; a3 =-8.0f; a4 = 4.0f; + break; + case 2: + default: + a0 = 0.0f; a1 = 0.0f; a2 = 0.0f; a3 = 0.0f; a4 = passbandCompensation; + break; + } +} + +}; diff --git a/src/DSP/MoogFilter.h b/src/DSP/MoogFilter.h @@ -0,0 +1,52 @@ +/* + ZynAddSubFX - a software synthesizer + + Moog Filter.h - moog style multimode filter (lowpass, highpass...) + Copyright (C) 2020-2020 Michael Kirchner + Author: Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ + +#pragma once +#include "Filter.h" + +namespace zyn { + +class MoogFilter:public Filter +{ + public: + //! @param Fq resonance, range [0.1,1000], logscale + MoogFilter(unsigned char Ftype, float Ffreq, float Fq, + unsigned int srate, int bufsize); + ~MoogFilter() override; + void filterout(float *smp) override; + void setfreq(float /*frequency*/) override; + void setfreq_and_q(float frequency, float q_) override; + void setq(float /*q_*/) override; + void setgain(float dBgain) override; + void settype(unsigned char ftype); + + private: + unsigned sr; + float gain; + + float step(float x); + + float tanhXdivX(const float x) const; + float tanhX(const float x) const; + float tan_2(const float x) const; + + float feedbackGain; + // aN: multimode coefficients for LP,BP,HP configurations + float a0, a1, a2, a3, a4; + float state[4] = {0.0f,0.0f,0.0f,0.0f}; + float passbandCompensation; + // c*: cutoff frequency c and precalced products (times t) and powers(p) of it + float c, ct2, cp2, cp3, cp4; +}; + +} diff --git a/src/Params/FilterParams.cpp b/src/Params/FilterParams.cpp @@ -69,7 +69,7 @@ const rtosc::Ports FilterParams::ports = { rOptions(ad_global_filter, ad_voice_filter, sub_filter, in_effect), "location of the filter"), rOption(Pcategory, rShort("class"), - rOptions(analog, formant, st.var.), rDefault(analog), + rOptions(analog, formant, st.var., moog), rDefault(analog), "Class of filter"), rOption(Ptype, rShort("type"), rOptions(LP1, HP1, LP2, HP2, BP, notch, peak, l.shelf, h.shelf), @@ -131,7 +131,9 @@ const rtosc::Ports FilterParams::ports = { {"type-svf::i", rProp(parameter) rShort("type") rOptions(low, high, band, notch) rDoc("Filter Type"), 0, rOptionCb(Ptype)}, - + {"type-moog::i", rProp(parameter) rShort("type") + rOptions(HP, BP, LP) + rDoc("Filter Type"), 0, rOptionCb(Ptype)}, //UI reader {"Pvowels:", rDoc("Get Formant Vowels"), NULL, [](const char *, RtData &d) { @@ -207,6 +209,19 @@ const rtosc::Ports FilterParams::ports = { (float)obj->Pstages, cf.b[0], cf.b[1], cf.b[2], 0.0, -cf.a[1], -cf.a[2]); + } else if(obj->Pcategory == 3) { + int order = 0; + float gain = dB2rap(obj->getgain()); + if(obj->Ptype != 6 && obj->Ptype != 7 && obj->Ptype != 8) + gain = 1.0; + auto cf = AnalogFilter::computeCoeff(4-obj->Ptype, + Filter::getrealfreq(obj->getfreq()), + obj->getq(), obj->Pstages, + gain, 48000, order); + d.reply(d.loc, "fffffff", + (float)obj->Pstages, + cf.c[0], cf.c[1], cf.c[2], + 0.0, cf.d[1], cf.d[2]); } }}, // "", NULL, [](){}},"/freq" diff --git a/src/Synth/ModFilter.cpp b/src/Synth/ModFilter.cpp @@ -19,6 +19,7 @@ #include "../DSP/SVFilter.h" #include "../DSP/AnalogFilter.h" #include "../DSP/FormantFilter.h" +#include "../DSP/MoogFilter.h" #include <cassert> namespace zyn { @@ -124,6 +125,8 @@ static int current_category(Filter *f) return 1; else if(dynamic_cast<SVFilter*>(f)) return 2; + else if(dynamic_cast<MoogFilter*>(f)) + return 3; assert(false); return -1; @@ -146,6 +149,8 @@ void ModFilter::paramUpdate(Filter *&f) svParamUpdate(*sv); else if(auto *an = dynamic_cast<AnalogFilter*>(f)) anParamUpdate(*an); + else if(auto *mg = dynamic_cast<MoogFilter*>(f)) + mgParamUpdate(*mg); } void ModFilter::svParamUpdate(SVFilter &sv) @@ -161,4 +166,10 @@ void ModFilter::anParamUpdate(AnalogFilter &an) an.setgain(pars.getgain()); } +void ModFilter::mgParamUpdate(MoogFilter &mg) +{ + mg.settype(pars.Ptype); + mg.setgain(pars.getgain()); +} + } diff --git a/src/Synth/ModFilter.h b/src/Synth/ModFilter.h @@ -45,6 +45,7 @@ class ModFilter void paramUpdate(Filter *&f); void svParamUpdate(SVFilter &sv); void anParamUpdate(AnalogFilter &an); + void mgParamUpdate(MoogFilter &mg); const FilterParams &pars; //Parameters to Pull Updates From diff --git a/src/UI/FilterUI.fl b/src/UI/FilterUI.fl @@ -164,6 +164,24 @@ delete (formantparswindow);} {} label 1NF xywh {164 164 100 20} labelfont 1 labelsize 10 } + } Fl_Choice moogfiltertypechoice { + label FilterType + tooltip {The Filter type} xywh {10 50 50 15} down_box BORDER_BOX labelsize 10 align 5 textsize 10 + code1 {o->init("Ptype");} + class Fl_Osc_Choice + } { + MenuItem {} { + label LP + xywh {134 134 100 20} labelfont 1 labelsize 10 + } + MenuItem {} { + label HP + xywh {144 144 100 20} labelfont 1 labelsize 10 + } + MenuItem {} { + label BP + xywh {154 154 100 20} labelfont 1 labelsize 10 + } } Fl_Choice filtertype { label Category @@ -184,6 +202,10 @@ delete (formantparswindow);} {} label StVarF xywh {70 70 100 20} labelfont 1 labelsize 10 } + MenuItem {} { + label Moog + xywh {80 80 100 20} labelfont 1 labelsize 10 + } } Fl_Dial cfreqdial { label {C.Freq} @@ -436,10 +458,10 @@ formantfiltergraph->redraw();} //formant_q_dial->value(pars->Pvowels[nvowel].formants[nformant].q); //formant_amp_dial->value(pars->Pvowels[nvowel].formants[nformant].amp); if (nformant<numformants->value()) formantparsgroup->activate(); - else formantparsgroup->deactivate(); + else formantparsgroup->deactivate(); if (nseqpos<sequencesize->value()) vowel_counter->activate(); - else vowel_counter->deactivate(); + else vowel_counter->deactivate(); //vowel_counter->value(pars->Psequence[nseqpos].nvowel);} {} @@ -451,25 +473,38 @@ formantfiltergraph->redraw(); const int Pcategory = filtertype->value(); const int Ptype = analogfiltertypechoice->value(); -if (Pcategory==2) svfiltertypechoice->value(Ptype); -if (Pcategory==0) analogfiltertypechoice->value(Ptype); - const int categ=Pcategory; -if ((categ==0)||(categ==2)) { - if (categ==0) { - analogfiltertypechoice->show(); - svfiltertypechoice->hide(); - } else { - svfiltertypechoice->show(); - analogfiltertypechoice->hide(); - }; - editbutton->hide(); + +if (categ == 3) { + stcounter->hide(); // not (yet?) implemented for moog filter +} +else { + stcounter->show(); +} + +switch(categ) +{ + case 2: svfiltertypechoice->value(Ptype); break; + case 0: analogfiltertypechoice->value(Ptype); break; + case 3: moogfiltertypechoice->value(Ptype); break; +} + +analogfiltertypechoice->hide(); +svfiltertypechoice->hide(); +moogfiltertypechoice->hide(); + +if ((categ==0)||(categ==2)||(categ==3)) { + switch (categ) + { + case 0: analogfiltertypechoice->show(); break; + case 2: svfiltertypechoice->show(); break; + case 3: moogfiltertypechoice->show(); break; + } + editbutton->hide(); formantparswindow->hide(); cfreqdial->label("C.freq"); } else { - analogfiltertypechoice->hide(); - svfiltertypechoice->hide(); - editbutton->show(); + editbutton->show(); cfreqdial->label("BS.pos"); }; diff --git a/src/globals.h b/src/globals.h @@ -67,6 +67,7 @@ class Part; class Filter; class AnalogFilter; +class MoogFilter; class SVFilter; class FormantFilter; class ModFilter; @@ -178,6 +179,7 @@ typedef std::complex<fftw_real> fft_t; #define LOG_2 0.693147181f #define PI 3.1415926536f +#define PIDIV2 1.5707963268f #define LOG_10 2.302585093f /*