[sharing] fast-response limiter
This didn't seem grand enough for a toolbox entry, so here is a brief form announcement. The description and download is here:
Hey, thanks, this might come in handy !
You're most welcome. It's nothing really complicated, but I never saw a limiter with overlapping windows to reduce response time, so it seemed worth sharing. It'd be easy enough to build one with four or eight overlapping windows and improve response time even more. the same idea could improve quality of compressors and expanders, and when I have time, I'll put together a kit of them.
This much-improved version adjusts for DC bias, applies saturation and distortion, increases response time to 50 msec, and does not require attack or release params. The dc bias removal allows proper adjustment of gain. The 50msec window supports frequencies down to 20Hz. There still can be higher transient peaks within the 50msec window, so it's clipped to +6B, but Im experimenting with adding an immediate response to transients for my synthLab library, which might be released by the time you read this.
Param sat; // 0 = limit only,
// 0~1 mixes saturation,
// 1 = perfect saturation,
// >1 adds mirror distortion
History fclck, period, hi, lo, gain, bias, range, offset, limit;
// set up limiter clock
period = 1 / mstosamps(50);
fclk = (fclk >1)? 0 : fclk + period;
if (change(fclk) > 0.5) {
if(lo >0){ // calc dc offset
gain = (hi - lo) * .5;
bias = neg(gain) - lo;
} else if (hi <= 0){
gain = (lo -hi) * .5;
bias = gain + lo;
} else {
gain = (hi - lo) * .5;
bias = (hi + lo) * -.5;
}
gain = (gain !=0)? min(1/gain, 1) : 0; // calc limiter factor
lo = 1000000000; // reset
hi = -1000000000;
}
hi = max(hi, in1);
lo = min(lo, in1);
offset = offset + period * (bias - offset);// ramp bias to new val
limit = limit + period * (gain - limit);// ramp limit to new val
x = (in1 + offset) * limit;
x = mix(x, tanh(x), 1 -abs(sat));// saturate
out1 = (sat>0)? fold(x *(sat*8 +1), -1, 1) : clip(x, -2, 2);//distort
An ideal limiter with optional saturation and distortion: I think I got it! Better than a filter based one, because it at most distorts half a wave cycle once every 50msec. What do you know. Something genuinely new that actually works, at least I think it works after half an hour of testing, but I'll keep hammering it for a while just to makes sure. I've been running it 4x oversampled, but maybe that's not so necessary with this redesign, except for reducing distortion on the first half wave when a changed signal exceeds the prior limiter level.
//---------------------------------------------------------------
saturator6(in1, sat, clk, period) {
// in1 : input in any range
// sat : saturation level, 0~1
// clk : zero crossing causes level resampling, typ. 20Hz.
// period : period of clk in msec.
// Return: Unity gain output.
Data qtanh(16384);
History hi, lo, peak, gain, amp, bias;
if (change(clk) > 0) {
bias, gain, amp = biasGain(in1, lo, hi);
lo = 999999999; // reset
hi, peak = -999999999;
}
lo = min(lo, in1); // remember min, max, peak
hi = max(hi, in1);
peak = max(peak, max(lo, hi));
flag = 0;
// if transient peak >+/-1 between clks,
// increase bias & gain at once
if (peak > amp + bias) {
bias, gain, amp = biasGain(in1, lo, hi);
flag = 1;
}
L, C, R = mixer3(sat);
// add bias for both left and right channels
signal = in1 + ramp1(bias, period, 1);
// left channel: limiter. Apply adjusted gain.
left = L * signal * ramp1(gain, period, 1);
// right channel: saturator. Apply tanh and mirror distortion.
right = tanh(in1 * 4);
right = (sat > 0)? fold(right *(R *8 +1), -1, 1) : C * right;
return left + right;
}
//---------------------------------------------------------------
// 3-way linear mixer, returns gain for three channels
mixer3(in1){
// x : -1= chan1, 0 = chan2, 1 = chan3
//return : gain for three channels in range 0~1
if(in1 >0) { return 0, 1 -in1, in1; }
else { return abs(in1), 1 +in1, 0; }
}
//---------------------------------------------------------------
// ramps to new val at rate set by incr
ramp0(in1, incr){
History prev;
prev = prev + incr * (in1 -prev);
return prev;
}
//---------------------------------------------------------------
// ramps to new val, or sets to in1 at once if flag=1
ramp1(in1, incr, flag){
History prev;
prev = (flag ==1)? in1 : prev + incr * (in1 -prev);
return prev;
}
//---------------------------------------------------------------
//gain and bias of windowed signal, used by limiter and saturator
biasGain(in1, lo, hi){
// lo : min window value
// hi : max window value
// return: bias, amp, and 1/amp as gain to apply for unity output
amp, bias = 0;
if(lo >0){ // calc dc offset
amp = (hi - lo) * .5;
bias = neg(amp) - lo;
} else if (hi <= 0){
amp = (lo -hi) * .5;
bias = amp + lo;
} else {
amp = (hi - lo) * .5;
bias = (hi + lo) * -.5;
}
gain = (amp ==0)? 0 : min(1/amp, 1); // calc limiter factor
return bias, gain, amp;
}
//---------------------------------------------------------------
MAIN
//---------------------------------------------------------------
Data qtanh(16384);
Param sat, finit;
if(finit==0){
// for limter clock, captures peak of waves down to 25Hz
period = 1 / mstosamps(50);
for (x = 0; x <16384; x +=1){
y = (x==0)? -4 : (x/16384 * 8) -4;
qtanh.poke(tanh(y), x);
} finit = 1;
}
// set up limiter clock
clk = (fclk >1)? 0 : clk + period;
out1 = saturator5(in1, ramp0(sat, period), clk, period);