A simple low-cut filter
We can use a biquad-filter class that comes with Plug'n Script to make different kinds of filters.
- // include filter class and PI constant
- #include "../library/BiquadFilter.hxx"
- #include "../library/Constants.hxx"
- // script name
- string name="Biquad Low-Cut filter";
- // make an input parameter to control cutoff frequency
- array<double> inputParameters(1); // how many input controls we need
- array<string> inputParametersNames={"Cutoff freq"}; // name of input param
- array<string> inputParametersUnits={"Hz"}; // which units to use
- array<double> inputParametersMin={20}; // min value
- array<double> inputParametersMax={5000}; // max value
- array<double> inputParametersDefault={250}; // default value
- // index of input param just for convenience
- const int CUTOFF = 0;
- // create a filter for all channels
- KittyDSP::Biquad::Filter myFilter(audioInputsCount);
- // audio per-sample processing function
- // process all channels
- }
- // is called when input parameters change
- // update filter cutoff frequency
- myFilter.setHighPass(2 * PI * inputParameters[CUTOFF] / sampleRate);
- }
Short info about filters
When we make an EQ or something similar, we use so called filters, which are mathematical equations or functions (from simple to very complex), that alter the signal when it passes through. To calculate current sample value they usually use previous sample values or previous precalculated values from the buffer.
There are IIR and FIR filters. FIR is doing a convolution, it multiplies and sums each sample with so called "kernel" (an array of numbers). It's like an "impulse" that we load for convolution reverb or convolution-based amp sim. Using such pre-calculated "kernels" (they can be recalculated "on-the-fly") allows creating very precise filters, and making linear-phase one is easy with FIR. Though usually FIR filter require more CPU, depending on kernel size. Because of that they are not often used in Plug'n Script AngelScript API (because AngelScript is comparatively slow with array access and some other stuff). But in C++ they are ok.
IIR filters don't rely on convolution, they use previous precalculated values to calculate new sample, and they are usually easier on CPU. Though it's more difficult to make them linear-phase, and if you dig deeper, there's a lot more to learn. There are many different types of IIR filters. Biquad filter is one of them, and it sounds good!
Explanation
First, we create a myFilter object for the required number of channels:
- KittyDSP::Biquad::Filter myFilter(audioInputsCount);
Don't be scared of these "::" in class name. If you're not familiar with Namespaces, they are just a way to make code better organized. We could use value 2 instead of audioInputsCount (for stereo track), but this way we make it work for all channel configurations.
Before the first call to processSample, the updateInputParameters is called (as well as after every change of input parameter), where we set the parameters of a filter. In our case we set it to be lowcut and use a formula to set the desired frequency in Hertz.
- // inputParameters[CUTOFF] holds our cutoff frequency in Hz
- myFilter.setHighPass(2 * PI * inputParameters[CUTOFF] / sampleRate);
We could write inputParameters[0] instead, but using constant for parameter index is convenient, especially in longer scripts.
And finally we apply a filter to all channels inside processSample function.
This myFilter.processSample takes our array of current sample for all channels, calculates new sample values and updates them in the same array. The myFilter.processSample also can process channels individually, so we could write it like that:
- for(uint channel=0;channel<audioOutputsCount;channel++) {
- // process each channel individually
- }
- }
The full code of out low-cut filter script can be found at the top of this page.
Different types of filters
If you take a look at "library/BiquadFilter.hxx" you'll see different types of filter available.
- // different methods from biquad filter class
- setLowPass(double theta);
- setHighPass(double theta);
- setResonantLowPass(double theta,double q /*peak value*/);
- setResonantHighPass(double theta,double q /*peak value*/);
- setLowShelf(double theta,double doubleSquareRootGain);
- setHighShelf(double theta,double doubleSquareRootGain);
- setPeak(double theta,double halfBwInOctava,double sqrtGain);
- setBandPass(double theta,double halfBwInOctava);
- setNotch(double theta, double halfBwInOctava);
- setAllPass(double theta, double Q);
- // there's also a function to reset filter buffer
- resetState();
As you see, the parameters for these methods are not in Hertz and Decibels, so to calculate them you can use formulas.
- double theta = 2 * PI * frequencyInHertz / sampleRate;
- double doubleSquareRootGain = sqrt(pow(10, gainInDecibels / 40.0)); // for shelf filter
- double sqrtGain = sqrt(pow(10, gainInDecibels / 20.0)); // for peak filter
For resonant LowPass and HiPass you can use q values > 0. Value ~0.707 gives a flat cutoff (with minimum resonance). Alternatively you can use values ranging from 0 (flat) to 1 (maximum) and convert them like this:
- // for setResonantLowPass and setResonantHighPass
- // map values 0..1 to range from flat (-3dB boost) to 20 dB resonance boost
- double q = pow(10, (-3 + Q_FROM_0_TO_1 * 23) / 20);
You can experiment with Q for AllPass as well as halfBwInOctava for Peak, Notch and BandPass. These values should also be > 0.
There are more examples in factory scripts located in "Filter" folder.