Limiting filter resonance to prevent distortion

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

Post

Hi there,

I'm implementing some basic mooog filter code from musicdsp.org and the gain increase caused by the resonance is causing distortion. I don't want to simply reduce the dynamic range of the input because that would reduce quality and also the overall volume when Q is not turned up. So how is this normally handled ? I've heard about clipping the feedback, but not sure how that would apply in my case.

Full class code below, it's a fixed point implementation, hence all the >>16's...

Thanks all!

Code: Select all

#include "AudioStream.h"

class MoogFilter : public AudioStream
{
public:
	MoogFilter()
	   : AudioStream(1, inputQueueArray){ }
	virtual void update(void);
	virtual void calc(void);
        float FREQ = 0.5;
        float Q = 0.5;
        uint16_t f = 22000;
        uint16_t q = 32768;
        int32_t y1,y2,y3,y4,oldy1,oldy2,oldy3,x,oldx;
        float t1,t2,k,p,r;
        int32_t K,P,R;
        int16_t outpoot;
        
        
private:
	audio_block_t *inputQueueArray[1];
        
        
};

//#endif
///////////////////////////////////////////////////
//#include "utility/dspinst.h"

void MoogFilter::update(void)
{
	audio_block_t *block;
	block = receiveWritable();
	if (!block) return;
        for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
         
          // process input
         x = (int32_t)block->data[i] - ((R * y4)>>16);
   

    // four cascaded one-pole filters 
    y1 =  ((x*P)>>16) + ((oldx*P)>>16) - ((K*y1)>>16);
    y2 = ((y1*P)>>16) + ((oldy1*P)>>16) - ((K*y2)>>16);
    y3 = ((y2*P)>>16) + ((oldy2*P)>>16) - ((K*y3)>>16);
    y4 = ((y3*P)>>16) + ((oldy3*P)>>16) - ((K*y4)>>16);

    // clipper band limited sigmoid
    y4 -= ((((y4 * y4)>>16)*y4)>>16) /6;

    oldx = x; oldy1 = y1; oldy2 = y2; oldy3 = y3;
    
    outpoot = (int16_t)y4; //}}
    
    block->data[i]=outpoot;
                    
          }

	transmit(block);
	release(block);
}


//Calcs p, k, t1, t2 & r
void MoogFilter::calc(){
  
    FREQ=(f/65535.0);
    Q=(q/65535.0);
  
    // empirical tuning
    p = FREQ * (1.8 - (0.8 * FREQ));
    // k = p + p - T(1.0);
    // A much better tuning seems to be:
    k = 2.0 * sin(FREQ * 3.1415926535897931 * 0.5) - 1.0;

    t1 = (1.0 - p) * 1.386249;
    t2 = 12.0 + (t1 * t1);
    r = Q * (t2 + (6.0 * t1)) / (t2 - (6.0 * t1));
    
    R=(int32_t)(r*65536);
    K=(int32_t)(k*65536);
    P=(int32_t)(p*65536);
   
}


Post

You should move to a more modern implementation rather than this old nonsense. It barely works as it is, and is significantly more expensive than other methods.

Why are you using 16.16 ? On modern processors float is significantly faster than integer shifts. If you want to use integer I would recommend utilizing a 64-bit result multiply and shifting that, which in some cases the compiler can optimize.

Code: Select all

// for visual studio
#include <intrin.h>

template <int shift>
#define INL __forceinline // or inline if it works
INL __int32 imul(const __int32 A, const __int32 B)
{
	return ((unsigned __int64)__emul(A, B) >> shift) & 0xFFFFFFFF;
}
Use this like: imul<31>(a, b)

Will produce faster code than the manual shift in most cases and gives you more bits instead. By adjusting the shift you can get whatever you need, 16.16, so on.

The __int32 allows msvc2010 to perform better optimization than with long. The msvc2010 stdint header uses long for int32_t for some reason rather than the compiler defines.

My suggestion though is that you should definitely look at more modern implementations.

Code: Select all

const float c = tan(hz * pi / sample_rate);
// if you want more harmonic, allow fb past 4.0, 4.5 is a good range.
// do not do this without oversample unless you want aliasing.
const float fb = 4.0f * shape(resonance);

