Why LeakDC and not a standard HPF for DC removal?

Hello everyone,

I’m a bit sceptical of the DC removal implementation in LeakDC. As I understand it, DC removal is nothing but a HPF, preferably placed below the audible range. For this purpose, LeakDC seems to be reaching far too high, attenuating the audible low frequencies. You can run the following code and look at the server meter to see the difference in level between the two audio channels (note: there is of course no DC offset in this example that would need fixing):

( play { [
	SinOsc.ar(50),
	LeakDC.ar(SinOsc.ar(50))
] * 0.2 } )

Also, LeakDC’s behaviour is not consistent between sample rates (as is mentioned in the help file).

I ran the it through Plugin Doctor to back up my suspicions and got these results:

standard coefficient of 0.995:
SR 48 kHz: Filter starts rolling off at 200 Hz, -2dB at 50 Hz.
SR 96 kHz: Filter starts rolling off at 500 hz! It reaches already -2dB at 100 Hz, -5 dB at 50 Hz.

coefficient of 0.999:
SR 48 kHz: Filter starts rolling off at 50 Hz, -2 dB at 10 Hz.
SR 96 kHz: Filter starts rolling off at 100 Hz, -2 dB at 20 Hz.

I wonder why one wouldn’t just use a standard HPF at 10 Hz (or so) instead? It is consistent between sample rates, it only starts to roll of below 30 Hz and it reaches only -0.5 dB at 20 Hz. So this actually looks much better in the server meter:

( play { [
	SinOsc.ar(50),
	HPF.ar(SinOsc.ar(50), 10)
] * 0.2 } )

So my question is: Is there any reason why the LeakDC implementation would be peferable to such a standard butterworth HPF?

One difference I can see is that the HPF still has an initial offset spike before settling around zero phase, LeakDC does not show that behaviour:

{ LeakDC.ar(DC.ar(0.5)) }.plot(0.5);
{ HPF.ar(DC.ar(0.5), 10) }.plot(0.5);

{ LeakDC.ar(SinOsc.ar + DC.ar(0.5)) }.plot(0.02);
{ HPF.ar(SinOsc.ar + DC.ar(0.5), 10) }.plot(0.02);

So this could be a reason – I don’t know enough about filters to understand why this happens though. Any enlightenment would be appreciated.

1 Like

By chance, I just ran across a block diagram for a “DC trap” in a synthesis cookbook (PDF link below).

I’m terrible at block diagrams but you might experiment with FOS or SOS to implement this diagram and see if its frequency response is an improvement over LeakDC.

hjh

http://argos.vu/wp-content/uploads/2016/05/Digital_Sound_Generation_1.pdf

2 Likes

Here’s my implementation of this topology in Faust - I’m still new to this, so no guarantees as far as accuracy goes, but the filter’s empirical behavior fits the author’s description (I get around 0.25dB of attenuation at 20Hz with the cutoff set at 5Hz, almost none at 100Hz). Here’s a link to the Faust IDE if anyone wants to play around with the code (paste and hit Ctrl-R).

// simple dc trap
// adapted from beat frei's "digital sound generation", appendix C
// http://argos.vu/wp-content/uploads/2016/05/Digital_Sound_Generation_1.pdf
// his recommendation is to set the cutoff (f_g) at 5Hz
// fast enough to track offset changes, but only ~0.26dB attenuation at 20Hz

import("stdfaust.lib");

dctrap(f_g) = + ~ (*(alpha) : (+ ~ _) : *(-1))
with {
    alpha = 2*ma.PI * f_g / ma.SR;
};

oscfreq = hslider("sinosc frequency (Hz)",80,0.1,100,0.1): si.smoo;
dctrap_fg = hslider("f_g",5,1,1000,0.1): si.smoo;
outgain = hslider("out gain (dB)",-35,-90,0,0.1) : si.smoo : ba.db2linear;

process = os.osc(oscfreq) : dctrap(dctrap_fg) : *(outgain);
1 Like

Really nice! One thing I like about faust is that the source is written in faust, so even this:

import(“stdfaust.lib”);

process = fi.dcblocker;

gives you a nice diagram, and you can look at source code to see Julius’s wizardry:

dcblocker

1 Like

Thank you very much for all your replies, and this beautiful Faust code!

I’ve also been looking at Faust’s solutions recently, more specifically at fi.dcblockerat(fb): It seems quite promising, letting you simply set the frequency that reaches -3 dB by an argument. (So I guess it’s also samplerate independent.) My current plan would be to just lazily convert it into a SuperCollider class with faust2supercollider and see what happens? Maybe I’ll find the time for trying it out this afternoon. (Though I have never used faust2supercollider before, so let’s see what unforeseen problems might arise.) Also playing around with bovil43810’s code for this purpose might be interesting.

1 Like

Nice, I’m interested! Keep in mind that “faust2” scripts only exist for macOS and Linux, at least last time I checked… might be the final push for me to switch to Linux :sweat_smile:
Edit: correction, some export options like Max (gen~) and VST do work on windows, but there’s no faust2supercollider. See here.

