Hi Max, Vadim, whoever else might be interested-- I tested Max's first order code, named "BLT DF" filters, please suggest a better name if that isn't a good one. Compared against Vadim's Trapezoidal first-order filters.

Under my simple test if both do not have identical transfer functions, then at least they are VERY similar. I test compared LP, HP and AP at Fc = 100 Hz and also Fc = 5 kHz. Six tests. Test input was stereo 10 sec duration, -6 dB peak sine wave sweep, 20 to 20 kHz at 44.1 k samplerate.

All six tests null at all locations to -90.3 dB, probably because I forgot to disable dither in Reaper when rendering the test files. Am guessing it would null at or nearer to -inf if done without dither but am too lazy to repeat the tests.

At both 100 Hz Fc and 5 kHz Fc, the LP and HP -3 dB points are "very close to the same" but I don't measure exactly identical. They may be exact, with the error due to my testing somehow. At both 100 and 5 kHz the -3 dB points of LP and HP seemed "a tiny bit different from each other" but it could be pilot error. Close enough for rock'n'roll anyway. IOW, both Vadim's and Max's filters seem to perform identically, and both MAY have the identical VERY SLIGHT disagreement of LP -3 dB frequency versus HP -3 dB frequency. Or maybe I should have tested it some better way for more accurate result..

For the Fc=5 kHz LP and HP, I measured -3 dB in the ballpark of 4900 Hz but it is ambiguous to measure-- I just counted the number of samples in 8 wave periods surrounding -3 dB, but it might be closer than that to 5 kHz (or exactly 5 kHz for all I know). Anyway even if it is really "about 4900 Hz" then that is much closer than required by rock'n'roll.

I do think Max's first order code works better than some other "old fashioned" first order codes I tried in the past, begging the question why professors would put sucky code in their books.

But because at least casually both kinds of filter seem to behave so similer, it comes full-circle back to, at least in the first-order case, is there a reason to choose one or the other?

Maybe one of the filters works better on certain kinds of transients? Or maybe one filter is better-behaved when swept? Someone else would be better qualified to test that if so interested. The code is in Reaper jsfx, using jsfx "name space objects" which are similar enough to C that it shouldn't take long to convert to C/C++ should someone desire. I stopped using C++ for awhile because simple things are less frustrating in jsfx. If I'm gonna get frustrated doing code it would be like having a job.

- Code: Select all
`desc: FirstOrdFiltCompare jcjr First Order Filter Test`

//version: 0.1

// This effect Copyright (C) 2017 and later James Chandler Jr

// http://www.errnum.com

// License: GPL - http://www.gnu.org/licenses/gpl.html

//tags: First Order Filters

//author: James Chandler Jr

//A quick comparison test of first order LP, HP and AP filters

//One a trapezoid "zdf" filter adapted from Vadim Zavalishin

//The second a "BLT Direct Form" [z-1] filter adapted from Max Mikhailov suggestions

//

//Both filters process the left channel of the input file (identical input for both filters)

//Trapezoid filter writes to Left channel output

//BLT DF filter writes to Right channel output

//

//Test setup lines are above the line "@slider" near the bottom of code

//Uncomment one pair of lines to do a test, or make your own lines to test in another fashion

//

//I used a 10 second duration stereo sine wave sweep

//20 to 20 kHz at 44.1 k samplerate and -6 dB peak amplitude

//

//I tested LP, HP and AP at Fc = 100 Hz and Fc = 5 kHz

//Both filters tested "identical" but perhaps some other test could find a difference, dunno

@init

//generic utility functions

//should only call js_malloc within js @init section

JS_MALLOC_NO_CLEARMEM = 0;

JS_MALLOC_CLEARMEM = 1;

next_js_malloc = 0;

function js_malloc(a_size, a_clearmem)

local (l_result)

(

l_result = next_js_malloc;

(a_clearmem != 0) ?

memset(l_result, 0, a_size);

next_js_malloc += a_size;

l_result;

);

//First order filtertype named consts for FirstOrdTrapezoidFilter and FirstOrdBltDfFilter

FIRST_ORD_FILTTYPE_LOPASS = 0;

FIRST_ORD_FILTTYPE_HIPASS = 1;

FIRST_ORD_FILTTYPE_ALLPASS_ADV = 2; //+180 degrees phase shift at DC, descending to 0 degrees phase shift at nyquist

FIRST_ORD_FILTTYPE_ALLPASS_RET = 3; //0 degrees phase shift at DC, descending to -180 degrees phase shift at nyquist

//First order trapezoidal filter object

//Code adapted from Vadim Zavalishin's book "The Art of VA Filter Design"

//

//FiltType : Use one of the filtertype named consts to set the return value of FirstOrdTrapezoidFilter_DoSamp()

// : However, all values are simultaneously accessible after calling DoSamp() by reading properties--

// : TheFilter.lp, TheFilter.hp, TheFilter.ap_A, TheFilter.ap_R

//a_FiltFC : Filter center frequency in Hz

//a_SampRate: Samplerate of filter

function FirstOrdTrapezoidFilter_Init(a_FiltType, a_FiltFC, a_SampRate)

(

this.FT = a_FiltType;

this.SR = a_SampRate;

this.Nyquist = floor(this.SR * 0.5);

this.FC = min(a_FiltFC, this.Nyquist - 1);

this.s = 0.0;

this.lp = 0;

this.hp = 0;

this.ap_A = 0;

this.ap_R = 0;

//calculate coefficient

this.g = tan($pi * this.FC / this.SR);

this.g /= (1 + this.g);

);

//only call after filter was created via _Init

function FirstOrdTrapezoidFilter_SetFC(a_FiltFC)

