Skip to content

Filters

Phil Schatzmann edited this page Apr 13, 2024 · 86 revisions

I am supporting the following Filter implementations:

  • IIR Filter
  • FIR Filter
  • Bi-Quad Filter (first and second order)
  • Median Filter
  • Second Order Filter
  • A Chain of Filters
  • Any Custom Filter Implementation

Here is the link to the documentation

You will usually use these filter classes together with the FilteredStream class. The use of this class is quite flexible since the filter is applied when you read and when you write the data: So you can wrap either the copy source or the copy target stream in this class to achieve the same result.

Filter Example

Here is an example using the AudioKit which supports both I2S input and output on the same channel. We are reading the audio data from AUDIO_HAL_ADC_INPUT_LINE2, apply a FIR filter on each of the 2 channels and write the output to the built in amplifier:

#include "AudioTools.h"
 
uint16_t sample_rate=44100;
uint16_t channels = 2;
I2SStream i2s;

// copy filtered values
FilteredStream<int16_t, float> inFiltered(i2s, channels);  // Defiles the filter as BaseConverter
StreamCopy copier(i2s, inFiltered);  // copies filtered audio to output

// define FIR filter
float coef[] = { 0.0209967345, 0.0960112308, 0.1460005493, 0.0960112308, 0.0209967345};

// Arduino Setup
void setup(void) {  
  // Open Serial 
  Serial.begin(115200);
  // change to Warning to improve the quality
  AudioLogger::instance().begin(Serial, AudioLogger::Info); 

  // setup filters for all available channels
  inFiltered.setFilter(0, new FIR<float>(coef));
  inFiltered.setFilter(1, new FIR<float>(coef));

  // start I2S input and output
  Serial.println("starting KIT...");
  auto config = i2s.defaultConfig(RXTX_MODE);
  config.sample_rate = sample_rate; 
  i2s.begin(config);

  Serial.println("KIT started...");
}

// Arduino loop - copy sound to out 
void loop() {
  copier.copy();
}

The first type parameter of the FilteredStream specifies the data format of the audio data. On the ESP32 we usually deal with 16 bit signed integers hence we use int16_t. The second typed parameter specifies the data type, that is used to specify the filter parameters and do the filter calculations: float might be a good choice here. Then we pass the stream on which we want to apply the filter to as the first parameter to the constructor. The second parameter specifies the number of channels. When we use I2S we always get 2 channels!

We need to define separate filters for each channel: so inFiltered.setFilter(0, new FIR<float>(coef)); is assigning a FIR filter to the channel 0 and inFiltered.setFilter(1, new FIR<float>(coef)); is assigning a FIR filter to the channel 1.

Filter Data Type and Filter Values

The most challenging step is to design your filter and determine the corresponding filter input values. There a quite a few online filter design tools: I can recommend https://fiiir.com/.

Usually the filter values are proposed as real numbers (in the range between 0.0 and 1.0). To keep things simple I recommend that you use floats as a starting point when designing your filter:

FilteredStream<int16_t, float> inFiltered(kit, channels); 
float coef[] = { 0.0209967345, 0.0960112308, 0.1460005493, 0.0960112308, 0.0209967345};
inFiltered.setFilter(0, new FIR<float>(coef));

When moving to an integer data type you could potentially run into calculation overflows. So you should start with a integer type which is bigger then the audio data. Because the parameters will need to be provided as integers as well, we apply a factor: 32767.

FilteredStream<int16_t, int32_t> inFiltered(kit, channels); 
int32_t coef[] = { 688, 3146, 4784, 3146, 688};
inFiltered.setFilter(0, new FIR<int32>(coef, 32767));

Here is an overview of the processing times of 44100 samples with a 137 TAP (parameter) FIR filter by data type:

Type ESP32 RP2040
int32_t 318 ms 717 ms
int64_t 788 ms 2637 ms
float 264 ms 8660 ms
double 4607 ms 14843 ms

With the current filter implementation we recommend to use floats on an ESP32. Doubles are too slow to be useful for real time processing and ints are not providing any advantage and have potentially rounding issues. Please note that many online filter generators are generating code with doubles!

The related test sketch can be found here.

FIR Filter

We provide a finite impulse response (FIR) filter

const float b_coefficients[] = { b_0, b_1, b_2, ... , b_N };
...
inFiltered.setFilter(0, new FIR<float>(b_coefficients));

IIR Filter

We also support a infinite impulse response (IIR) filter

