and simulation where "copying the analog" would result in the better solution.
That' missing the point a bit. It's not that one cares much about matching or copying the "analog" but that digital IIR filters have some very nice and useful properties.
For example in audio, IIR filter are very common place and I use Butterworths on a daily basis.
The most straight forward reason is simply filter length. To do anything meaningful at 40 Hz when your sample rate is 48 kHz, an FIR filter needs to be thousands of taps long. In some cases you can use multi rate filters to implement this very efficiently, but I'm not sure if this is universally applicable. Another possible option are FFT based filter (overlap add, etc.) but that presents a non-trivial trade off between latency and efficiency.
If you stick with linear phase filters, the latency quickly gets prohibitively large and you loose causality. Technically you can turn any linear phase FIR into a minimum phase FIR by simply inverting the zeros that are outside the unit circle, but that's numerically awkward procedure at high orders.
The human ear is also fairly insensitive to monaural phase distortions (e.g. minimum phase) as long as its causal. However, it's very sensitive to pre-ringing, so linear phase filters can be perceptually problematic.
A more "philosophical reason" is that the human auditory uses more or less logarithmic frequency axis and IIR filters are much more natural fit for this. For example, an IIR octave bandpass filter at 8kHz looks exactly the same as one at 125 Hz (other than the center frequency). The FIR filters would be dramatically different.
IIRs are great for cross overs: odd order Butterworth low pass and high pass filters sum to a flat frequency response (although it's typically an all pass). For even order, you can use Linkwitz Riley filters (which are based on Butterworths). You could use linear phase FIRs instead, but again you run into latency and causality problems.
It's also very easy to do things like real time adjustable high and low pass filters: It's very easy to design Butterworth filters on the fly or to simply table up the pole locations and interpolate. Filter order always stays constant and you are guaranteed to have the same slope & shape.
In other words: turn on any loudspeaker in your home and you will be hearing plenty of Butterworth & friends in action :-)
Example 1
This just a very simple highpass you would in any garden variety active speaker. 40 Hz sampled at 48 kHz. I designed both a Butterworth Highpass and a "matching" FIR using firls()
. Matching was done manually so they kind of look the same. I needed about 6000 taps on the FIR filter to get it in the ballpark
% IIR
[z,p,k] = butter(6,40*2/48000,'high');
%FIR
h=firls(6000,[0 10 65 fs/2]*2/fs,[0 0 1 1])';
Transfer function look like this.
To me it seems like the FIR is very much inferior in terms of latency, memory footprint. A 6th order BW has only 13 coefficients and 6-8 state variables. You can reduce this even further by leveraging that the zeros are all at $z = 1$.
Example 2:
This one is expands on the previous example by making the cutoff frequency of the high pass adjustable in real time. This creates a "sliding high pass" filter which is commonly found in sealed smart speakers. I tried to make requirements realistic, as you would find them in real products.
The Butterworth allows an extremely efficient implementation with no audible artifacts, no inherent latency, very low memory footprint and very low CPU consumption.
%% Script to implement a sliding high pass filter, that can be adjusted on the fly
% This type of "sliding highpass" is typically used in smart speakers with a
% "closed box" topology to control trasnducer excursion at high output
% volume
%
% Requirements:
% - sample rate: 48 kHz
% - frame size: 128 samples
% - cutoff frequency varies from 40 Hz to 100 Hz
% - highpass slope: 36 dB/octave. In other words level at half the cutoff
% frequency should be <= -36 dB
% - Level at cutoff frequency not less than -3dB
% - passband ripple: < .01 dB above 200 Hz (where spectral perception is
% more sensitive)
% - cutoff frequency is updated once per frame
% - no additional latency
% - smooth updating filter, no pops or clicks
%
% Implementation
% - 6th order butterworth
% - pole location and filter gain as a function of frequency are
% approximated as a polynomial fit. At these low frequencies, a linear
% fit (1rst order polynomial) works perfectly.
% - The filter coefficients are calcuated once per frame based on the
% current cutoff frequency
% - Filter is implemented as cascaded second order sections in Direct Form
% I. For Direct Form I, the state variable are simply the inputs and
% outputs, so updateing the filter deosn't create a discontinuity in the
% state variables.
%
% Test
% - input signal is a 50 Hz sine wave, low frequency sine waves are very
% sensitive to artifacts
% - cutoff frequency input is a mixture of step function, up and down sweeps
% and uniformly distributed random numbers between 40Hz and 100Hz
%
%% Table up the poles and the gain of the filters, perform polynomial fit
ord = 6;
fs = 48000;
fr = (40:.1:100)';
nfr = length(fr);
p0 = cell(nfr,1); % pole locations
k0 = zeros(nfr,1); % filter gains
for i = 1:nfr
[z,p,k] = butter(ord,2*fr(i)/fs,'high');
p0{i} = p(imag(p)>0);
k0(i) = k;
end
p0 = [p0{:}].'; % convert cell array to regular array
%% do a simple linear for poles and gains
% we fit the real part and the gain in (1-x) since they are very close to 1
ppPoles = cell(3,2);
for i = 1:3
ppPoles{i,1} = polyfit(fr,1-real(p0(:,i)),1); % 1 minus real part
ppPoles{i,2} = polyfit(fr,imag(p0(:,i)),1); % imginary part
end
% and the gains in 1-k
ppGain = polyfit(fr,1-k0,2);
%% test the polyfit, calculate the resulting transfer functions at a few
% frequencies
frTest = 40:10:100;
nfrTest = length(frTest);
nFFT = 16384*2;
d0 = zeros(nFFT,1); d0(1) = 1;
fy = zeros(nFFT,nfrTest);
sos = zeros(ord/2,6);
sos(:,1) = 1; sos(:,2) = -2; sos(:,3) = 1;
for i = 1:nfrTest
f = frTest(i);
for ip = 1:3
x = 1-polyval(ppPoles{ip,1},f); % real part
y = polyval(ppPoles{ip,2},f); % imginary part
sos(ip,4:6) = [1 -2*x x^2+y^2];
end
k = 1-polyval(ppGain,f);
fy(:,i) = fft(k*sosfilt(sos,d0));
end
% this checks out fine
%% Real time part, preperation
frameSize = 128; % frame size
frameRate = fs/frameSize; % number of frames per second
% let's do 10 seconds but an integer number of frames
nx = 10*fs;
numFrames = floor(nx/frameSize);
nx = numFrames*frameSize;
% build a test signal: 50 hz sine wave
xin = sqrt(.5)*sin(2*pi*(0:nx-1)'*50/fs); % 50 Hz sine wave at -3 dB
% build control frequency signal, one frequency per frame
frInput = 40*ones(numFrames,1);
% let's do a bit of rectangular switching plus some random stuff
t = (1:frameRate);
frInput(frameRate+t) = 100;
frInput(3*frameRate+t) = 70;
% up and down sweep
frInput(5*frameRate+t) = linspace(40,100,frameRate);
frInput(6*frameRate+t) = flip(linspace(40,100,frameRate));
% some random numbers for good measure
frInput(7*frameRate+1:end) = 40+60*rand(3*frameRate,1);
%
% in order to smooth the very aprupt frequency transitions in the test
% vector, we smooth the frequency input with a time constant to 30ms
timeConstant = 0.03;
frSmooth = exp(-1./(timeConstant*frameRate));
frCurrent =40;
%% Real time over all frames
% we implement this as driect form I so the states are always guaranteed to
% be continous
signalState = zeros(2,4); % filter state, we need total of 8
freqState = frInput(1); % cutoff frequency states
xout = 0*xin; % initialize output
t = 1:frameSize; % time vector
t0 = 0; % current time
yy = [xout xout xout];
for iFrame = 1:numFrames
% get frequency input and apply smoothing
frCurrent = frSmooth*frCurrent + (1-frSmooth)*frInput(iFrame);
% calculate filter gain, grab input and scale it
k = 1-polyval(ppGain,frCurrent);
y = k*xin(t0+t);
% over all biquads
x1 = signalState(1,1); % grab input state for the first biquad
x2 = signalState(2,1);
for iPole = 1:3
% calculate the filter coeefficents
pReal = 1-polyval(ppPoles{iPole,1},frCurrent); % real part
pImag = polyval(ppPoles{iPole,2},frCurrent); % imaginary part
a1 = -2*pReal; % filter coefficient "a1"
a2 = pReal*pReal+pImag*pImag; % biquad coefficient "a2"
% grab the output state
y1 = signalState(1,iPole+1);
y2 = signalState(2,iPole+1);
% inner loop. Here efficieny is the most important
for i = t
x0 = y(i); % get input sample
y0 = x0-2*x1+x2-a1*y1-a2*y2; % DF1 Butterworth stage
% update state
x2 = x1; x1 = x0;
y2 = y1; y1 = y0;
y(i) = y0; % write output
end % end sample loop
% save the output state of this stage
signalState(1,iPole) = x1;
signalState(2,iPole) = x2;
% grab the input state for the next stage
x1 = signalState(1,iPole+1);
x2 = signalState(2,iPole+1);
% now write the output state
signalState(1,iPole+1) = y1;
signalState(2,iPole+1) = y2;
yy(t0+t,iPole) = y;
end % end poles/stages
% write output
xout(t0+t) = y;
t0 = t0 + frameSize;
end % end frame
plot(xout);