Hacker News new | ask | show | jobs
by ReactiveJelly 904 days ago
For a couple years I've wanted to write a subtractive synth from scratch in software. But I _cannot_ find any source that explains how to write a low-pass filter, in code, without assuming tons of knowledge about calculus and these odd things like Z-transforms?

Like, I understand FFTs and DCTs. But the explanation for how to construct filters in software is pathetic.

I looked at the code for VCVRack but could not understand it. It's already optimized for SIMD, processing 4 signals at once, which didn't help readability. (I'm sure it's very fast though)

12 comments

Classic biquad approach with instructions for calculating coefficients of various filter shapes:

https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbo...

An analog modeling approach, but cross-reference the coefficient calculations!

https://cytomic.com/technical-papers/

This was one of the things that was most surprising to me doing software defined radio. It was "how does a finite impulse response filter work?" and "how to I make it work in a specific way?" The interesting question for synths would be how to give it an input that that changes its cut off frequency.

For me, the "aha" moment was when I connected the dots between "averaging" (which is a common way to filter noise out in an embedded computer) and "rolling average" is just summing the previous 'n' samples as sample * 1/n, and a picture of a fir filter that was (C code but it's pretty readable)

   out = 0;
   for (i = 0; i < n; i++) {
     out = out + sample[n-i]/n;
   }
   return(out);
That is a "FIR" filter where the coefficients are all 1/n. Now take that and do the FFT of n samples of n/1 (adding zeros to get the resolution you want). And that is the frequency response of your filter for frequencies between 0 (DC) and sample_rate/2.

For me at least that connected a lot of dots in what I was reading.

You probably want to defer the division until output, and not do the loop each time - instead, just subtract back N samples when adding the current sample.
To be fair, I typically use the MAC in an FPGA to implement this with the coefficient as a fixed point value. As a result the entire step is one cycle, depending on how many MACs are available I will parallelize the algorithim to support all available MAC blocks.

But in C I typically compute the coeficent outside the loop and use it in the multiply sample * (1/coefficient) vs sample/coefficient, even the STM32 microcontrollers have single cycle multiply available.

this depends on how many samples you have and what kind of precision you're working with for the accumulator. as floats get bigger they lose precision.
https://github.com/ThePJB/okiir/blob/a356b5b09917c0dbd856c9b...

Here's mine lol

Yeah I can't help but think there might be a niche for intermediate level discussion of these topics. I have a burning desire to wrap my head around z transform and other dsp black magic... (Like for example application of Hilbert transform to synthesis, anyone?) A lot of it comes back to understanding the complex plane. Like, I think Z transform relies on the fact that every *e^j2pi wraps around the unit circle again for one sample period.

I wrote a subtractive synth in java for my masters project. It was a fascinating domain to learn. For example, how to manage the independent voices, wavetable interpolation, envelopes. I was probably in over my head, and if I recall correctly, my supervisor was warning me about the complexity, since it was a software engineering programme.

The filter was the weirdest, least intuitive part. I ended up porting some open source code to java. This eventually worked, but was essentially impossible to debug without some sort of software oscilloscope.

That was about 18 years ago (yikes), and I still think about DSP from time to time.

The best intuition I have of low pass filters is to imagine it like an averaging, smoothing function which operates on the sample values as a sliding window. But that's not really true - it's not a straight rolling average, instead the sinc function (sin x / x) is used to scale the sample values with respect to time.

The way FIR filters work assumes a finite amount of samples (otherwise it would be technically impossible to know all past and future samples), so instead you pick a time period over which to calculate the output. Since there are always past and future samples included, this leads to a delayed output. Eg for a 100 sample filter window, you'd need to first have 100 samples in order to calculate the first output sample. This is intuitive, since a filter is sort of a smoothing, averaging function.

DSP is mainly mathematics, and so there's not much substitute for understanding someof the theory if you actually want to understand what's going on. It may look intimidating with all the calculus but it's not actually that bad if you just want to understand how the mathematical description of a filter relates to the implementation in terms of the multiplications and additions that are happent.
Check out GNU Radio. In contrary to what the name suggests, it's not just for radio, but can be used for doing complex DSP operations. It includes a nice filter design tool, too.

https://www.gnuradio.org/grcon/grcon18/presentations/The_Bri...

https://w7fu.com/audio-oscillator-project/

The best option to install it on Windows is radioconda: https://github.com/ryanvolz/radioconda

> But the explanation for how to construct filters in software is pathetic.

Pick your favorite FIR filter, set up a ring buffer, input samples go in, at each step you multiply the buffer elements by your filter coefficients, sum, and output the result.

The hard part is calculating the coefficients.
Not really, there's plenty of online tools to do it for you such as http://t-filter.engineerjs.com , and more offline ones: https://docs.scipy.org/doc/scipy/reference/signal.html#filte...
A intuition for creating a lowpass filter ... audio happens in the time domain ... Fourier transform outputs the frequency domain representation of the audio you feed it ... number of audio samples you feed into the FFT call determines the element count of the array returned ... now you have an array of your audio in the frequency domain ... for lowpass crank down to zero array elements above the first quarter of them ... this silences those frequencies ... to render the audio ( generate sound ) feed your edited frequency domain array into an IFFT ( inverse Fourier transform )
This does work, but the sound of it isn’t super pleasing and it limits your filter design options. Checking out time domain IIR filters and methods for generating coefficients gets you flexibility and efficiency with no latency. I posted some links above but the RBJ Cookbook is a good place to start.
Z-transform is very similar to Fourier or Laplace transform, but in discrete time. These things are not that hard, if you get FFT already then you can get there if you want.
https://fiiir.com/ will help you design a filter. Select "Python script" to get some comments on how to apply that filter on your data (look for "convolve").

For more understanding, I do recommend reading up on DSP and Signals concepts, they're pretty critical to know if you're designing filters.

Will Pirkle's books might be close to what you're looking for. He covers Z transforms (which you do ultimately need to understand to code up filters) but also provides implementations you can use as a starting point/to get a better idea of what to do concretely in software.
Was going to recommend them too!
Have a look at the filter code in Csound, for example:

https://github.com/csound/csound/blob/master/Opcodes/butter....