Shelving filter

Digital filters like the Bessel, Butterworth or Chebychev filter cut down the output signal to 0 in the blocking band. This is not always wanted. In digital equalizing for instance we just want to reduce or boost the output signal by a certain amount in the blocking band. This can be done by the Shelving filter. The shelving filter uses an amplification factor G that increases the output signal if bigger than 1 or decreases it if smaller than 1.

Drawing the transfer function for different values of G gives the specific image that gives the filter its name:


Shelving

It looks like (with some fantasy :-)) a continental shelf (https://en.wikipedia.org/wiki/Continental_shelf). In the German speaking areas it’s even better. There they call “Kuhschwanz Filter”, (https://de.wikipedia.org/wiki/Kuhschwanzfilter) what means exactly translated "cow tail filter", because the transfer function would look like a waving cow tail. I think they have no idea about cows.


Shelving
Cows are so lovely beings

Cows don’t care for digital filtering :-)


High pass Shelving filter

As the Shelving filter uses an amplification factor which can be bigger or smaller than 1, it is not that easy to distinguish a high pass from a low pass filter. Usually a high pass filter affects the low frequencies of a signal and leave the high frequency’s untouched. The same is here: At the high pass Shelving filter the transfer ration at high frequency’s is 1.

The transfer ration for lower frequencies depends on the amplification G.The transfer function of the Shelving filter in the Laplace domain is:


Shelving

with


Shelving

and G as the amplification.


On the first glimpse that looks quite strange. Complex numbers in the s domain are rather unusual. But if we spend the effort and do some calculations, we get for

N = 2


Shelving

and the enumerator


Shelving


and denominator


Shelving


the transfer function becomes:


Shelving



which is similar to


Shelving



As it is usually written in the literature.

(See Complex numbers for the math with complex numbers)


For N = 3:


Shelving



And, similar to above, the transfer function becomes:


Shelving



For N = 4


Shelving
Shelving
Shelving
Shelving



The transfer function becomes a bit bigger


ShelvingShelving




And for N = 5


Shelving
Shelving
Shelving
Shelving
Shelving




A kind of an order becomes visible for the transfer function:


ShelvingShelving




These transfer functions can be written in general formulations:


For N is even we get:


Shelving




Only odd n’s.


For N is odd we get:


Shelving




Only even n’s.



And that’s quite easy to be implemented in a C# function:


public void CalcShelving(double amp)
{
     int i = 1;
     a_s = new double[1];
     b_s = new double[1];
     double[] poly = new double[3];
     double gl = Math.Pow(amp, 1.0 / order);
     double tempD;
    
     if (order % 2 == 0) // order is even
     {
         a_s = new double[1];
         b_s = new double[1];
         b_s[0] = 1.0;
         a_s[0] = 1.0;
         for (i = 1; i < order; i += 2)
         {
              poly[0] = 1.0;
              poly[1] = 2.0 * Math.Cos(Math.PI * i / 2.0 / order) * gl;
              poly[2] = gl * gl;
              b_s = Poly.Mult(b_s, poly);
              poly[0] = 1.0;
              poly[1] = 2.0 * Math.Cos(Math.PI * i / 2.0 / order);
              poly[2] = 1.0;
              a_s = Poly.Mult(a_s, poly);
         }
     }
     else // order is odd
     {
         a_s = new double[2];
         b_s = new double[2];
         b_s[0] = 1.0;
         b_s[1] = gl;
         a_s[0] = 1.0;
         a_s[1] = 1.0;
         for (i = 2; i < order; i += 2)
         {
              poly[0] = 1.0;
              poly[1] = 2.0 * Math.Cos(Math.PI * i / 2.0 / order) * gl;
              poly[2] = gl * gl;
              b_s = Poly.Mult(b_s, poly);
              poly[0] = 1.0;
              poly[1] = 2.0 * Math.Cos(Math.PI * i / 2.0 / order);
              poly[2] = 1.0;
              a_s = Poly.Mult(a_s, poly);
         }
     }
 
     for (i = 0; i < b_s.Length / 2; i++)
     {
         tempD = b_s[i];
         b_s[i] = b_s[b_s.Length - 1 - i];
         b_s[a_s.Length - 1 - i] = tempD;
     }
}




The class Poly is a small static class for polynomial math I implemented.

This function returns the transfer function in the s domain in the arrays a_s and b_s. With a_s[0] as the first parameter a0 in the polynomial.

Shelving

This transfer function must be converted into the transfer function in the z domain which is done by the bilinear transformation (see Digital filter design):

Shelving

with

Shelving

and fc = cut off frequency of the filter and fs = sampling frequency. If this transformation is done and the compound fractions are removed we get.

Shelving

For this transformation I use more or less the same function I already used in Butterworth filter:


