I have been reading The Scientist and Engineer's Guide to DSP to learn how to create filter kernels with an arbitrary frequency response (I design the magnitude response by hand). The method proposed in the link is:
- Create an array that describes how you want to multiply the magnitudes of each bin in the frequency domain to achieve the desired response
- Create a corresponding phase array of all zeros
- Convert mag and phase to rectangular coordinates
- Take the inverse DFT using the rectangular arrays to get the impulse response
- Roll and window the impulse response to get the filter kernel
This method works well, except that I have recently realized it produces a filter that is non-causal. This is problematic for the audio processing I am doing on drums because it results in an audible ghosting of the drum hit before it actually hits:
I started digging into another source on this at DSP Related and realized The Scientist and Engineer's Guide to DSP probably has me creating a zero-phase filter which is a special case of linear-phase filter that apparently cannot be causal.
Thanks to an answer to a related question, I have figured out how to create a minimum phase version of my filter that is causal, however, I am still curious if there is a simpler way to create a linear-phase version that is not zero-phase so that it can be causal. Is it possible, within the inverse DFT workflow outlined above, to simply set the phase array and processes the resulting kernel in a particular way such that the result is a causal, linear-phase filter with the magnitude response I want? My suspicion is "yes", but I am struggling to find a source that outlines exactly how I would achieve this.
Here's what I've tried:
Following the original steps I outlined, I do the following:
shift = filter_size // 2
phase = np.zeros_like(desired_mag)
kernel_Z = polar_to_rect(desired_mag, phase)
kernel = np.fft.irfft(kernel_Z)
kernel = np.roll(kernel, shift)[:filter_size] * np.hanning(filter_size)
This gets me the following kernel:
This has the magnitude response I want, but as you can see from the top plot, it is not causal. This is the kernel that causes the ghosting.
Next I tried adjusting the phase:
shift = filter_size // 2
phase = 2 * np.pi * shift * np.linspace(0, 0.5, len(desired_mag))
kernel_Z = polar_to_rect(desired_mag, phase)
kernel = np.fft.irfft(kernel_Z)
kernel = np.roll(kernel, shift)[:filter_size]
This gets me the following kernel:
This looks causal (at least if I flip it), but obviously the magnitude response is wrong, and I'm also not sure how I ought to window this.
What should I do here to get the kernel I'm after?