Issue on implementation of Moog Ladder filter using ZDF

DSP, Plug-in and Host development discussion.
KVRer
5 posts since 12 Oct, 2021 from Italy

Post Fri Oct 15, 2021 5:50 am

Hi everyone, this is my first post here (rather excited). I don't know if this has been already discussed, so forgive if I am repeating, but I felt I should have shared this somewhere, as it could be useful to others as well.

I've been working in the past few days on a simple implementation of a Moog Ladder filter following Vadim Zavalishin's techniques, as illustrated in his book.

I coded right from the book and filter itself was very poor, little stability overall and even with very low amount of resonance the filter would explode for high cutoff frequencies. In addition, the gain attenuation at DC was much more than the theoretical maximum of about -14 db.

Then I decided to look at some existing implementation and tried faust's one, which is in turn based on Will Pirkle's implementation, and found out the same issues I had with mine. It seemed strange because Pirkle's one seems to be a widely accepted implementation.

Anyway, I got back to paper and redid all the computations and realized that, in Pirkle's, and subsequently faust's, implementation there are a couple of errors.

First of all, a little reminder of Zavalishin's technique to fix notation: we see a filter as a linear operator composed of a part that depends on the current time and a part that depends on the past, i.e.

y = H_LP[x] = Gx + S.

This allows for an easy analysis and implementation of zdf filter with more complex structures.
In particuar, Zavalishin's implementation is

Code: Select all

v = (x - s)*G
y = v + s
s = y + v
where G = g/(1 + g) and g = tan(π w_c), wc being the normalized cutoff frequency.


In the case of the ladder filter we have four low pass H_1, H_2, H_3, H_4, wich are fed with the input and a feedback line. Hence we got

y = H_4[H_3[H_2[H_1[x - ky]]]]
= S_4 + G(S_3 + G(S_2 + G(S_1 + G(x - ky))))

that is

y = (G^4 x + kS)/(1 + G^4),
where S = S_4 + G*S_3 + G^2 * S_2 + G^3 * S_1.

And here is the first difference: G should be the the same as in the one pole filters. Instead, Pirkle's implementation uses g (the warped cutoff frequency) for the computation fo the feedbacks. This is not just a notation issue, since in the one pole filters he correctly uses G. This sounded very strange.

Then I realized what the issue was. In Pirkle's implementations the memory states S_1,...,S_4 for the computation of the ladder feedback where taken directly as the inner memory states of the one pole filters . However, I believe this is wrong. If we write Zavalishin's implementation of the one pole eliminating the intermediate value v we get

Code: Select all

y = s + (x - s)*G = Gx + (1-G)s
s = y + (x-s)*G
that is, the memory part that allow us to view the filter as y = Gx + S is not S = s, but it's S = (1-G)s!
Correcting these issues lead to a correct and stable implementation of the moog ladder.


Here are two images of the frequency response of the two implementations. Both have been compared with the theoretically corrected one, i.e. the one obtained as the composition of moog filter's response (H(s) = 1/((1+ s)^4 + k)) and the bilinear transform (s(z) = (1 + z^-1)/(1 - z^-1)/tan(π wc) ).
For the implementations the frequency response are measured by feeding an impulse as input and then taking the absolute value of the fft of the output.

The first one depicts the "right" response (the dotted one) and the result of faust/pirkle's filter. Actually, this code came directly from faust code, I just translated it to MatLab, which I am more familiar with.
Same color means same cutoff frequency, resonance is k = 3. As you can see the result of the filter is very far from the "right" response. And in addition to that, from the resonance peaks we can see that the cutoff frequency is altered as well.

On the other hand, the second image is the result of the corrected filter implementation. Here the difference between filter's response and "right" response is not even noticeable.






Image
Image


So, I don't know if anybody already did encounter and fixed this issue but, if not, I hope this might help :-D

P.S.
It was almost ten years I didn't write in a forum, damn social networks!
Lorenzo from Italy. Developer, Mathematician

KVRian
1353 posts since 12 Apr, 2002

Post Sat Oct 16, 2021 6:29 am

This seems correct and consistent with 3.29 on p.76 (referring to rev 2.1.2 of the book).
3.29 is also explicitly referred to in sec.5.3, so I assume there is no mistake (in this regard) in the book itself.
I'm a bit confused by you writing g=tan(pi wc). wc notation normally assumes radians/sec or at least radians/timeunit, so I think it's rather tan(wc/2) or tan(wc T/2).

