Book: The Art of VA Filter Design 2.1.2

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

andy_cytomic wrote:Having a few references to materials used doesn't seem to be too much problem for the following people
And Vadim stated that he would add references in the next revision.
give credit where it is due and show some respect to fellow authors.
In this context this is really a lesser value of references; you're drinking too much of the koolaid. I can give countless examples of synth diy folks reusing the work of others without significant reference. Often, the only "citation" is in the description, e.g. "this is a modification of Korg's MS-20 filter."

You've cited some of the most "academic" of authors, Tim, for example, holds a Ph.D. in mathematics, he drinks the coolaid as well. I'm not being critical of Tim in any way, mind you, I'm simply observing that he chooses to exercise a very academic approach and that not everyone that offers valuable contributions will work that way.

Rob Hordijk's very nice manual of synthesis, for example, doesn't include a single "reference" yet there is clear influence from some of the same sources.

http://rhordijk.home.xs4all.nl/G2Pages/index.htm

I want Vadim to continue working on his book, personally, however, I'd prefer that he focus his energy on improving the usefulness of the book. To that end, references that help elucidate ideas or give context to a discussion would be valuable, crediting Bob Moog, on the other hand, has less value and, for my taste, whenever he gets around to it is just fine.

Post

Thanks for the book!
Orion Platinum, Muzys 2

Post

Hello !

I have well understood the 2 ways to simulate a generic analog lowpass filter, using the classic bilinear transform to get direct forms, and the use of the BLT integrator with the technique to have zero delay feedback which is preserving the transfer function and the topology (TPT)

At one moment, the author explains how much the Direct Form 2 is less interesting than the TPT block scheme, with the example of one multiplication being after the integrator in the first case, leading to unwanted artefacts when any parameter is modulated. In the second case, the topology is preserved by design.

So, I would like to know something. What about the performance of the transposed direct form 2 ? If I have understood everything well, then we don't have there a multiplication being at the end of the block scheme, so we shouldn't have with it any artefact when a parameter is modulated. So, it is possible to say that TDF2 is topology-preserving too ?

Then, I think the choice between both may be a matter of other considerations when someone wants to model the nonlinearities of an analog circuit (god, I hate using the term "filter" for something which has a nonlinear behaviour, even if it's called a filter in the analog world :D )

Thanks in advance !

Post

Wolfen666 wrote:So, it is possible to say that TDF2 is topology-preserving too ?
TDF2 is not topology preserving, as the topology preservation is not about the artifacts, but about... preserving the topology :), that is the connections within the structure. As to the artifact argument - this is not the only problem with the direct forms, and it was brought up solely for the intuitive demonstration purposes (e.g. you can get similar artifacts from the TPT version by placing the cutoff after the integrator). The other issue is different choice of state variables, leading to different time-varying performance (how critical this might get is somewhat subjective and depends on the context, so YMMV :) ). Intuitively thinking, the time-varying performance of direct forms should be way "less natural" than TPT, since there are no explicit cutoff parameter gains. There are also precision issues with direct forms, which might not come up in TPT. And of course the nonlinearity placement.

Edit: it's difficult to analyse all that formally, so maybe your own experience should be the judging factor for you. Simply try both and compare.

Edit2: also, artifact-wise, there is a difference between an integrator and a z^-1 block used as a buffer between a gain and the output. An integrator changes the order of the discontinuity, while a z^-1 does not. Of course all this is rather intuitive than rigorous.

Edit3. Another way to put it (a little bit exaggerated): direct form implementations have NOTHING in common with the original analog filter, besides the transfer function. And the latter describes ONLY the linear time-invariant case. If the transfer function is all you care for - go ahead (precision issues aside). Otherwise it's rather a matter of luck, how closely direct form can suit your analog emulation needs :)

Regards,
{Z}
Last edited by Z1202 on Thu Jan 10, 2013 2:05 pm, edited 1 time in total.

Post

TDF2 is not topology preserving, as the topology preservation is not about the artifacts, but about... preserving the topology
Well, that sentence was a hook :hihi: I have understood well that TPT is designed to preserve the topology, so the talk was essentially about the performance :wink:
As to the artifact argument - this is not the only problem with the direct forms, and it was brought up solely for the intuitive demonstration purposes
What I have seen so far there is that DF2 is better than DF1 because it is canonic and that TDF2 is better than DF2 because the zeros are processed first, which leads to more numerical robustness (critical in fixed-point calculus notably). And then, I have seen in the block schemes that TDF2 structures have not this artefact problem too.
The other issue is different choice of state variables, leading to different time-varying performance (how critical this might get is somewhat subjective and depends on the context, so YMMV Smile ). Intuitively thinking, the time-varying performance of direct forms should be way "less natural" than TPT, since there are no explicit cutoff parameter gains.
OK, I have understood that now :wink: It may be better to have state variables close to "natural parameters" (the cutoff frequency here) in specific contexts. I can understand it well, I'm more from the guitar amp world than the synthesizer one, and the calculus of the classical guitar amp tonestack transfer function parameters is a nightmare, even if I use automatic tools (the equations are very large, and not simple combinations of the bass/med/treble pot values).

