How to prevent cramping on filters?

Hi,
I just saw this video and tried to test it with one of the JUCE filters at 44100Hz and it is cramping a lot when it gets closer to the Nyquist freq(in my case I tried from 10K to 20K). When I try to increase the sample rate(as expected), it disappeared. I saw a few plugins that don’t cramp. I assume that they are using internal oversampling to be able to get beyond the Nyquist freq. I was planning to use the JUCE oversampling to do the same but this means that it will introduce a latency which I don’t want. Do you have any suggestions for solving this issue? oversampling without latency? I tried to create a simple oversampler to solve the issue but it didn’t work as I expected. My solution is something like this:
Let’s say I have 4 samples in my buffer.

inBuffer{10,11,12,13}

I simply create a new buffer that has inBuffer.getNumSamples()*2-1 and then insert the inBuffer with one sample space.
so the new buffer looks like this:

osBuffer {10,0,11,0,12,0,13}.

then I replace the “0” values with the average of the numbers that are near to it.
it becomes :

osBuffer {10, (10+11)/2.0f, 11, (11+12)/2.0f, 12, (12+13)/2.0f, 13}.

osBuffer {10, 10.5f, 11, 11.5f, 12, 12.5f, 13}.

I use this buffer for filtering.
Let’s say the new buffer after filtering is :

osBuffer {23, 44, 75, 32, 66, 788, 123}

after filtering I am deleting the extra samples at the extra indexes from the osBufffer and I am going back to the previous state of the buffer.

osBuffer {23,44, 75, 32, 66, 788, 123}

osBuffer {23, 75, 66, 123}. This method worked fine except for only one point. Sound-wise it works fine, with no latency, and also it is not cramping but somehow it lowers the volume of the total sound and when it gets closer to the Nyquist freq, the gain increases for a few dBs.

Could you please let me know if you have a better way to prevent cramping?

Juce biquads are using the bilinear transform, which leads to this “cramping” near nyquist. Oversampling is a poor solution. To do it properly you need very steep filters and these tend to mess up the phase completely near nyquist + at some point users will complain that they found missing super-high frequencies in their audio while competing products can do it correctly all the way up to nyquist.
The oversampling you describe is a simple fir filter which does not have a the required flat frequency response.

In my opinion the best public resource with a simple-ish non-oversampling solution can be found here:

To sum it up crudely… instead of transforming poles/zeros or coefficients of the biquad filters, the digital filter is built by trying to match an ideal biquad filter (in analog frequency space) using various constraints.

3 Likes

Thank you very much :pray: I will need to work on it a bit :slight_smile:

So I’ve seen this paper linked in a couple places and decided to give it a spin…

When trying a lowpass I’m getting a filter whose resonance starts rapidly increasing as the cutoff approaches Nyquist, despite using a consistent Q of 0.707. I’ve triple-checked that my coefficient calculations are as described in the paper — is there something I’m missing? (Probably.)

I’ve verified that using the regular BLT coeffs yield the expected result–so it’s not an issue with processing the filter output.

Coefficient calculation for a matched lowpass:

auto w0 = 2.0 * M_PI * (cutoff / (sampleRate * 0.5));
auto q = 1.0 / (2.0 * reso);
auto alpha = q * std::sin(w0);

a1 = -2.0 * std::cos(w0) / (1.0 + alpha);
a2 = (1.0 - alpha) / (1.0 + alpha);

auto f0 = cutoff / sampleRate;
auto freq2 = f0 * f0;
auto fac = (1.0 - freq2) * (1.0 - freq2);

auto r0 = 1.0 + a1 + a2;
auto r1_num = (1.0 - a1 + a2) * freq2;
auto r1_denom = std::sqrt(fac + freq2 / (reso*reso));
auto r1 = r1_num / r1_denom;

b0 = (r0 + r1) / 2.0;
b1 = r0 - b0;
b2 = 0.0;

Without having tested it in combination with your code, I looked up my implementation and the b0/b1/b2 parts seem to be the same. But I have a different way to calculate a1 and a2, I think this comes directly from the matched-Z transform:

  auto tmp = std::exp(-q * w0);
  a1 = -2.0 * tmp;
  if (q <= 1.0) 
    a1 *= std::cos(std::sqrt(1 - q * q) * w0);
  else 
    a1 *= std::cosh(std::sqrt(q * q - 1) * w0);  
  a2 = tmp * tmp;

It seems you were using a1 and a2 from the bilinear transform - but the method requires matched-z for the poles to avoid the bilinear cramping.

Ah okay, I was wondering whether my assumption to carry over the a’s from the BLT was correct or not. Either I skimmed some part of the paper that specified this, or I’m simply missing a bigger piece of the puzzle…

Thanks, I will try that out!