FPGA music visualiser – part 2

In this post, I’ll cover the digital audio processing part of the project. I needed some way to process the incoming I2S samples, and filter them.

A brilliant tutorial series on Beyond Circuits gave some hints on how to code an I2S receiver. I2S works by sending a sample on the left channel while a channel select signal is held high. Then, a sample is sent on the right channel and the process repeats.

On each clock cycle the data would be shifted in, and once the channel select sample changes from the ADAU1761 (after 32 cycles, as the ADAU1761 outputs 32-bit samples), the receiver would send a rising edge which would signal that a new sample was available. I’ve attached the code below.

Designing the digital filters

I then moved on to the design of the digital audio filters. I created a new IP component for a biquad filter that I was going to implement. A biquad filter is a second order IIR (infinite impulse response) filter – the same as the sort of analogue filter you’d make with an op amp. I chose to use IIR filters over FIR filters as I’d need to work at low frequencies. A FIR filter needs a long filter length to have a low frequency cut-off.

A biquad filter has a transfer function which can be written in this format:

H(z) = \frac{a_0 + a_1z^{-1}+a_2z^{-1}}{1-b_1z^{-1}-b_2z^{-1}}

Note that a lot of literature swaps the coefficient names a and b around, and the coefficients are positive on the denominator. The filter can be implemented with the difference equation:

y_n = a_ox_n+a_1x_{n-1}+a_2x_{n-2}+b_1y_{n-1}+b_2y_{n-2}

Writing the coefficients as negative on the denominator makes this simpler as no subtraction is needed. To implement this with HDL, I effectively used fixed point numbers, as FPGAs of course can not easily deal with floating-point numbers.

The filter uses a state machine, going through and evaluating each term at a time. Each coefficient is stored as an integer multiplied by 2^{16}. Then, they are added up and the final result divided by 2^{16}. This avoids any need for decimal numbers, and gives reasonable precision.

I then wrote some Python code to generate coefficients for the filters. SciPy conveniently makes this quite easy. I chose Chebyshev filters with 0.5dB of ripple and a Q factor of 2 to get a sharp cut off and a reasonably flat pass band. I then wanted to test my filter. For this I used the Vivado simulator and a bit of trickery.

Testing the filters

First, I wrote a VHDL testbench that read in samples from a file, passed them to the filter and wrote out the filtered samples to another file. Then, I wrote a Python program which generated sine wave samples at frequencies between 1% of the sample frequency and half of the sample frequency (Nyquist frequency).

The Vivado simulator can be controlled from a command line interface. So, my Python program first started up an instance of the simulator. Then, it would write out samples of a sine wave and run the simulator. It would then read in the result, calculate the RMS value of the signal, reset the simulator and repeat. I could run the simulator 100 times in about 10 seconds, which made it great for debugging. I was able to generate a nice Bode plot for my filter.

Fig 1. Frequency response of the filter

You can see that this filter has unity gain at 6kHz, or 0.125 of the sampling frequency. However, I needed to design a filter with a centre frequency of 20Hz for the lowest frequency band. This is 0.000417 of the sampling frequency. Designing a filter at this frequency would result in some very large numerical errors. The solution was to downsample first. This was easily done by discarding every other sample, and then passing through a low pass filter to eliminate the high frequency artefacts caused by aliasing. For the 20Hz filter, I downsampled 32x to give a more reasonable 0.0133 of the sampling frequency.

Linear -> log conversion

I then needed to perform some kind of linear-log conversion to make the visualiser look a bit more pleasing (see my Nixie visualiser part 3 for an explanation). I started by creating a new IP component. My module first estimated the power of the signal by squaring the value of each sample and calculating the mean of this over a certain number of samples.

Then, I used a look up table to calculate the log of this average. The program compared the mean value to each value in the look up table each clock cycle until it found the two values that the mean lied between. My look up table ended up being 512 values long, giving 9 bit precision.

Final block diagram

I then had to put the whole thing together. For some reason, I decided to do this using a block diagram, which I definitely regret, as it took a long time to connect everything. I used sixth order band pass filters in the end, which meant three stages back to back.

Fig 2. Entire block diagram

After a lot of tweaking of filter coefficients and other things, the program works nicely. I’ve uploaded a short video to show it in action.

If I was to repeat the design of the project, I would have definitely just designed a pipeline for the filter instead of a state machine as it would have made the overall design a bit simpler. Also, I think some of the VHDL code could be a bit neater, but I can’t criticise myself too much as this was my first FPGA project.

Also, there’s not much point to this project as the computations could be performed by a computer in a simpler and easier way. Next time, I’ll try for a more complicated project that fully utilises the power of my Zynq board.

Audio quality is not great here


Leave a Reply

Your email address will not be published. Required fields are marked *