public void TransformToZPlane()
{
     int i, j;
     List<double[]> aa = new List<double[]>();
     List<double[]> bb = new List<double[]>();
     double[] tempA = { 1.0, 1.0 };
     tempA[0] = 1;
     tempA[1] = 1;
 
     for (i = 0; i < a_s.Length; i++)
     {
         aa.Add(new double[] { 1.0, -1.0 });
         bb.Add(new double[] { 1.0, -1.0 });
     }
 
     for (i = 0; i < a_s.Length; i++)
     {
         double[] tempEl = aa.ElementAt(i);
         tempEl = Poly.Mult(Poly.Power(tempA, i), Poly.Power(tempEl, a_s.Length - 1 - i));
         tempEl = Poly.Mult(tempEl, a_s[i] * Math.Pow(2.0 / wc, a_s.Length - 1 - i));
         aa.RemoveAt(i);
         aa.Insert(i, tempEl);
     }
 
     for (i = 0; i < b_s.Length; i++)
     {
         double[] tempEl = bb.ElementAt(i);
         tempEl = Poly.Mult(Poly.Power(tempA, i), Poly.Power(tempEl, b_s.Length - 1 - i));
         tempEl = Poly.Mult(tempEl, b_s[i] * Math.Pow(2.0 / wc, b_s.Length - 1 - i));
         bb.RemoveAt(i);
         bb.Insert(i, tempEl);
     }
 
     a_z = new double[aa.Count];
     for (i = 0; i < a_z.Length; i++)
     {
         a_z[i] = 0;
         for (j = 0; j < aa.Count; j++)
         {
              a_z[i] = a_z[i] + aa.ElementAt(j)[i];
         }
     }
 
     b_z = new double[bb.Count];
     for (i = 0; i < b_z.Length; i++)
     {
         b_z[i] = 0;
         for (j = 0; j < bb.Count; j++)
         {
              b_z[i] = b_z[i] + bb.ElementAt(j)[i];
         }
     }
 
     for (i = 0; i < b_z.Length; i++)
     {
         b_z[i] = b_z[i] / a_z[0];
     }
     for (i = a_z.Length - 1; i >= 0; i--)
     {
         a_z[i] = a_z[i] / a_z[0];
     }
}



The variable wc is the T from above.

Now the transfer function in the z domain is in the arrays a_z and b_z and the Shelving filter is ready to use like:

Shelving


The following code fragment shows how I implemented that:


t = 2.0 * Math.PI * fc / fs;
TShelving shelv = new TShelving(order, t);
shelv.CalcShelving(G, cbHighPass.Checked == true);
shelv.TransformToZPlane();
 
a = shelv.a_z;
b = shelv.b_z;
 
// create a signal
for (i = 0; i < datapoints; i++)
{
     t_in[i] = i / fs;
     y_in[i] = 20.0 * Math.Sin(2.0 * Math.PI * t_in[i] * f);
}
 
// as long as there are not enough values to fill the polynomials
for (i = 0; i < a.Length; i++)
{
     y_out[i] = 0;
     for (j = 0; j <= i; j++)
     {
         y_out[i] = y_out[i] + y_in[i-j] * b[j];
     }
     for (j = 1; j <= i; j++)
     {
         y_out[i] = y_out[i] - y_out[i-j] * a[j];
     }
}
 
// now there are enough values
for (i = a.Length; i < datapoints; i++)
{
     y_out[i] = 0;
     for (j = 0; j < a.Length; j++)
     {
         y_out[i] = y_out[i] + y_in[i - j] * b[j];
     }
     for (j = 1; j < a.Length; j++)
     {
         y_out[i] = y_out[i] - y_out[i - j] * a[j];
     }
     if ((y_out[i] > max) && (i > 100))
         max = y_out[i];
}


With N = 3, cut off frequency = 300 Hz and samlpling frequency = 10 kHz that creates the following transfer functions for different G’s

Shelving



And with a fixed G = 2.0 and N = 2 to 5.

Shelving



Looks possibly a bit confusing as it is a high pass filter and it increases the output at low frequencies. But high pass means the filter affects to low frequencies ant leaves the high frequencies untouched.


Or for G = 0.4:

Shelving



Low pass Shelving filter

To get the same functionality on the high frequency side we have to do a high pass to low pass transformation. This is done by the substitution of s by 1/s in the Laplace domain.

For a transfer function like

Shelving

hat is

Shelving

If we remove the compound fractions, that just switches the order of the ai and the bi values what can be done in the function


public void CalcShelving(double amp, bool bHighPass)



by adding the parameter bHighPass and modifying the lines at the end of the function to:


if(!bHighPass)
{
     for(i=0; i < a_s.Length / 2; i++)
{
         tempD = a_s[i];
         a_s[i] = a_s[a_s.Length - 1 - i];
         a_s[a_s.Length - 1 - i] = tempD;
     }
 
     for (i = 0; i < b_s.Length / 2; i++)
     {
         tempD = b_s[i];
         b_s[i] = b_s[b_s.Length - 1 - i];
         b_s[a_s.Length - 1 - i] = tempD;
     }
}


That’s all :-)


With this we get the graphs:

For N = 3 and various G’s.

Shelving




With fixed G = 2.0 and various N’s

Shelving



With fixed G = 0.4 and various N’s.

Shelving



The Shelving filter could easily be transformed into a band block filter as I did it in Digital band pass and band stop filter. But I think that does not make too much sense as this filter would have the same G on the high and low frequency side. That wouldn’t be that cool :-)


A online solver in JavaScript, that returns the filter parameters, can be found on Shelving filter


C# Demo Project Shelving filter
  • Shelving.zip