const float b_coefficients[] = { b_0, b_1, b_2, ... , b_P };
const float a_coefficients[] = { a_0, a_1, a_2, ... , a_Q };
...
inFiltered.setFilter(0, new IIR<float>(b_coefficients, a_coefficients));

BiQuadDF1

A first order BiQuadratic filter implementation.

const double b_coefficients[] = { b_0, b_1, b_2 };
const double a_coefficients[] = { a_0, a_1, a_2 };
...
inFiltered.setFilter(0, new BiQuadDF1<float>(b_coefficients, a_coefficients));

BiQuadDF2

When dealing with high-order IIR filters, they can get unstable. To prevent this, BiQuadratic filters (second order) are used.

const double b_coefficients[] = { b_0, b_1, b_2 };
const double a_coefficients[] = { a_0, a_1, a_2 };
...
inFiltered.setFilter(0, new BiQuadDF2<float>(b_coefficients, a_coefficients));

SOSFilter

Instead of manually cascading BiQuad filters, you can use a Second Order Sections filter (SOS).

const double sosmatrix[][6] = {
    {b1_0, b1_1, b1_2,  a1_0, a1_1, a1_2 },
    {b2_0, b2_1, b2_2,  a2_0, a2_1, a2_2 }
};
const double gainarray[] = {1, 1};
...
inFiltered.setFilter(0, new SOSFilter<float,2> filter(sosmatrix, gainarray));

Chained Filters

We can chain multiple filters, so that they are processed in sequence.

const float coef[] = { b_0, b_1, b_2, ... , b_N };
const float coef1[] = { b_10, b_11, b_12, ... , b_1N };
...
inFiltered.setFilter(0, new FilterChain<float, 2>({new FIR<float>(coef),new FIR<float>(coef1)}));

Testing Filters

To test the filters, I came up with the following approach. In an Arduino Sketch I generate tones from musical notes from low to high on 2 channels. One channel is unprocessed and the second channel will go thru the low pass filter with a cut off frequency of 2000. The result is stored in a wav file. Then we can do a frequency analysis on each channel in Audacity.

  • Unprocessed Frequencies:
unfiltered
  • Frequencies after Filter:
filtered

Your Custom Filter

There are plenty of internet sites that help you generate your custom filter code. To use this in our framework all you need to do is to implement a subclass of Filter. In this example we create a filter based on int:

class MyFilter : public Filter<int> {
 public:
  Filter() {
     // your initialization
  };
  virtual ~Filter() {
     // your cleanup 
  }
  virtual int process(int in) {
     // add your logic 
  };
};

Here is a complete example which is based on code that was generated from http://t-filter.engineerjs.com/

#include "AudioTools.h"

class MyFilter : public Filter<int> {
 public:
  MyFilter() {
     SampleFilter_init(&sampleFilter);
  };
  virtual ~MyFilter() = default;
  virtual int process(int in) {
     SampleFilter_put(&sampleFilter, in);
     return SampleFilter_get(&sampleFilter);
  };

 protected:
 static const int SAMPLEFILTER_TAP_NUM = 27;

 struct SampleFilter {
  int history[SAMPLEFILTER_TAP_NUM];
  unsigned int last_index;
 } sampleFilter;

 const int filter_taps[SAMPLEFILTER_TAP_NUM] = {
    272,
    449,
    -266,
    -540,
    -55,
    -227,
    -1029,
    745,
    3927,
    1699,
    -5616,
    -6594,
    2766,
    9228,
    2766,
    -6594,
    -5616,
    1699,
    3927,
    745,
    -1029,
    -227,
    -55,
    -540,
    -266,
    449,
    272
  };
  
  void SampleFilter_init(SampleFilter* f) {
    int i;
    for(i = 0; i < SAMPLEFILTER_TAP_NUM; ++i)
      f->history[i] = 0;
    f->last_index = 0;
  }
  
  void SampleFilter_put(SampleFilter* f, int input) {
    f->history[f->last_index++] = input;
    if(f->last_index == SAMPLEFILTER_TAP_NUM)
      f->last_index = 0;
  }
  
  int SampleFilter_get(SampleFilter* f) {
    long long acc = 0;
    int index = f->last_index, i;
    for(i = 0; i < SAMPLEFILTER_TAP_NUM; ++i) {
      index = index != 0 ? index-1 : SAMPLEFILTER_TAP_NUM-1;
      acc += (long long)f->history[index] * filter_taps[i];
    };
    return acc >> 16;
  }

};
Clone this wiki locally