const float g = 1.0f / (1.0f + c);
const float cg = c / (1.0f + c);
const float c2 = c * 2.0f;
const float cgfbr = 1.0f / (1.0f + fb * cg*cg*cg*cg);

// if you use saturation functions, recommended you use at least 2x oversample.
// "works" with linear and no oversample but results are very poor.
// if you only 2x oversample with linear, even zero-stuffing works better than nothing.
//upsample(input);

//for (int i = 0; i < oversamples; i++) {
	//const float in = oversample[i];

	const float prediction = 
		buffer[3] * g + 
		cg * (buffer[2] * g + 
		cg * (buffer[1] * g + 
		cg * (buffer[0] * g +
		cg * in)));
			
	const float xv = in - clamp(fb * prediction) * cgfbr;

	const float xw = saturate(g * (buffer[0] + c * xv));
	const float xx = saturate(g * (buffer[1] + c * xw));
	const float xy = saturate(g * (buffer[2] + c * xx));
	const float xz = saturate(g * (buffer[3] + c * xy));

	buffer[0] += c2 * (xv - xw);
	buffer[1] += c2 * (xw - xx);
	buffer[2] += c2 * (xx - xy);
	buffer[3] += c2 * (xy - xz);

	//oversample[i] = xz;
//}
// gain is compensated by 1.0 + fb to make up for that lost via negative feedback
//out = decimate() * (1.0f + fb);
out = xz * (1.0f + fb);
Simple examples for clamp() and saturate():

Code: Select all

float clamp(float x) { return x > 2.0f ? 2.0f : x < -2.0f ? -2.0f : x; }
float saturate(float x) { return x; } // could be tanh, etc. 
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

The Moog filter is well known for having soft clipping at every cascaded one-pole filter section. This soft clipping is often done with tanh() (which I've read was a good approximation of behavior of many analog components, especially OTA 's which form the basis for many voltage controlled parts like VCFs and VCAs).

Tanh() is kinda slow so you should probably also look into soft clipping polynomials too (my favorite is y = 1.5*x - 0.5*x*x*x, which can be used after had clipping to -1..1, which can be done with something like y = 0.5*(abs(x+1) - abs(x-1)) or the code aciddose suggested).

It might also be sufficient to soft clip only the resonance path instead of every cascaded filter section, but of course this doesn't give you the famous slightly-distorted Moog filter sound...

Post

It does in fact give you "a" distorted sound. There is no significant contribution from each diode and it certainly is not "clipping". If you believe it to be so, please give the math showing that to be true.

It will not be sufficient to clip in the feedback path alone however as the most significant contribution comes from the inputs. The output buffer is reasonably linear.

So, you need to apply a "tanh-like" (tanh is not accurate) to the input signal as well as the output before feedback. This is the same point at which the main output is taken, in most cases it will be accurate enough to combine them.

So, you get this:

stage1 = saturate1(input - feedback)
stage2 = stage1
stage3 = stage2
stage4 = stage3
output = saturate2(highpass(stage4))
feedback = highpass(output)

That would be reasonably accurate, although the input and feedback also have highpass applied to them. The input highpass will be significant, the feedback can for the most part be combined into the output highpass.

Load this up in ltspice if you like:

Code: Select all

*** rails ***
V1 8 0 DC 10
V2 12 0 DC -10

*** input ***
R45 3 9 33k
C5 27 9 10u
R10 27 16 470

*** "ladder" ***
* voltage divider for base biases
R11 16 30 150
R12 30 31 150
R13 31 32 150
R14 32 33 150
R15 33 8 220
* input LTP
Q3 21 27 28 Q2N3904
Q4 22 19 28 Q2N3904
* "stage 1"
C1 21 22 10n
Q5 25 30 21 Q2N3904
Q6 26 30 22 Q2N3904
* "stage 2"
C2 25 26 10n
Q7 23 31 25 Q2N3904
Q8 24 31 26 Q2N3904
* "stage 3"
C3 23 24 10n
Q9 5 32 23 Q2N3904
Q10 4 32 24 Q2N3904
* "stage 4"
C4 5 4 10n
Q11 8 33 5 Q2N3904
Q12 8 33 4 Q2N3904