However, I'm wondering if it would be easy to get the block scheme of the tonestack model using the integrator substitution instead. I guess not at first :lol:
There are also precision issues with direct forms, which might not come up in TPT.
Does that concern TDF2 as well for you ?
Edit: it's different to analyse all that formally, so maybe your own experience should be the judging factor for you. Simply try both and compare.
I agree totally. I'm just studying formally your very good book right now, and I was "formally" wondering if I should try to replace a few of my TDF2 filters with TPT ones :wink:

Post

Wolfen666 wrote:However, I'm wondering if it would be easy to get the block scheme of the tonestack model using the integrator substitution instead. I guess not at first :lol:
I'm not sure of the specific equations there, but as soon as your equations are using just the first-order time derivatives, building the integrator-based block diagram should be fairly straightforward. What might be not as straightforward is a good numerical algorithm for solving the zero-delay feedback if you have lots of mixed feedback paths or complex nonlinearities.
Wolfen666 wrote:
There are also precision issues with direct forms, which might not come up in TPT.
Does that concern TDF2 as well for you ?
I'm not sure, it might. My experiments with direct forms are pretty old, I just remember that there were precision issues.
Wolfen666 wrote:I agree totally. I'm just studying formally your very good book right now, and I was "formally" wondering if I should try to replace a few of my TDF2 filters with TPT ones :wink:
I guess the best way to find out is to simply try :wink:

I'd say if there's lot of nonlinear processing or fast changing parameters, it might be a good idea, otherwise the difference might be marginal, if any.
Last edited by Z1202 on Thu Jan 10, 2013 2:15 pm, edited 1 time in total.

Post

Edit2: also, artifact-wise, there is a difference between an integrator and a z^-1 block used as a buffer between a gain and the output. An integrator changes the order of the discontinuity, while a z^-1 does not. Of course all this is rather intuitive than rigorous.
I see :wink:
Edit3. Another way to put it (a little bit exaggerated): direct form implementations have NOTHING in common with the original analog filter, besides the transfer function. And the latter describes ONLY the linear time-invariant case. If the transfer function is all you care for - go ahead (precision issues aside). Otherwise it's rather a matter of luck, how closely direct form can suit your analog emulation needs
Ok, it's a matter of modeling how the analog circuit has its behaviour changing when one of its parameter is modulated. I imagine that interpoling the poles and the zeros in direct form structures would be closer to reality than just recalculating the DF parameters however :wink:
Last edited by Ivan_C on Thu Jan 10, 2013 2:19 pm, edited 1 time in total.

Post

Thanks for the explanations, I will try a few things now :wink:

Post

Wolfen666 wrote:I imagine that interpoling the poles and the zeros in direct form structures would be closer to reality than just recalculating the DF parameters :wink:
If we are talking about choosing the interpolation scheme, then interpolating the poles/zeroes vs. interpolating the DF parameters might be better. As to modeling the time-varying behavior of the analog prototype they both are way off (imagine that the parameters are updated on each sample, so that no interpolation is necessary, still your state variables are completely different).

Post

imagine that the parameters are updated on each sample, so that no interpolation is necessary, still your state variables are completely different
Of course :wink:

Post

Hello !

I have other questions. I have used state-space representations for ages, and variations for nonlinear modeling / simulation.

Let's say I have an analog circuit, and that I have the equations of its behaviour this way :

dX/dt = AX + BU
Y = CX + DU

with X the state variables, U the input, Y the output, and A, B, C, D various matrixes.

Now, if I want to discretize that, I have various solutions :

1) We can see that the transfer function of this model is H(s) = C*(s-A)^-1*B + D. Then, we can use the BLT and frequency warping to get the DF1 to TDF2 structures.

2) We can also use the state space representation to get the equivalent topology preserving structures (I still don't get it now, but I'm going to study that)

3) Or, we can apply directly standard numerical integration methods like Runge-Kutta methods, or implicit Euler, implicit trapezoidal (bilinear), Gear / BDF2 etc. to the scheme.

So my questions are :

- How would you do what I'm talking about in 2) ? My problem is that I have a state-space representation of the tonestack model I have been talking previously, but the transition between that and the block schemes still looks like magic for me :hihi:

