Continuity issue when applying an IIR filter on successive time-frames
Asked Answered
F

1

3

I would like to apply a FIR or IIR filter (example: lowpass filter) on successive blocks/time-frames of 1024 samples each.

Possible applications:

  • realtime audio processing, such as EQing. At a precise time, we only have the next 1024 samples in a buffer. The next samples to process are not available yet (realtime).

  • make a cutoff-time-varying filter by splitting the input signal in blocks, as suggested in this answer.

I tried this here:

import numpy as np
from scipy.io import wavfile
from scipy.signal import butter, lfilter, filtfilt, firwin

sr, x = wavfile.read('input.wav')
x = np.float32(x)
y = np.zeros_like(x)

N  = 1024  # buffer block size = 23ms for a 44.1 Khz audio file
f = 1000  # cutoff
pos = 0  # position

while True:
    b, a = butter(2, 2.0 * f / sr, btype='low')
    y[pos:pos+N] = filtfilt(b, a, x[pos:pos+N])
    pos += N
    f -= 1   # cutoff decreases of 1 hz every 23 ms, but the issue described here also present with constant cutoff!
    print f
    if pos+N > len(x):
        break

y /= max(y)  # normalize

wavfile.write('out_fir.wav', sr, y)

I tried:

  • both with a Butterworth filter or a FIR (replace the line before by b, a = firwin(1000, cutoff=f, fs=sr), 1.0)

  • both with lfilter and filtfilt (the latter has the advantage to apply the filter forward and backwards, and this solves phase issues),

but here is the problem:

At the boundaries of each time-frames' output, there is a continuity issue, that makes the audio signal heavily distorded.

How to solve this discontinuity problem? I thought about windowing+OverlapAdd method, but there surely must be an easier way.

enter image description here

Freakish answered 23/9, 2018 at 17:26 Comment(7)
Since you are using an IIR-filter, you will need to persist the state of your filter from one block to the next.Binucleate
Not the parameters, the state of the filter, the data that is in its memory. If you jump from one block to the next and use a completely uninitialized IIR-filter, of course you are going to get an artifact. The output of your processing has to be identical to if you hadn't processed it in blocks, except for the lag from buffering.Binucleate
Look at the z_f output and z_i input parameters of lfilt. You need to set z_i of the filter for the current block to z_f of the filter of the previous block. That way your filter state is persisted from one block to the next. This topic from dsp stackexchange might also be useful. dsp.stackexchange.com/questions/28725/… Also it would probably be a good idea for you to understand the basics of how a filter is built up internally so this all makes sense.Binucleate
@Binucleate Thanks! I posted an answer. It seems to work. Do you think it's ok even in the case where the cutoff changes at each iteration (thus the a, b too)?Freakish
The update to the cutoff frequency seems slow, so you might get away with this. You always have to take care that parameters don't jump instantaneously, as this will also cause audible artifacts.Binucleate
@sobek: If we want to do a "filter sweep" in 5 seconds, from 10k to 1khz (imagine someone is turning the cutoff knob quickly on a DJ mixer), it's mandatory to have the frequency decrease of a few hz during each 1024 samples-block, right? create a ramp function from parameter start value to parameter end value and try to make the change as smooth as possible, not instantly: how would you do a more continuous ramp than a few hz for each buffer/block? (Thanks again for all the tips, it really helped me!)Freakish
I think it's alright in this case.Binucleate
F
3

As mentioned by @sobek in a comment, it's of course needed to specify the initial conditions to allow continuity. This is done with the zi parameter of lfilter.

The problem is solved by changing the main loop by:

while True:
    b, a = butter(2, 2.0 * f / sr, btype='low')
    if pos == 0:
        zi = lfilter_zi(b, a)
    y[pos:pos+N], zi = lfilter(b, a, x[pos:pos+N], zi=zi)
    pos += N
    f -= 1 
    if pos+N > len(x):
        break

This seems to work even if the filter's cutoff (and thus the a and b) is modified at each iteration.

Freakish answered 23/9, 2018 at 19:10 Comment(2)
Just one more comment, you could save performance if you would only calculate and update the filter coefficients if they have actually changed.Binucleate
@Binucleate Maybe you would have an idea for the filtfilt case? #52503374Freakish

© 2022 - 2024 — McMap. All rights reserved.