*** feedback ***
C9 6 17 10u
* resonance 50k log
R114 18 17 1
* trimmer 1k + 150
R9A 20 19 990
R9B 19 18 10
R8 16 20 150

*** input bias ***
* this capacitor is 200u on schematic,
* 33 is more than enough for quick test
* larger values take longer to settle
C6 16 0 33u
R7 0 16 200

*** output buffer ***
R17 6 8 220
R18 7 6 1k
R19 0 11 47k
R20 12 13 220k
R21 12 15 1k
R22 12 14 220k
R23 0 10 47k
C7 11 5 220n
C8 10 4 220n
Q13 7 11 13 Q2N3904
Q14 7 13 15 Q2N3904
Q15 8 10 14 Q2N3904
Q16 8 14 15 Q2N3904

*** expo current source ***
* built with pnp-buffered npn transistor,
* in this case we just use a current source
R6 29 28 680
Is1 29 0 4u

* input signal 100hz 0...1 pulse
V4 3 0 DC 0 PULSE(0 1 0 10u 10u 5m 10m)

.TRAN 10u 100m 0 10u
.MODEL Q2N3904 NPN(IS=6.73E-15 BF=416.4 VAF=74.03 IKF=0.06678 ISE=6.73E-15 NE=1.259 BR=0.7374 RB=10 RC=1 CJE=4.5E-12 TF=3.012E-10 XTF=2 VTF=4 ITF=0.4 CJC=3.638E-12 MJC=0.3085 TR=2.395E-7 XTB=1.5 KF=9E-16 )
.END
I've cleaned things up most of the way. The component numbers don't all match the schematic, I've dropped the 60x part.

The output is v(9), input is v(3), feedback is v(6) before the capacitor/pot.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

aciddose wrote:You should move to a more modern implementation rather than this old nonsense. It barely works as it is, and is significantly more expensive than other methods.
I'd love to, but I'm finding it hard to find them! I was going to try the Moog ladder next. What kind of filter is the code that you provided?
aciddose wrote: Why are you using 16.16 ?
My program is running on an embedded platform, and all the core DSP instructions are designed for int16_t. I won't bother actually using the instructions until the filter works, because it involves packing integers and makes the code near impossible to follow.


And MadBrain, I don't mind 'soft clipping' but mine distorts like crazy! Thanks for you info.

Post

The code I posted is configured to be OTA-like. For example the SH-09 filter built from four BA662 with a diode clamp on feedback and no highpass would match this.

It really depends upon a very good set of functions: upsample, decimate, clamp and saturate. It will only sound like a IR3109 if you give it functions that provide sufficient anti-aliasing and accurate diode-clamp and OTA input/output saturation behavior.

I also posted a description of a moog-like configuration. The saturation there applies only to the input / first stage and output after a highpass filter.

So you can take exactly the same code but highpass filter and saturate the feedback signal. You can probably just use a scaled version of the feedback as output and it will be accurate enough.

If you want a minimoog-like filter, do not saturate anything but the first stage and output. The highpass filters on the input and feedback are also critical.

Also note that you need to include some bias / dc offset into your saturation functions. The even-harmonic of the real life circuit is due to mismatching between transistors and the resulting DC offsets and therefore asymmetrical saturation characteristic.

The saturation of the first stage and output are very distinct, this is why I labelled them saturate1 and saturate2.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

By the way, the obvious, "naive" method to limit the signal in your existing filter is to simply limit the signal.

Literally,

Code: Select all

   // clipper band limited sigmoid
    y4 -= ((((y4 * y4)>>16)*y4)>>16) /6;

vs.

    y4 = y4 > maximum ? maximum : y4 < minimum ? minimum : y4;
This should solve your problem. The "band-limited sigmoid" barely works at all and while it may not produce the harmonics a hard-clip will produce, the hard-clip will produce little with feedback/signal levels below unity anyway.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Thanks for that info, aciddose. That really helps me to experiment.