(

this.FC = min(a_FiltFC, this.Nyquist - 1);

this.g = tan($pi * this.FC / this.SR);

this.g /= (1 + this.g);

);

//only call after filter was created via _Init

//Returns the filtered sample

function FirstOrdTrapezoidFilter_DoSamp(a_InSamp)

local (l_v, l_result)

(

//Vadim Zavalishin code

//v = (x-z1_state)*g/(1+g);

//y = v + z1_state;

//z1_state = y + v;

l_v = (a_InSamp - this.s) * this.g;

this.lp = l_v + this.s;

this.s = this.lp + l_v;

this.hp = a_InSamp - this.lp;

this.ap_A = this.hp - this.lp;

this.ap_R = this.lp - this.hp;

(this.FT == FIRST_ORD_FILTTYPE_LOPASS) ?

l_result = this.lp

:

(this.FT == FIRST_ORD_FILTTYPE_HIPASS) ?

l_result = this.hp

:

(this.FT == FIRST_ORD_FILTTYPE_ALLPASS_ADV) ?

l_result = this.ap_A

:

l_result = this.ap_R;

l_result;

);

//First order BLT Direct Form filter object

//Code adapted from Max Mikhailov's suggestions:

//

//The BLT LP expresses in direct form (i.e. numerator/denominator, transfer-function form) as:

//b = [1-k, 1-k]/2; a = [1, k];

//Thus HP is:

//b = [1+k, -1-k]/2; a = [1, k];

//And AP is:

//b = [-k, 1]; a = [1, k];

//

//Where k = cos(w)/(1 + sin(w));

//and w = 2*pi*Fc/Fs;

//

//In practice they are usually implemented in opposite fashion:

//first your get the AP and then LP = (IN + AP)/2; HP = (IN - AP)/2;

//FiltType : Use one of the filtertype named consts to set the return value of FirstOrdBltDfFilter_DoSamp()

// : However, all values are simultaneously accessible after calling DoSamp() by reading properties--

// : TheFilter.lp, TheFilter.hp, TheFilter.ap_A, TheFilter.ap_R

//a_FiltFC : Filter center frequency in Hz

//a_SampRate: Samplerate of filter

function FirstOrdBltDfFilter_Init(a_FiltType, a_FiltFC, a_SampRate)

local (l_w)

(

this.FT = a_FiltType;

this.SR = a_SampRate;

this.Nyquist = floor(this.SR * 0.5);

this.FC = min(a_FiltFC, this.Nyquist - 1);

this.FBFF = 0.0; //abbreviation for the [z-1] feedback+feedforward calculated value

this.lp = 0;

this.hp = 0;

this.ap_A = 0;

this.ap_R = 0;

//calculate coefficient

l_w = (2 * $pi * this.FC) / this.SR;

this.k = cos(l_w) / (1 + sin(l_w));

);

//only call after filter was created via _Init

function FirstOrdBltDfFilter_SetFC(a_FiltFC)

local (l_w)

(

this.FC = min(a_FiltFC, this.Nyquist - 1);

l_w = (2 * $pi * this.FC) / this.SR;

this.k = cos(l_w) / (1 + sin(l_w));

);

//only call after filter was created via _Init

//Returns the filtered sample

function FirstOrdBltDfFilter_DoSamp(a_InSamp)

local (l_result)

(

//From Max Mikhailov suggestions

//this.ap_R = (a_InSamp * -1.0 * this.k) + this.FBFF;

this.ap_R = this.FBFF - (a_InSamp * this.k);

this.FBFF = a_InSamp + (this.ap_R * this.k);

this.lp = 0.5 * (a_InSamp + this.ap_R);

this.ap_A = -1.0 * this.ap_R;

this.hp = 0.5 * (a_InSamp + this.ap_A);

(this.FT == FIRST_ORD_FILTTYPE_LOPASS) ?

l_result = this.lp

:

(this.FT == FIRST_ORD_FILTTYPE_HIPASS) ?

l_result = this.hp

:

(this.FT == FIRST_ORD_FILTTYPE_ALLPASS_ADV) ?

l_result = this.ap_A

:

l_result = this.ap_R;

l_result;

);

pdc_bot_ch = 0;

pdc_top_ch = 2;

//Instantiate two test filters

//Allpass test 100 Hz

//o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 100, srate);

//o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 100, srate);

//Lopass test 100 Hz

//o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_LOPASS, 100, srate);

//o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_LOPASS, 100, srate);

//Hipass test 100 Hz

//o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_HIPASS, 100, srate);

//o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_HIPASS, 100, srate);

//Allpass test 5 kHz

//o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 5000, srate);

//o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 5000, srate);

//Lopass test 5 kHz

//o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_LOPASS, 5000, srate);

//o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_LOPASS, 5000, srate);

//Hipass test 5 kHz

o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_HIPASS, 5000, srate);

o_1stOrdBltDfFilt.FirstOrdBltDfFilter_Init(FIRST_ORD_FILTTYPE_HIPASS, 5000, srate);

@slider

//no sliders declared

@block

//no block processing

@sample

//Apply test filters

//Use left channel input to feed both the trapezoid filter and the BLT DF filter

//Send trapezoid filter output to left channel and send BLT DF filter output to right channel

//So that the two transfer functions can be compared with a audio editor program or whatever

InSamp = spl0;

spl0 = o_1stOrdTrapezoidFilt.FirstOrdTrapezoidFilter_DoSamp(InSamp);

spl1 = o_1stOrdBltDfFilt.FirstOrdBltDfFilter_DoSamp(InSamp);

@gfx

//no graphics displayed