1
$\begingroup$

I moved this out of another question, where I'd put it to give more context, and answered it here for the next reader.

ELI5 how to implement a windowed sinc FIR filter in real time for an audio synthesizer. The synthesizer may adjust the filter's cutoff by an LFO, and there are a lot of filters in use, so this is both not stored and required to be fast.

I use a $48000Hz\times 288\times 12$ core clock frequency and a $40960Hz$ internal sample rate. The final output is upsampled via sinc interpolation to $48000Hz$ so as to allow effective noise shaping and dithering.

$\endgroup$

1 Answer 1

1
$\begingroup$

I didn't understand the math behind a sinc filter until I read some code that does it and some other basic material. Once you get it, it's easy. This approach works at the specified clock rates well enough for 128 channels of additive synthesis including separately-filtered noise for each channel to get an 83-tap filter with a 2,271Hz transition bandwidth (made wider by truncating the kernel), interpolating the output to 48,000Hz sample rate with shaped noise dithering, giving a fixed and guaranteed 1.502ms latency from key-on command reaching the SPI bus to sample output. The latency comes entirely from the number of taps used and the output upsampling.

I store the common Blackman and Hamming windows as two look-up tables, defined below, for the range $[0:M]$ where $M=82$ and $m=\dfrac{M}{2}$. I actually store $m$ as the first entry in the array and only store half the values, as it's mirrored across the $y$ axis. This consumes few LUTs for ROM.

$blackman[i] = 0.42 - \dfrac{cos\left(2i\dfrac{\pi}{M}\right)}{2} + 0.08\times cos\left(4i\dfrac{\pi}{M}\right)$

$hamming[i] = 0.54 - 0.46\times cos\left(2i\dfrac{\pi}{M}\right)$

That saves me having to calculate any of that at runtime.

Configuration of the filter sets $c=\dfrac{\text{Cutoff Frequency}}{\text{Sample Rate}}$, where the sample rate is 40,960Hz (I interpolate output to 48,000Hz before dithering, in the end).

Every time I generate a sample, I perform the following computation using a sinc look-up table. The sinc table has points at every $\dfrac{pi}{2}$ interval and in between for a domain of $(0:41\pi]$, so I don't need to interpolate between entries (other processes in the chip use linear interpolation between points in various look-up tables), so this is light-weight and easy to pipeline.

$\begin{align} ir\left[i\right] &=\begin{cases} sinc\left(2\pi c\times\left(i-m\right)\right) & \text{if }i\neq M, \\ c & \text{if }i=M \end{cases} \\ kernel\left[i\right] &= ir\left[i\right]\times window\left[i\right] \end{align}$

This produces a filter kernel. If I need a high-pass or band-pass, I have enough cycles (when pipelining and due to when precisely I'm able to start precomputing the filter in parallel with everything that happens before it's used) to use spectral inversion (flip the sign, add 1 to the midpoint) and to calculate a second kernel and add it to the first.

The sample generated by the synthesizer is multiplied by the gain, then multiplied by all 42 points, and added to a ring buffer in an output channel, symmetrically around the midpoint. 42 operations, pipelined, in parallel, using a total of 18 $18\times 18$ multipliers.

The kernel is then discarded and a new one computed for the next sample.

By pipelining this and producing the filters during additive synthesis, I'm able to filter 128 additive signals and 128 noise signals (to mix with the additive signals) within my time limits of 4,000 clock ticks per sample tick.

Hopefully the above is comprehensible for people with approximately no background. Took me weeks to figure out how this stuff works. (Hopefully it's correct….)

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.