I'm finding it difficult to find resources with information on up-to-date modelling of various filters. When I find some code, it's difficult to know how old /respected that method is. I'm learning that MusicDSP is not the place anymore! Do you know of any good sites / books to start?

Thanks

Post

The leading edge isn't really published anywhere. MusicDSP has never been a good place to start, unless you just want a roughly working implementation of something very well known.

Somebody else should be able to give you some advice; All I can say is that you're probably looking at years and years and years of experience working in DSP and electronics to get a really good grasp of what is going on. If you just want to try to keep track of what is being discussed, forums like KVR are not too bad. You certainly won't find very leading edge stuff though. It'll be reused, recycled, rehashed into different formats.

In your situation the best implementation I've found (few bits, little processing power) is actually a "state-variable" or known widely as "Chamberlin" filter due to: Hal Chamberlin, "Musical Applications of Microprocessors," 1985.

You can compensate the feedback level and run the filter with 2x zero-order hold and naive decimation for very reasonable results. As frequency increases feedback will need to decrease in order to maintain stability. This will increase the Q of the filter, so I call this "minimum-Q". Definitely the best place to look for a quick&dirty implementation that works well and is very efficient.

The information I'm providing regarding filter structure and so on is very hard to source anywhere online. You'll find like with most subjects the signal to noise ratio (accurate vs. inaccurate) for the information is poor.

Learn and double-check everything yourself. Never take at face value what you read.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Some of the old analog synths would get loud as hell and distort at large Q, but others seemed to "automagically" lower gain at high Q settings, so that the overall volume of a patch would fall at high Q but the razor sharp filter sweeps remained clean sounding. Clean being a relative word of course because they were never "squeaky clean hifi".

I think the auto gain reduction at high Q was just a feature of some of the vcf designs, not some intentional workaround like an addon circuit to trim input level according to Q. I think I studied that at some time long ago, but can't recall details at all. Some of such circuits MAY have inserted a spare vca in the circuit for that purpose.

IIRC, for instance the filters in the arp/rhodes chroma polaris sounded sweet and clean under almost all conditions, difficult to make the machine sound "nasty distorted".

Can't offer advice on emulating the various filter distortions, and haven't even written a plugin for a few years. But in the "intended to be clean" plugins I've written, would adjust the gain of the filters according to Q to make them "well behaved".

Just saying, not all the old synths were out of control messes of distortion at high Q.

Post

The reason the 4-stage series lossy integrator reduces volume at high feedback amounts is that it uses negative feedback. So you subtract the output from the input. This obviously reduces the input by the amount of feedback and how correlated the output is to the input.

It works out to 1 / (1 + feedback). So to compensate that the input level is increased by feedback. Usually very desirable providing gain adjustment separately and at the discretion of the operator with capability to handle a significant amount of headroom.

For example, the tb-303 implements this. The sh-101 does also. (The feedback signal is also mixed in with the output.)
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Thanks aciddose. Thinking more about it, have dredged up a foggy recollection of one of the old curtis appnotes describing how to configure one of their filter chips either way, ie either constant passband gain or constant resonance peak gain, or something like that. Perhaps remembering wrong.

Post

No, that sounds right. The circuit is very, very basic. Some people prefer the constant peak gain, but this is very irritating if you attempt to adjust feedback in real-time. You have to get the volume adjustment just right to cancel out volume variations and the suddenly reduced volume will often make the sound sit wrong in a mix.

This is why in instruments like the TB-303 this feature is absolutely critical. Without it, making those real-time tweaks of resonance would be far more difficult.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Thanks aciddose for the perspective. Shows a blindspot in my perspective, because after a patch or effect is programmed, I rarely modulate it in real time, thinking about notes, timings, harmonies, meta structure of the music rather than musical manipulation of timbre thruout a song.

Therefore the issue of keeping an evolving timbre pegged at a desired mix level thruout a song, hadn't ocurred to me. I probably would "reflexively" deal with such an issue using a dynamics processor, but filter saturation does offer an alternate way to "get er done". Perhaps a better way to do it if the saturation is pleasing to the ear.

I like distortion in some contexts, but may suffer from old ingrained assumption distortion == bad. :)

Post Reply

Return to “DSP and Plugin Development”