read 6 min
1 / 40
Jul 2023

post by Derozer on Jul 4, 2023

8 days later

post by Derozer on Jul 12, 2023

post by kerfuffle on Jul 13, 2023

Not sure what you’re asking. The SVF filter from JUCE gives you the frequency response of a second-order filter, which is resonant. But it’s not going to give the same frequency response as that Paul Kellet filter.

post by Derozer on Jul 13, 2023

For “resonant” I mean it will produce “a sine wave” near the cutoff, at higher res value…

I wrong term probably, meaning “Self-Oscillate”, which seems more appropriate :slight_smile:

post by kerfuffle on Jul 13, 2023

IIRC that SVF filter indeed self-oscillates with high values or Q. But you can always verify this for yourself by writing a few lines of code. :wink:

post by Nitsuj70 on Jul 13, 2023

I don’t think the SVF does self-oscillate. IIRC, the ladder (Moog style) filter does, but not the SVF.

post by kerfuffle on Jul 13, 2023

My recollection is that the Cytomic SVF does self-oscillate. I haven’t tried this with the TPT version that’s built into JUCE. I was under the impression both have the exact same frequency response, but perhaps they diverge when driving Q very hard.

post by Nitsuj70 on Jul 13, 2023

Andy’s SVF’s don’t self-oscillate. They’re pretty much a drop in replacement for RBJ biquads which don’t self-oscillate either. FWIW, the TPT ones don’t as well. Filters that can self-oscillate generally require non-linearities.

post by Derozer on Jul 13, 2023

Ok, so for what I see won’t be that easy to enable self oscillate (at least you are very strong in math).
Maybe I’ll start with one which already got this property by nature.

Excluding Ladder (which seems only LP), which one (with LP/HP/BP) do you know which self oscillate?

post by kerfuffle on Jul 13, 2023

I’m willing to be wrong on this, but in that case I do want to understand why I am wrong. :slight_smile: Perhaps I don’t understand the meaning of “self-oscillation”, but I thought it was this:

After exciting the filter by a short noise pulse or an impulse (or any other kind of signal), it creates a sine wave at the cutoff frequency.

Is that indeed the behavior we’re discussing here? Because if so, I have no problems getting the Cytomic SVF to do this. I’m using this filter in the synth from my book and even describe how to change Q to get it to self-oscillate.

If I feed the filter with nothing but some low-level noise (like you’d have in an analog synth) or even a pure impulse, it outputs a sine wave at the cutoff point.

(I’m perfectly happy to be corrected on this if I’m misunderstanding, since I’d want to update the book if this is a mistake.)

post by Derozer on Jul 13, 2023

Which Cytomic SVF implementation are you considering? Could you post the sources?

post by kerfuffle on Jul 13, 2023

post by Derozer on Jul 13, 2023

Thanks, but that seems “only” a lowpass filter, not a complete SVF (with simultaneous LP/HP/BP)…

post by Nitsuj70 on Jul 13, 2023

Yep, that’s pretty much the one I’m using. I might well be wrong on this. I’m sure I read some while (years) ago that they didn’t self-oscillate and TBH until very recently I’ve only employed them in a synth as local filters to the oscillators so I’ve never actually tried. If you’ve managed to get them to self-oscillate then I’ll defer to your empirical result.

post by Nitsuj70 on Jul 13, 2023

post by Derozer on Jul 13, 2023

Yeah, I mean “quickly” from the @kerfuffle code, so I can test it faster :slight_smile:
The low pass seems to works nice, just miss the HP/BP part.

post by Nitsuj70 on Jul 13, 2023

From the Cytomic paper (last page) and kerfuffle’s filter source code, something like:

// Resonant low-pass filter based on Cytomic SVF. class Filter { public: float sampleRate; void updateCoefficientsLP(float cutoff, float Q) { g = std::tan(PI * cutoff / sampleRate); k = 1.0f / Q; a1 = 1.0f / (1.0f + g * (g + k)); a2 = g * a1; a3 = g * a2; m0 = 0; m1 = 0; m2 = 1; } void updateCoefficientsBP(float cutoff, float Q) { g = std::tan(PI * cutoff / sampleRate); k = 1.0f / Q; a1 = 1.0f / (1.0f + g * (g + k)); a2 = g * a1; a3 = g * a2; m0 = 0; m1 = 1; m2 = 0; } void updateCoefficientsHP(float cutoff, float Q) { g = std::tan(PI * cutoff / sampleRate); k = 1.0f / Q; a1 = 1.0f / (1.0f + g * (g + k)); a2 = g * a1; a3 = g * a2; m0 = 1; m1 = -k; m2 = -1; } void reset() { g = 0.0f; k = 0.0f; a1 = 0.0f; a2 = 0.0f; a3 = 0.0f; m0 = 0.f; m1 = 0.f; m2 = 0.f; ic1eq = 0.0f; ic2eq = 0.0f; } 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; } private: const float PI = 3.1415926535897932f; float g, k, a1, a2, a3; // filter coefficients float m0, m1, m2; // filter mixers float ic1eq, ic2eq; // internal state };

post by Derozer on Jul 13, 2023

But that’s move away from original “concept” of SVF (i.e. having multi response at the same time, lp/hp/bp simultaneous).

If I want one at time, I’ve already the RBJ’s Biquad for that.

post by kerfuffle on Jul 13, 2023

You can get multiple outputs at the same time, something like this (warning: I didn’t test it):

void render(float x, float& lp, float& bp, float& hp) { 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; lp = v2; bp = v1; hp = x - k * v1 - v2; }

The coefficients are the same for LP, BP, and HP, only the “mix” amounts m change.

post by Derozer on Jul 14, 2023

Oh right! Nice :slight_smile:
I’ll try it out, and test self oscillation, and let you know :slight_smile: meanwhile, lots of thanks to both, very kind!

post by Derozer on Jul 14, 2023

@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.

post by kerfuffle on Jul 14, 2023

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.

post by Nitsuj70 on Jul 14, 2023

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.

post by Derozer on Jul 18, 2023

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…

post by kunz on Jul 18, 2023

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

post by kerfuffle on Jul 18, 2023

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.

post by Nitsuj70 on Jul 18, 2023

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.

post by Derozer on Jul 25, 2023

post by Nitsuj70 on Jul 25, 2023

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.

post by Derozer on Jul 25, 2023

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…

post by Nitsuj70 on Jul 26, 2023

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.

post by Derozer on Jul 26, 2023

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?

post by kerfuffle on Jul 26, 2023

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.

post by Derozer on Jul 26, 2023

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

post by kerfuffle on Jul 26, 2023

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

post by Nitsuj70 on Jul 26, 2023

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.

post by kerfuffle on Jul 26, 2023

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.

post by Derozer on Jul 26, 2023

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?

post by Nitsuj70 on Jul 26, 2023

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.

post by Derozer on Jul 26, 2023

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: