yodel icon indicating copy to clipboard operation
yodel copied to clipboard

Numba compilation

Open jonashaag opened this issue 5 years ago • 6 comments

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)

jonashaag avatar Oct 22 '20 16:10 jonashaag

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

rclement avatar Oct 25 '20 16:10 rclement

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 :-)

jonashaag avatar Oct 25 '20 17:10 jonashaag

It’s part of a deep learning data augmentation pipeline

jonashaag avatar Oct 25 '20 17:10 jonashaag

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!

rclement avatar Oct 25 '20 17:10 rclement

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.

jonashaag avatar Oct 25 '20 20:10 jonashaag

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.

jonashaag avatar Oct 25 '20 20:10 jonashaag