yodel
yodel copied to clipboard
Numba compilation
Not expecting this to be merged, just if someone wants to speed up yodel using Numba, this seems to be working OK (Biquad example):
import math
class Biquad:
"""
A biquad filter is a 2-poles/2-zeros filter allowing to perform
various kind of filtering. Signal attenuation is at a rate of 12 dB
per octave.
*Reference:*
"Cookbook formulae for audio EQ biquad filter coefficients",
Robert Bristow-Johnson
(http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt)
"""
def __init__(self):
"""
Create an inactive biquad filter with a flat frequency response.
To make the filter active, use one of the provided methods:
:py:meth:`low_pass`, :py:meth:`high_pass`, :py:meth:`band_pass`,
:py:meth:`all_pass`, :py:meth:`notch`, :py:meth:`peak`,
:py:meth:`low_shelf`, :py:meth:`high_shelf` and :py:meth:`custom`.
"""
self.reset()
def reset(self):
"""
Make the filter inactive with a flat frequency response.
"""
self._a_coeffs0 = 0.0
self._a_coeffs1 = 0.0
self._a_coeffs2 = 0.0
self._b_coeffs0 = 1.0
self._b_coeffs1 = 0.0
self._b_coeffs2 = 0.0
self._x1 = 0.0
self._x2 = 0.0
self._y1 = 0.0
self._y2 = 0.0
def low_pass(self, samplerate, cutoff, resonance):
"""
Make a low-pass filter.
:param samplerate: sample-rate in Hz
:param cutoff: cut-off frequency in Hz
:param resonance: resonance or Q-factor
"""
self._compute_constants(samplerate, cutoff, resonance)
self._a_coeffs0 = (1.0 + self._alpha)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs0 = ((1.0 - self._cos_w0) / 2.0) / self._a_coeffs0
self._b_coeffs1 = (1.0 - self._cos_w0) / self._a_coeffs0
self._b_coeffs2 = ((1.0 - self._cos_w0) / 2.0) / self._a_coeffs0
def high_pass(self, samplerate, cutoff, resonance):
"""
Make a high-pass filter.
:param samplerate: sample-rate in Hz
:param cutoff: cut-off frequency in Hz
:param resonance: resonance or Q-factor
"""
self._compute_constants(samplerate, cutoff, resonance)
self._a_coeffs0 = (1.0 + self._alpha)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs0 = ((1.0 + self._cos_w0) / 2.0) / self._a_coeffs0
self._b_coeffs1 = -(1.0 + self._cos_w0) / self._a_coeffs0
self._b_coeffs2 = ((1.0 + self._cos_w0) / 2.0) / self._a_coeffs0
def band_pass(self, samplerate, center, resonance):
"""
Make a band-pass filter.
:param samplerate: sample-rate in Hz
:param center: center frequency in Hz
:param resonance: resonance or Q-factor
"""
self._compute_constants(samplerate, center, resonance)
self._a_coeffs0 = (1.0 + self._alpha)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs0 = (self._alpha) / self._a_coeffs0
self._b_coeffs1 = 0
self._b_coeffs2 = (- self._alpha) / self._a_coeffs0
def all_pass(self, samplerate, center, resonance):
"""
Make an all-pass filter.
:param samplerate: sample-rate in Hz
:param center: center frequency in Hz
:param resonance: resonance or Q-factor
"""
self._compute_constants(samplerate, center, resonance)
self._a_coeffs0 = (1.0 + self._alpha)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs0 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._b_coeffs2 = (1.0 + self._alpha) / self._a_coeffs0
def notch(self, samplerate, center, resonance):
"""
Make a notch filter.
:param samplerate: sample-rate in Hz
:param center: center frequency in Hz
:param resonance: resonance or Q-factor
"""
self._compute_constants(samplerate, center, resonance)
self._a_coeffs0 = (1.0 + self._alpha)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha) / self._a_coeffs0
self._b_coeffs0 = (1.0) / self._a_coeffs0
self._b_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._b_coeffs2 = (1.0) / self._a_coeffs0
def peak(self, samplerate, center, resonance, dbgain):
"""
Make a peak filter.
:param samplerate: sample-rate in Hz
:param center: center frequency in Hz
:param resonance: resonance or Q-factor
:param dbgain: gain in dB
"""
self._compute_constants(samplerate, center, resonance, dbgain)
self._a_coeffs0 = (1.0 + self._alpha / self._a)
self._a_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._a_coeffs2 = (1.0 - self._alpha / self._a) / self._a_coeffs0
self._b_coeffs0 = (1.0 + self._alpha * self._a) / self._a_coeffs0
self._b_coeffs1 = (-2.0 * self._cos_w0) / self._a_coeffs0
self._b_coeffs2 = (1.0 - self._alpha * self._a) / self._a_coeffs0
def low_shelf(self, samplerate, cutoff, resonance, dbgain):
"""
Make a low-shelf filter.
:param samplerate: sample-rate in Hz
:param cutoff: cut-off frequency in Hz
:param resonance: resonance or Q-factor
:param dbgain: gain in dB
"""
self._compute_constants(samplerate, cutoff, resonance, dbgain)
self._a_coeffs0 = ((self._a + 1) +
(self._a - 1) * self._cos_w0 + self._sqrtAlpha)
self._a_coeffs1 = ((-2.0 * ((self._a - 1) +
(self._a + 1) * self._cos_w0)) /
self._a_coeffs0)
self._a_coeffs2 = (((self._a + 1) +
(self._a - 1) * self._cos_w0 - self._sqrtAlpha) /
self._a_coeffs0)
self._b_coeffs0 = ((self._a *
((self._a + 1) -
(self._a - 1) * self._cos_w0 +
self._sqrtAlpha)) /
self._a_coeffs0)
self._b_coeffs1 = ((2.0 * self._a *
((self._a - 1) - (self._a + 1) * self._cos_w0)) /
self._a_coeffs0)
self._b_coeffs2 = ((self._a *
((self._a + 1) -
(self._a - 1) * self._cos_w0 -
self._sqrtAlpha)) /
self._a_coeffs0)
def high_shelf(self, samplerate, cutoff, resonance, dbgain):
"""
Make a high-shelf filter.
:param samplerate: sample-rate in Hz
:param cutoff: cut-off frequency in Hz
:param resonance: resonance or Q-factor
:param dbgain: gain in dB
"""
self._compute_constants(samplerate, cutoff, resonance, dbgain)
self._a_coeffs0 = ((self._a + 1) -
(self._a - 1) * self._cos_w0 + self._sqrtAlpha)
self._a_coeffs1 = ((2.0 *
((self._a - 1) - (self._a + 1) * self._cos_w0)) /
self._a_coeffs0)
self._a_coeffs2 = (((self._a + 1) -
(self._a - 1) * self._cos_w0 -
self._sqrtAlpha) /
self._a_coeffs0)
self._b_coeffs0 = ((self._a *
((self._a + 1) +
(self._a - 1) * self._cos_w0 +
self._sqrtAlpha)) /
self._a_coeffs0)
self._b_coeffs1 = ((-2.0 * self._a *
((self._a - 1) + (self._a + 1) * self._cos_w0)) /
self._a_coeffs0)
self._b_coeffs2 = ((self._a *
((self._a + 1) +
(self._a - 1) * self._cos_w0 -
self._sqrtAlpha)) /
self._a_coeffs0)
def custom(self, a0, a1, a2, b0, b1, b2):
"""
Make a custom filter.
:param a0: a[0] coefficient
:param a1: a[1] coefficient
:param a2: a[2] coefficient
:param b0: b[0] coefficient
:param b1: b[1] coefficient
:param b2: b[2] coefficient
"""
self._a_coeffs0 = a0
self._a_coeffs1 = a1 / a0
self._a_coeffs2 = a2 / a0
self._b_coeffs0 = b0 / a0
self._b_coeffs1 = b1 / a0
self._b_coeffs2 = b2 / a0
def process_sample(self, x):
"""
Filter a single sample and return the filtered sample.
:param x: input sample
:rtype: filtered sample
"""
curr = x
y = (self._b_coeffs0 * x +
self._b_coeffs1 * self._x1 +
self._b_coeffs2 * self._x2 -
self._a_coeffs1 * self._y1 -
self._a_coeffs2 * self._y2)
self._x2 = self._x1
self._x1 = curr
self._y2 = self._y1
self._y1 = y
return y
def process(self, x, y):
"""
Filter an input signal. Can be used for in-place filtering.
:param x: input buffer
:param y: output buffer
"""
num_samples = len(x)
for n in range(0, num_samples):
y[n] = self.process_sample(x[n])
def _compute_constants(self, fs, fc, q, dbgain=0):
"""
Pre-compute internal mathematical constants
"""
self._fc = fc
self._q = q
self._dbgain = dbgain
self._fs = fs
self._a = math.pow(10, (self._dbgain / 40.0))
self._w0 = 2.0 * math.pi * self._fc / self._fs
self._cos_w0 = math.cos(self._w0)
self._sin_w0 = math.sin(self._w0)
self._alpha = self._sin_w0 / (2.0 * self._q)
self._sqrtAlpha = 2.0 * math.sqrt(self._a) * self._alpha
import numba
NumbaBiquad = numba.experimental.jitclass([
(f, numba.float64) for f in [f"_{x}_coeffs{i}" for x in "ab" for i in range(3)] + """
_fc
_q
_dbgain
_fs
_a
_w0
_cos_w0
_sin_w0
_alpha
_sqrtAlpha
_x1
_x2
_y1
_y2""".strip().splitlines()
])(Biquad)
Hi @jonashaag,
I cannot believe some people are actually still using yodel!
Out of curiosity, may I ask in what context are you using it?
Last time I wrote a line of code for this project was exactly 6 years ago, judging from my last commit. It was a pet project at the time, an excuse to implement some text-book DSP algorithms, learn some more Python and how to distribute packages (and get a break from DSP C++/ASM code at work).
Unfortunately, I do not intend on working on yodel anymore: I'm leaving it as is, as an artifact of the past.
Feel free to make a fork! (My advice: start over, and use numpy to vectorize a lot of code!)
Cheers
Just wanted some simple way to do filtering with Python and yodel was the first thing I found that seemed reasonably simple and complete. I was surprised that it works with Python 3 with no modifications. Well after I had it integrated into the rest of the code I realized it’s very slow due to all of the computations being done in pure Python, and adding Numba was easier than changing to another library so here we are :-)
It’s part of a deep learning data augmentation pipeline
Just wanted some simple way to do filtering with Python and yodel was the first thing I found that seemed reasonably simple and complete
Yeah, I remember looking for the same thing back then, and was aiming at a very high level usage (compared to usual very math-involved libraries). But for sake of completeness, you might want to look at Scipy for this now: https://docs.scipy.org/doc/scipy/reference/signal.html
I was surprised that it works with Python 3 with no modifications
If I remember correctly, it was originally developed against Python 3.4 or something. Glad it is still working!
Well after I had it integrated into the rest of the code I realized it’s very slow due to all of the computations being done in pure Python, and adding Numba was easier than changing to another library so here we are :-)
Yeah, sorry for that (I was optimizing DSP algorithms in assembly all day back then, I needed to reconcile with simple and elegant code). Glad you found a workaround!
It’s part of a deep learning data augmentation pipeline
Oh I pretty nifty!
Yeah SciPy has a lot of stuff, but for example I wanted to quickly test some low/high shelf filtering and SciPy doesn't have it -- or at least I can't find it under that name; maybe you can find it under some other name if you are more knowledgeable about signal processing than I am, but I wasn't willing to invest time into more search/research.
I still think Yodel is incredibly valuable, even if you understandably don't plan to work on it any further, simply for its simplicity, ease of use, and educational value.