KVRer

Topic Starter

5 posts since 12 Oct, 2021 from Italy

Post Sat Oct 16, 2021 8:25 am

Z1202 wrote:
Sat Oct 16, 2021 6:29 am
This seems correct and consistent with 3.29 on p.76 (referring to rev 2.1.2 of the book).
3.29 is also explicitly referred to in sec.5.3, so I assume there is no mistake (in this regard) in the book itself.
There is indeed no mistake in the book, I think the mistakes come from the fact that no explicit implementation is given in the book, so mistakes have been made.
Z1202 wrote:
Sat Oct 16, 2021 6:29 am
I'm a bit confused by you writing g=tan(pi wc). wc notation normally assumes radians/sec or at least radians/timeunit, so I think it's rather tan(wc/2) or tan(wc T/2).
I'm sorry, I didn't specify this, for me wc = f_c/f_s that is the cutoff frequency divided by the sampling frequency. Since f_c < 0.5*f_s (usually!), then the argument of the tangent is always strictly less than pi/2.
Lorenzo from Italy. Developer, Mathematician

KVRer
1 posts since 8 Jul, 2013

Post Sun Oct 17, 2021 11:06 am

AUTO-ADMIN: Non-MP3, WAV, OGG, SoundCloud, YouTube, Vimeo, Twitter and Facebook links in this post have been protected automatically. Once the member reaches 5 posts the links will function as normal.
Hi All - someone clued me into this thread and I thought I'd chime in. The original App Note that you are referencing here was found to be in error not long after I posted it on my site back in 2013. A KVR user named raphx had sent me an email about it. I re-derived the filter and tested it, then updated the app note with v2.0 in July 2013, and sent it to raphx, who checked it and presumably posted about it though I do not have his post or a link to it. I still have a "Legacy" link to a new page with the updated v2.0 app note on my current site (the Legacy menu item), and a note explaining the error on that webpage (below). The page was updated in 2016, but the v2.0 App Note did not change.

https://www.willpirkle.com/706-2/ (https://www.willpirkle.com/706-2/)

From the page above:
"There was an error in my original implementation of the TPT Ladder Filter that has now been corrected (July 2013). The TPT Ladder works correctly without oversampling right up to Nyquist and will self oscillate at the highest resonance setting. The derivation for the block diagram is fully worked out and the code has been corrected."

Note that the updated (correct) version was published in both of my synth books and (I've been told) is currently used in Audulus (the iOS app). The 2nd and much improved/shortened edition of the synth book was released in June 2021 and also includes a mod to get finite gain at Nyquist to overcome the zero-at-Nyquist issue that these VA LPF filters all suffer from (as they are still ultimately bilinear transform based).

Oli Larkin published a SOUL version of this filter, but it uses a 1st order Direct Form structure rather than the trapezoidal integrator based building block.

I do still have the original (incorrect) App Note sitting on my server, though it is not linked anywhere from my site - I suppose I should replace it with that old but updated version 2.0 to avoid this issue in the future.

Will Pirkle

EDIT: I have uploaded a renamed version of the app note, so that links will still work, but will now get the corrected version.

- Will

KVRer

Topic Starter

5 posts since 12 Oct, 2021 from Italy

Post Mon Oct 18, 2021 1:11 am

willpirkle wrote:
Sun Oct 17, 2021 11:06 am
Hi All - someone clued me into this thread and I thought I'd chime in. The original App Note that you are referencing here was found to be in error not long after I posted it on my site back in 2013. A KVR user named raphx had sent me an email about it. I re-derived the filter and tested it, then updated the app note with v2.0 in July 2013, and sent it to raphx, who checked it and presumably posted about it though I do not have his post or a link to it. I still have a "Legacy" link to a new page with the updated v2.0 app note on my current site (the Legacy menu item), and a note explaining the error on that webpage (below). The page was updated in 2016, but the v2.0 App Note did not change.
Ah, ok, I'm sorry I didn't realize this, all the links given in the documentations were to the old version of the pdf note, so I supposed that was the most recent version.
Happy that it was no longer an issue, though!
Lorenzo from Italy. Developer, Mathematician

Return to “DSP and Plug-in Development”