SVF resonant?

@kerfuffle yes it self oscillate, nice! I have to put a very higher q (such as over 30), but than it scream eheh.

Do you also have the “code” for plotting the frequency response? Using magnitude/phase method? Such as “Plotting the filter precisely” suggested by Nigel here.

To use that method you’d need to get the transfer function for the filter. I believe that’s actually the same as the transfer function for a BZT-derived biquad (see the RBJ cookbook for example).

To get a plot for the actual output of the filter, you need to capture the impulse response and apply an FFT. Doing the FFT and making a plot is pretty easy with Python, which is what I would use.

Here’s how you get the magnitude response for the SVF in discussion here:

float SVF::magnitude(double frequency, double sampleRate)
{
    auto z = std::exp(juce::dsp::Complex<double>(0.0, -2.0 * juce::MathConstants<double>::pi) * frequency / sampleRate);

    const double gsq = g_ * g_;
    const auto zsq = z * z;
    const auto twoz = z * 2.0;
    const double gm1 = g_ * m1_;
    const double gk = g_ * k_;
    const double twogsq = gsq * 2.0;

    auto numerator = gsq * m2_ * (zsq + twoz + 1.0) - gm1 * (zsq - 1.0);
    auto denominator = gsq + gk + zsq * (gsq - gk + 1.0) + z * (twogsq - 2.0) + 1.0;

    return std::abs(static_cast<double>(m0_) + (numerator / denominator));
}

The variables ending in ‘_’ are the member variables g, k and m0, m1, m2 mixer values for the SVF.

If you want to get the phase instead just replace the last line with:

return std::arg(complexResponse);

Which will give the you the phase in radians.

On the basis that you want to draw the filter in a Juce UI:

Producing the plot is pretty easy. Have an x coordinate range with a mapping from say 20…2000Hz logarithmically and convert the magnitudes to a y coordinate. Draw lines.

3 Likes

Thanks :slight_smile:
Unfortunately at the moment I’m using the last version posted by @kerfuffle , which doesn’t have m0/m1/m2. Can I resolve them from g/k/a1/a2/a3? Without introduce some more variables…

m0 … m2 is probably a1…a3 (bandpass lowpass and highpass outputs).

The m0-m2 variables and how they are filled in are clearly visible in @Nitsuj70’s code here: SVF resonant? - #17 by Nitsuj70

That code has the line return m0 * x + m1 * v1 + m2 * v2; in it, which mixes the different “voltages” according to m0-m2 and returns the result.

In the code I posted, I got rid of these variables so the function could return the LP, BP, and HP values directly. I did this by looking up the values of m0-m2 for the LP / BP / HP cases and hardcoding their values.

2 Likes

You won’t get similtaneous multiple magnitudes for lp, bp and hp out of the ‘magnitude’ method I gave in the same way (almost for free) that you get lp, bp and hp values out of the filter ‘render’ method. So just decide which response you want (lp, bp, hp) and set m0, m1 and m2 up accordingly as per my example.

The reason I like using m0, m1 and m2 is because my usage of this filter has a ‘shape’ parameter that specifies the filter shape as LP->BP->HP->BR->LP as a control (BR = notch). As you change the control the filter smoothly interpolates between the shapes. This is easily achieved by interpolating between the different values of m0, m1 and m2.

1 Like

Just awesome! Works like a charm!!! Thanks man!

(note, not sure what is complexResponse in return std::arg(complexResponse).
Probably the same static_cast(m0_) + (numerator / denominator)?)

Do you also have the freq response of the SVF (native) of JUCE? (which is not the Cytomic one): https://github.com/juce-framework/JUCE/blob/master/modules/juce_dsp/processors/juce_StateVariableFilter.h

Yes, ‘complexResponse’ is just ‘static_cast(m0_) + (numerator / denominator)’ again.

The JUCE SVF is using Zavalishin’s Trapezoidal integration which AFAIK has exactly the same magnitude response as the Cytomic SVF and, for that matter, the same magnitude response as the classic RBJ filters. They’re all straight forward 2-pole 12dB filters and the magnitude response is calculated from the z-plane by taking that into account.

The main difference between the Zavalishin TPT and Cytomic SVF vs RBJ is that they’re more numerically stable. So I’d just use the response method I gave and adjust for lp, bp and hp - it’ll represent the TPT filter just fine.

The problem it seems TPT use g, r2 and h coefficients, while Cytomic only g and k, so seems can’t compare the two on the code you give to me above…

In the Cytomic SVF:

g = std::tan(PI * cutoff / sampleRate);
k = 1.0f / Q;
a1 = 1.0f / (1.0f + g * (g + k));

is equivalent to:

g  = static_cast<NumericType> (std::tan (MathConstants<double>::pi * frequency / sampleRate));
R2 = static_cast<NumericType> (1.0 / resonance);
h  = static_cast<NumericType> (1.0 / (1.0 + R2 * g + g * g));

It’s just that two of the variables are called different things and the derivation of a1 and h looks different but gives the same numeric result.

Again, it works like a charm!!! Many thanks dude, so kind and useful.

It seems that also TPT version self-oscillate using the same res value as well, but this should be pretty obvious since also you said previously “The JUCE SVF is using Zavalishin’s Trapezoidal integration which AFAIK has exactly the same magnitude response as the Cytomic SVF”.

Not very sure now about what’s the difference between the two filters so :slight_smile:
Just the numberic stability?
They both TPT and Cytomic give the same frequency response?
Why a person would use one instead of the other?

Different methods were used to convert the analog schematic of SVF into code. For the TPT method, a purely mathematical approach was used. For the Cytomic version, a method based on circuit analysis was used, essentially calculating the voltages at different points in the SVF circuit (which is why the variables are named v for voltage and i for current). Both lead to code that gives the same results, but the way they got there was different.

But in the practice, what change? Why a person would use one instead of the other?

I don’t think it really matters which one you use. The TPT one uses slightly fewer instructions, IIRC.

Comparing the two:

float render(float x) {
    float v3 = x - ic2eq;
    float v1 = a1 * ic1eq + a2 * v3;
    float v2 = ic2eq + a2 * ic1eq + a3 * v3;
    ic1eq = 2.0f * v1 - ic1eq;
    ic2eq = 2.0f * v2 - ic2eq;
    return m0 * x + m1 * v1 + m2 * v2;
  }

Results:

  • Multiplication: 9
  • Subtraction: 3
  • Addition: 5

The JUCE Zavalishin’s SVF:

template <bool isBypassed, typename Parameters<NumericType>::Type type>
        SampleType JUCE_VECTOR_CALLTYPE processLoop (SampleType sample, Parameters<NumericType>& state) noexcept
        {
            y[2] = (sample - s1 * state.R2 - s1 * state.g - s2) * state.h;

            y[1] = y[2] * state.g + s1;
            s1   = y[2] * state.g + y[1];

            y[0] = y[1] * state.g + s2;
            s2   = y[1] * state.g + y[0];

            return isBypassed ? sample :  y[static_cast<size_t> (type)];
        }

Results:

  • Multiplication: 7
  • Subtraction: 3
  • Addition: 4

So at first glance the JUCE SVF uses slightly less, but…if the Cytomic SVF was changed to get rid of m0, m1 and m2 so that it worked like the JUCE SVF, then it’d have 6 multiplies and 3 additions.

Bottom line, the Cytomic one could use less operations than the JUCE SVF, but really there’s hardly anything in it.

To be fair, the JUCE code repeats two of the multiplications:

Not sure if that’s done for a good reason, but it could be factored out and that would save two multiplies. (Chances are the compiler already does this.)

I also think the Cytomic version is perhaps easier to parallelize with SIMD but I never tried that.

1 Like

Still really can’t get why a person like Cytomic would invest so much time to maka a different implementation which does the same exact result :frowning:

only for simd Speed up? Maybe I miss some points?

Also @Nitsuj70 : It seems you know very well both filters implementations :slight_smile:
at the beginning of the discussion you said TPT won’t self oscillate, but how is It possible if both have the same freq response?

Well, as @kerfuffle said, they come at it from slightly different starting points. And who knows who did what first. Not that it matters, they’re both perfectly viable methods.

I generally use the SVF filter for simple filtering tasks. Kind of as a replacement for RBJ biquads. The main filters I use have non-linearities (ladder etc) and it’s those that I’ve ever cared about self oscillation for. So, my bad, I was probably mistaken about the self oscillation (or lack of) in these SVF filters in particular.

1 Like

Wouldn’t be nice for devs specify on documentation of that SVF filters that with higher res values it can self oscillate?

This could be an added value to the product (its not somethings “free” a self oscillation aspect) :slight_smile: