A few months ago, I’ve posted a note on an overdrive. The main issue of this kind of non-linear filter is aliasing, a process that adds digital acoustic content. The best way to solve the issue is to oversample the input before processing the signal.
Symptom of non-linear processing
Let’s try to check how the overdrive impacts the spectral content of the original signal. Let’s take the overdrive code of my last note. For a pure sinusoid, an analog filter adds frequencies proportional to the sinusoid’s frequency. As the next picture shows it in the numerical world, the spectral content aliases when frequency increases. It is thus mandatory to oversample the original signal, apply the non-linear process, filter it to remove the high frequencies and then undersample it.
Let’s start with the oversampling.
When a signal is digitized at a sampling frequency of 48kHz (for instance), its content from -24khZ to 24kHz is reproduced from -infinity to infinity. When oversampling it, you can observe the content that will be reproduced.
The goal of the oversampling filter is to remove the new spectrum from 24kHz to 48kHz. One solution may be to pad the original signal with zeros, but as can be seen in the next image, it doesn’t change a thing.
So I’ve decided to use the optimal filter described in http://www.student.oulu.fi/~oniemita/dsp/deip.pdf for an oversample of 2. As can be seen, it’s a good compromise in term of performance.
The code in Python is the following one:
def oversample2_6point_5_order(signal): signal_ex = np.hstack((np.zeros((signal.shape, 2)), signal, np.zeros((signal.shape, 3))))[:,:,None] even1 = signal_ex[:,2:-3] + signal_ex[:,3:-2] even2 = signal_ex[:,1:-4] + signal_ex[:,4:-1] even3 = signal_ex[:,0:-5] + signal_ex[:,5:] odd1 = signal_ex[:,3:-2] - signal_ex[:,2:-3] odd2 = signal_ex[:,4:-1] - signal_ex[:,1:-4] odd3 = signal_ex[:,5:] - signal_ex[:,0:-5] c0 = even1 * 0.40513396007145713 + even2 * 0.09251794438424393 + even3 * 0.00234806603570670 c1 = odd1 * 0.28342806338906690 + odd2 * 0.21703277024054901 + odd3 * 0.01309294748731515 c2 = -even1 * 0.191337682540351941 + even2 * 0.16187844487943592 + even3 * 0.02946017143111912 c3 = -odd1 * 0.16471626390554542 - odd2 * 0.00154547203542499 + odd3 * 0.03399271444851909 c4 = even1 * 0.03845798729588149 - even2 * 0.05712936104242644 + even3 * 0.01866750929921070 c5 = odd1 * 0.04317950185225609 - odd2 * 0.01802814255926417 + odd3 * 0.00152170021558204 z = np.array((-1./2, 0), dtype = np.float32)[None,None,:] return (((((c5 * z + c4) * z + c3) * z + c2) * z + c1) * z + c0).reshape(2, -1)
Let’s overdrive this oversampled signal, which gives the following solution (I came back to a 48kHz-sampled signal).
If this signal is directly undersampled, an aliasing occurs (lower than before, but it is still important):
It is thus mandatory to filter the signal. Let’s try a 8th order Bessel:
Now, the signal can be undersampled:
Let’s face it, it is far from perfect. This is due to several facts:
- The original signal should have a higher sampling frequency.
- The oversample factor should be higher.
- Of course, I’ve used an extreme overdrive.
Just to give an example, here is the same signal with a sampling frequency of 88.2kHz (the most efficient/accessible sampling frequency for a CD/44.1kHz production IMHO).
Some digital filters are not linear, and they can’t be applied as is in digital audio processing. The same even happens for a linear filter like a low-pass with a high cut frequency, near the Nyquist frequency. Oversampling the signal will help designing a correct filter that will mimic an analog filter.
Now, I still have to implement this process in C++, because in Python the Newton optimization takes too much time.
The code of this experiment is available on github.