- What do you think of the performance of 3) in the time-varying case ?

Thanks to help me a little, even if my questions may be dumb :wink:

Post

Wolfen666 wrote:Hello !

I have other questions. I have used state-space representations for ages, and variations for nonlinear modeling / simulation.

Let's say I have an analog circuit, and that I have the equations of its behaviour this way :

dX/dt = AX + BU
Y = CX + DU

with X the state variables, U the input, Y the output, and A, B, C, D various matrixes.

Now, if I want to discretize that, I have various solutions :

1) We can see that the transfer function of this model is H(s) = C*(s-A)^-1*B + D. Then, we can use the BLT and frequency warping to get the DF1 to TDF2 structures.

2) We can also use the state space representation to get the equivalent topology preserving structures (I still don't get it now, but I'm going to study that)

3) Or, we can apply directly standard numerical integration methods like Runge-Kutta methods, or implicit Euler, implicit trapezoidal (bilinear), Gear / BDF2 etc. to the scheme.

So my questions are :

- How would you do what I'm talking about in 2) ? My problem is that I have a state-space representation of the tonestack model I have been talking previously, but the transition between that and the block schemes still looks like magic for me :hihi:

- What do you think of the performance of 3) in the time-varying case ?

Thanks to help me a little, even if my questions may be dumb :wink:
2) can be directly expressed by a continuous-time block diagram using integrators. An example of how to do it has been presented in the book for the 1-pole filter. 3) will give you more or less the same results for trapezoidal integration, except that you'll have twice as many state variables and won't have a visual representation (which is every once in a while quite helpful, e.g. to understand the feedback or the application of nonlinearities in a more intuitive way). Using other integration methods is possible as well of course, but then you don't get the same nice translation of the frequency response as for trapezoidal integration/BLT

Post

Thanks for the answers :wink:

Post

because of a request via PM, i post some class that implements a ZDF-SVF which can give all the well-known response types of the RBJ cookbook filters:

header file:

Code: Select all

#ifndef RS_STATEVARIABLEFILTER_H
#define RS_STATEVARIABLEFILTER_H

namespace RSLib
{

  /**

  This is an implementation of a state variable filter using topology-preserving transform (TPT) 
  and zero-delay feedback (ZDF) technology. 

  You can either use 3 outputs (lowpass, bandpass, highpass) from the SVF core by calling 
  getOutputs() or let the filter itself form a linear combination of the these 3 to obtain a 
  desired filter mode (in addition to the 3 modes above, there are also shelvers, a bell, etc.).

  */

  class RSLib_API rsStateVariableFilter
  {

  public:

    /** Enumeration of the available filter modes. */
    enum modes
    {
      BYPASS = 0,
      LOWPASS,
      HIGHPASS,
      BANDPASS_SKIRT,  // constant skirt gain
      BANDPASS_PEAK,   // constant peak gain
      BANDREJECT,
      BELL,
      LOWSHELF, 
      HIGHSHELF,
      ALLPASS,

      MORPH_LP_BP_HP,  // under construction

      NUM_MODES
    };


    /** \name Construction/Destruction */

    /** Constructor. */
    rsStateVariableFilter();   


    /** \name Setup */

    /** Sets the sample-rate. */
    void setSampleRate(double newSampleRate);

    /** Chooses the filter mode. See the enumeration for available modes. */
    void setMode(int newMode);

    /** Sets the characteristic frequency (which is the cutoff-frequency for lowpass and highpass, 
    the center-frequency for bandpass, bandreject and bell, and the halfgain-frequency for 
    shelvers). */
    void setFrequency(double newFrequency);

    /** Sets the resonance gain (as linear gain factor) for low-, high- and (constant skirt gain) 
    bandpass filters or the boost/cut gain for bell- and shelving filters. */
    void setGain(double newGain);

    /** Sets the bandwidth (in octaves) for (constant peak gain) bandpass filters and bell filters. 
    In the case of shelving filters, this also determines the slope at the halfgain point. 
    At B = (2*asinh(1/sqrt(2)))/log(2) = 1.899968626952992, the slope is as steep as it can be 
    without overshooting. */
    void setBandwidth(double newBandwidth);

    /** Morphing parameter for the morphing filter types - this feature is under construction. */
    void setMorph(double newMorph);


    /** \name Inquiry */

    /** When you use getOutputs() directly to obtain the lowpass, bandpass and highpass signals 
    of the core SVF, this function returns the value by which the bandpass signal has to be scaled 
    in order to achieve lowpass + scaler*bandpass + highpass == original input. */
    inline double getBandpassScaler() const { return R2; }


    /** \name Audio Processing */

    /** Returns the 3 outputs (lowpass, bandpass, highpass) of the core SVF. */
    RS_INLINE void getOutputs(double in, double &yL, double &yB, double &yH);

    /** Returns an appropriate linear combination of the 3 outputs of the core SVF in order to 
    achieve various filter modes as selected via setMode(). */
    RS_INLINE double getSample(double in);


    /** \name Misc */

    /** Resets the internal state buffers to zero. */
    void reset();

  protected:

    /** \name Internal Functions */

    /** Calculates filter coefficients from the user parameters */
    void calcCoeffs();  

    /** Computes damping coefficient R from desired bandwidth and the prewarped radian center 
    frequency (for bandpass with constant peak gain, bandreject and allpass). */
    double bandwidthToR(double B);


    /** \name Data */

    // state:
    double s1, s2;

    // filter coefficients:
    double g;          // embedded integrator gain
    double R2;         // twice the damping coefficient (R2 == 2*R == 1/Q)
    double h;          // factor for feedback (== 1/(1+2*R*g+g*g))
    double cL, cB, cH; // coefficients for low-, band-, and highpass signals

    // parameters:
    double fs;    // sample-rate
    double fc;    // characteristic frequency
    double G;     // gain
    double B;     // bandwidth (in octaves)
    double m;     // morph parameter (0...1)
    int    mode;  // filter-mode

  };

  //-----------------------------------------------------------------------------------------------
  // inlined functions:

  RS_INLINE void rsStateVariableFilter::getOutputs(double in, double &yL, double &yB, double &yH)
  {
    // compute highpass output via Eq. 5.1:
    yH = (in - R2*s1 - g*s1 - s2) * h;

    // compute bandpass output by applying 1st integrator to highpass output:
    yB = g*yH + s1;
    s1 = g*yH + yB; // state update in 1st integrator

    // compute lowpass output by applying 2nd integrator to bandpass output:
    yL = g*yB + s2;
    s2 = g*yB + yL; // state update in 2nd integrator

    // Remark: we have used two TDF2 integrators (Fig. 3.11) where one of them would be in code:
    // y = g*x + s; // output computation
    // s = g*x + y; // state update

    // as a cheap trick to introduce nonlinear behavior, we apply a nonlinearity to the states of 
    // the integrators (uncomment, if you want that):
    //s1 = tanh(s1);
    //s2 = tanh(s2);
  }

  RS_INLINE double rsStateVariableFilter::getSample(double in)
  {
    double yL, yB, yH;
    getOutputs(in, yL, yB, yH);
    return cL*yL + cB*yB + cH*yH;
  }

}

#endif

[/size]



implementaion:

Code: Select all

using namespace RSLib;

// Construction/Destruction:

rsStateVariableFilter::rsStateVariableFilter()
{
  fs   = 44100.0;
  fc   = 1000.0;
  mode = LOWPASS; 
  G    = ONE_OVER_SQRT2;
  B    = 2.0;
  m    = 0.0;
  calcCoeffs();
  reset(); 
}

// Setup:

void rsStateVariableFilter::setSampleRate(double newSampleRate)
{
  fs = newSampleRate;
  fc = rsClipToRange(fc, 0.0, 0.5*fs);
  calcCoeffs();
}

void rsStateVariableFilter::setMode(int newMode)
{
  mode = newMode;
  calcCoeffs();
}

void rsStateVariableFilter::setFrequency(double newFrequency)
{
  fc = rsClipToRange(newFrequency, 0.0, 0.5*fs);
  calcCoeffs();
}
 
void rsStateVariableFilter::setGain(double newGain)
{
  G = rsClipToRange(newGain, 0.0, 100.0);
  calcCoeffs();
}

void rsStateVariableFilter::setBandwidth(double newBandwidth)
{
  B = rsClipToRange(newBandwidth, 0.0, 100.0);
  calcCoeffs();
}

void rsStateVariableFilter::setMorph(double newMorph)
{
  m = rsClipToRange(newMorph, 0.0, 1.0);
  calcCoeffs();
}

// Misc:

void rsStateVariableFilter::calcCoeffs()
{
  g = tan(PI*fc/fs);  // embedded integrator gain (Fig 3.11)

  switch( mode )
  {
  case BYPASS:
    {
      R2 = 1 / G;  // can we use an arbitrary value here, for example R2 = 1?
      cL = 1; 
      cB = getBandpassScaler(); 
      cH = 1;
    }
    break;
  case LOWPASS:
    {
      R2 = 1 / G;
      cL = 1; cB = 0; cH = 0;
    }
    break;
  case HIGHPASS:
    {
      R2 = 1 / G;
      cL = 0; cB = 0; cH = 1;
    }
    break;
  case BANDPASS_SKIRT: 
    {
      R2 = 1 / G;
      cL = 0; cB = 1; cH = 0;
    }
    break;
  case BANDPASS_PEAK: 
    {
      R2 = 2*bandwidthToR(B);
      cL = 0; cB = R2; cH = 0;
    }
    break;
  case BANDREJECT:
    {
      R2 = 2*bandwidthToR(B);
      cL = 1; cB = 0; cH = 1;
    }
    break;
  case BELL:
    {
      double fl = fc*pow(2, -B/2); // lower bandedge frequency (in Hz)
      double wl = tan(PI*fl/fs);   // warped radian lower bandedge frequency /(2*fs)
      double r  = g/wl; r *= r;    // warped frequency ratio wu/wl == (wc/wl)^2 where wu is the 
                                   // warped upper bandedge, wc the center
      R2 = 2*sqrt(((r*r+1)/r-2)/(4*G));
      cL = 1; cB = R2*G; cH = 1;
    }
    break;
  case LOWSHELF:
    {
      double A = sqrt(G);
      g /= sqrt(A);               // scale SVF-cutoff frequency for shelvers
      R2 = 2*sinh(B*log(2.0)/2);
      cL = G; cB = R2*A; cH = 1;
    }
    break;
  case HIGHSHELF:
    {
      double A = sqrt(G);
      g *= sqrt(A);               // scale SVF-cutoff frequency for shelvers
      R2 = 2*sinh(B*log(2.0)/2);
      cL = 1; cB = R2*A; cH = G;
    }
    break;
  case ALLPASS:
    {
      R2 = 2*bandwidthToR(B);
      cL = 1; cB = -R2; cH = 1;
    }
    break;

    // experimental - maybe we must find better curves for cL, cB, cH:
  case MORPH_LP_BP_HP:
    {
      R2 = 1 / G;
      double x  = 2*m-1;

      //double x2 = x*x;
      //cL = 0.5*(x2-x); cB = 1-x2; cH = 0.5*(x2+x); // nah - not good

      // better:
      cL = rsMax(-x, 0.0); cH = rsMax(x, 0.0); cB = 1-(cL+cH);
      cB = pow(cB, 0.25);
        // freq-responses look good (on a linear scale), but we really have to check how it "feels" - it
        // would also be nice to get rid of the expensive pow-function and to replace it by 
        // something cheaper - the function should map the range 0...1 monotonically to itself

      // another (cheap) possibility:
      //cL = rsMax(-x, 0.0); /*cL *= cL;*/
      //cH = rsMax( x, 0.0); /*cH *= cH;*/
      //cB = 1-x*x;
      
        // bottom line: we need to test different versions for how they feel when tweaking the 
        // morph parameter

      // this scaling ensures constant magnitude at the cutoff point (we divide the coefficients by 
      // the magnitude response value at the cutoff frequency and scale back by the gain):
      double s = G * sqrt((R2*R2) / (cL*cL + cB*cB + cH*cH - 2*cL*cH));
      cL *= s; cB *= s; cH *= s;
    }
    break;

  }

  h = 1 / (1 + R2*g + g*g);  // factor for feedback precomputation
}

double rsStateVariableFilter::bandwidthToR(double B)
{
  double fl = fc*pow(2, -B/2); // lower bandedge frequency (in Hz)
  double gl = tan(PI*fl/fs);   // warped radian lower bandedge frequency /(2*fs)
  double r  = gl/g;            // ratio between warped lower bandedge- and center-frequencies
                               // unwarped: r = pow(2, -B/2) -> approximation for low
                               // center-frequencies
  return sqrt((1-r*r)*(1-r*r)/(4*r*r));
}

void rsStateVariableFilter::reset()
{
  s1 = s2 = 0.0;
}

[/size]

i just copy/pasted the code directly from my library, hence the namespace stuff, which, without context, is just clutter. you may want to edit away that stuff when using the code. i didn't bother. there's also some experimental morphing stuff which is not yet finished. ...and the referenced rsClipToRange does the obvious thing. dunno, if there are any further things to worry about.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

wow, thanks robin, that is really nice of you. Do you still suggest oversampling this filter? I get great performance from it without any oversampling. Is this design based on the "Simper" SVF?, the response definitely looks similar to a version of it that somebody posted on the max msp forum.

I tried the tanhs but this seemed to kill the resonance... any ideas?

thanks again.

ps... what license is the latest code you posted under?

Post Reply

Return to “DSP and Plugin Development”