Yes – all those Faust tools (including faust2supercollider and the wonderful mephisto.lv2) are the main reason I recently brought my Arch Linux system back on track… It’s really worth switching :grinning_face_with_smiling_eyes:

Just FYI, you can compile to SC for linux and mac directly in the faustide online compiler. Just press the truck button and chose your platform and architecture. The class name will come from the name of the .dsp file. Super slick, and I have had no problems with the compilations in intel mac.

1 Like

I’d be very surprised if there’s anything wrong with LeakDC, just because a DC blocker is such a basic piece of DSP. It’s just a simple 1 pole filter, whereas HPF is 2 pole filter.

1 pole filters have a shallower cut off than two poles, but they do fewer weird things in the time domain. Which I’m guessing (haven’t looked into it) is probably what you want for a DC blocker (which is removing an unchanging signal). There may be also some issues with phasing, which given that DC blockers are often used in places where you care about phasing, may also be a factor. Obviously if you have a signal that you’re going to HPF anyway, then there’s no real reason to use a DC blocker. I mostly used them inside synths, rather than for mixing (as I tend to HPF everything because I guess that’s what I was taught).

The tradeoff (there are always tradeoffs in DSP) with the coefficient really comes down to whether you want a tighter cut off, or a faster response. 0.999 will remove less signal, but it responds less quickly. I’d guess 0.995 is used so widely because in practice it’s a good compromise.

2 Likes

Ah, thank you for this explanation! I wasn’t aware of the difference in response time.

To follow up on the little project of porting Faust’s DC removal filter to SuperCollider:

faust2supercollider on Linux doesn’t work for me yet (I just get an empty class file, despite having Ruby installed – but I’ll move this issue to the relevant thread if I can’t solve it myself). Thanks to Sam_Plutta’s tip I had no problem compiling it with the FaustIDE though and it works like a charm in Linux.

I can’t put it through PluginDoctor yet because I can’t get it to run on Windows – despite recompiling the class library (and the new UGen being highlighted correctly in the SCIDE), I get an error message when running it: “exception in GraphDef_Recv: UGen ‘DCFaust’ not installed”.

This is the simple Faust code I used (following Mads Kjeldgaard’s tutorial “Getting started with Faust for SuperCollider”), putting it into a file called DCFaust.dsp:

import ("stdfaust.lib");

fb = vslider("freq",10,1,20000,1);
process = _ : fi.dcblockerat(fb) : _;

When compiled it creates a UGen class called DCFaust which takes a signal and the filter frequency as arguments.

Having just looked at the code for LeakDC it seems absolutely standard.

1 Like

I didn’t mean to imply that the implementation of LeakDC was faulty in some way, I was just wondering whether there are better / less intrusive (audible frequency attenuating) ways to do it. (Or if not to understand why.) I appreciate that the Faust implementation takes frequency as an argument and not the coefficient, for example. (I started researching ways of converting between coefficient and sample rate but it seems to open a pit of non trivial mathematics.) All in all it’s more a quest to understand what’s going on and less the desire to solve a problem that would be all that relevant in everyday coding. Your posts really do help me to understand by the way, so thanks for taking the time :slightly_smiling_face:

Yeah coefficient and frequency are just different ways of representing the same thing. You can represent the frequency as the coefficient if you want, but I would guess people just don’t for DC removal because it’s really not worth the extra CPU cycles.

All in all it’s more a quest to understand what’s going on and less the desire to solve a problem that would be all that relevant in everyday coding.

Fair enough. Unfortunately I think the issue you’re going to run into is that it’s impossible to understand without having a reasonable level of math (early college, or very advanced high school) and then learning DSP theory, followed by digital filter theory. All of which is pretty useful for SuperCollider, but also can be quite challenging.

Intuitively you can think of a low pass filter as kind of function that averages streams of data, and a high pass filter is everything that gets filtered out. But I have a suspicion that’s one of those things that makes perfect sense when you understand how filters work, but is confusing otherwise :frowning:

Incidentally if you do want to learn this stuff you’ll see people recommending Julius O Smith’s textbooks. Ignore them. They can be excellent reference books, but they’re hopeless teaching resources.

1 Like

Well, now I’m intrigued.

Anything you could recommend instead? Recently I’ve been peeking into Puckette’s classic work:

Yes this i agree. The online text isn’t as good as teaching material for the uninitiated.

Thanks for these explanations, I understand the maths a bit better now. But this prompt at the top…

… triggered another completely field of answers in my head, as for me, it was always a matter of sound - maybe my DAW (composition/sound design/mixing/mastering) and modular thinking polluted it, but how to deal with low-end and DC, and when in the chain, has always been driven by how they sound. LeakDC (a first-order filter) vs 2nd and much higher orders (and frequencies) have different sound - but in relation to the rest of that register where there is a single critical band is quite something much more impactful in my decision process. And hmmmm, asymmetrical (DC-offseted) clipping, I couldn’t live without it :slight_smile:

So I wonder, what was the musicking context that triggered this (fascinating) comparison?

… if you know anything about me, you’ll know this might sound sarcastic but it isn’t :slight_smile: Trying to understand on a theoretical base the difference in implementation is as valid as anything. I am just curious for real what was the trigger and end-point!