Yes, it is oversampled in a "cheap" way suggested by Nigel Redmon:
http://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/
I doubt that it sounds much better at the frequency range over which it is stable, but apparently it is unstable above ~fs/6, so at fs=44.1k that would mean it shouldn't be used for fc > 7.4 kHz. Oversampling 2x makes it more accurate at the higher frequency ranges so the difference might be audible above 2 kHz fc, but I haven't compared.
Nigel Redmon suggested running the filter on the same sample twice. I extended this slightly by using linear interpolation on the input before running the filter on it but still using zero order hold resampling of the output, so I'm not burning a great deal of resources on resampling.
Here's the working part of the code:
void
svf_tick_n(sv_filter* s, float* x, int N)
{
int i = 0;
//double-sample rate processing, linear interpolated
for(i=0; i < N; i++)
{
//Run 1 : Linear interpolate between x[n-1] and x[i]
s->lpf = s->lpf + s->f * s->bpf;
s->hpf = s->g5 * (x[i] + s->x1) - s->lpf - s->q * s->bpf; //g5 includes the 0.5 factor for averaging x[n] and x[n-1]
s->bpf = s->f * s->hpf + s->bpf;
//Run 2
s->lpf = s->lpf + s->f * s->bpf;
s->hpf = s->g * x[i] - s->lpf - s->q * s->bpf;
s->bpf = s->f * s->hpf + s->bpf;
s->x1 = x[i];
x[i] = s->lmix * s->lpf + s->hmix * s->hpf + s->bmix * s->bpf;
if(s->normalize == false) x[i] *= s->gout;
}
}
Below is the function I am using in which frequency response is continuously modulated and distortion is added. On my TODO list is to also linearly interpret the frequency coefficient, but my ears are telling me it's probably a waste of CPU resources.
void
svf_tick_fmod_soft_clip_n(sv_filter* s, float* x, float *f, int N)
{
int i = 0;
//double-sample rate processing, linear interpolated
for(i=0; i < N; i++)
{
//Run 1 : Linear interpolate between x[n-1] and x[i]
s->lpf = soft_clip(s, s->lpf + 0.5*(f[i] + s->f1)* s->bpf);
s->hpf = soft_clip(s, s->g5 * (x[i] + s->x1) - s->lpf - s->q*s->bpf);
s->bpf = soft_clip(s, f[i]* s->hpf + s->bpf);
//Run 2
s->lpf = soft_clip(s, s->lpf + f[i]* s->bpf);
s->hpf = soft_clip(s, s->g * x[i] - s->lpf - s->q*s->bpf);
s->bpf = soft_clip(s, f[i]* s->hpf + s->bpf);
s->x1 = x[i];
s->f1 = f[i];
x[i] = s->lmix*s->lpf + s->hmix*s->hpf + s->bmix*s->bpf;
if(s->normalize == false)
x[i] *= s->gout;
if(s->outclip)
x[i] = clip1(x[i]);
}
}
This is the part that adds the most CPU load, but it's the part that adds the most character to the filter that
I am unwilling to sacrifice:
inline float
soft_clip(sv_filter* s, float xn_)
{
float xn = s->drive*xn_;
if(xn > 1.0) xn = 1.0;
else if (xn < -1.0) xn = - 1.0;
else if(xn < 0.0) xn = (xn + 1.0)*(xn + 1.0) - 1.0;
else xn = 1.0 - (1.0 - xn)*(1.0 - xn);
return s->idrive*xn;
}
inline float
sqr(float x)
{
return x*x;
}
inline float
clip1(float x)
{
float thrs = 0.8;
float nthrs = -0.72;
float f=1.25;
//Hard limiting
if(x >= 1.2) x = 1.2;
if(x <= -1.12) x = -1.12;
//Soft clipping
if(x > thrs){
x -= f*sqr(x - thrs);
}
if(x < nthrs){
x += f*sqr(x - nthrs);
}
return x;
}