Spectrum analyzer using Arduino Mega and 4x MAX7219 8x8 LED display modules

Hi guys!

I’m making a little spectrum analyzer using an Arduino Mega and 4x MAX7219 8x8 LED display modules. My plan is to assign a LED bar for each frequency band (30 60 120 240 480 1000 2000 8000 16000 Hz) just as commercial spectrum analyzers do.

My first attempt was replicating this project: 32-Band Audio Spectrum Visualizer Analyzer - Arduino Project Hub which kinda works, but sadly the code is very hard to modify / maintain.

After many hours of searching I found this another code, which is great to get the amplitude of a specific frequency.
https://www.norwegiancreations.com/2017/08/what-is-fft-and-how-can-you-implement-it-on-an-arduino/

I tried to adapt it to work with my LED matrix display, and at first glance looks like each bar corresponds to the amplitude. However the latency is extremely high, making the visualizer useless.

Please, could you help me to find out if I’m going on the right direction?

Thank you very much

#include "LedControl.h"
#include "arduinoFFT.h"

LedControl lc = LedControl(12, 11, 10, 4);
arduinoFFT FFT = arduinoFFT();

//Original code stolen from https://www.norwegiancreations.com/2017/08/what-is-fft-and-how-can-you-implement-it-on-an-arduino/

//Original samples was 128

#define SAMPLES 256     //Must be a power of 2
int SAMPLING_FREQUENCY; //Original code warns agains using more than 10000 due to ADC, this is ignored


unsigned long microseconds;
double vReal[SAMPLES];
double vImag[SAMPLES];

//Each LED bar would correspond to a band
//30 60 120 240 480 1000 2000 4000 8000 16000

int frecuenciasDeseadas[] = { 60, 120, 240, 480, 960, 2000, 4000, 8000, 16000, 32000 };


int arrayPosEquivX[32];
int ultimoValorCol[33];


void setup()
{
    int devices = lc.getDeviceCount();

    for (int address = 0; address < devices; address++)
    {
        lc.shutdown(address, false);
        lc.setIntensity(address, 3);
        lc.clearDisplay(address);
    }

    //Since the LED matrix is inverted (ex: 0 corresponds to the 32th led, 
    //and 32 corresponds to the first led, I made this little look up table 
    //to drive it on a more intuitive way

    int xEquiv = 0;
    int index = 1;

    for (int i = 32; i >= 1; i--)
    {
        arrayPosEquivX[i] = xEquiv;

        if (xEquiv < 7)
        {
            xEquiv++;
        }
        else
        {
            xEquiv = 0;
        }
        index++;
    }
}

void loop()
{
  
  //Make the analysis for each band
  
    for (int i = 0; i < 10; i++)
    {
        SAMPLING_FREQUENCY = frecuenciasDeseadas[i];
    
        /*SAMPLING*/
        for (int i = 0; i < SAMPLES; i++)
        {
            vReal[i] = analogRead(0);
            vImag[i] = 0;         
        }

        /*FFT*/
        FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
        FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
        FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);

        int constrainedInput = constrain(vReal[127], 0, 80);
        int mappedImput = map(constrainedInput, 0, 60, 0, 8);

        animBarra(i, mappedImput);
    }
}


//This section of the code makes the animation for the bar movement

void animBarra(int col, int nuevoValDeseado)
{
    int valorActualCol = ultimoValorCol[col];

    //if the bar needs to go up

    if (nuevoValDeseado > valorActualCol)
    {
        for (int i = valorActualCol; i <= nuevoValDeseado; i++)
        {
            accionaLed(col, i, true);         
        }
        ultimoValorCol[col] = nuevoValDeseado;
        return;
    }

    //If the bar needs to go down

    if (nuevoValDeseado < valorActualCol)
    {
        for (int i = valorActualCol; i >= nuevoValDeseado; i--)
        {
            accionaLed(col, i, false);         
        }
        ultimoValorCol[col] = nuevoValDeseado;
        return;
    }
}

//Turns on or off each LED on the matrix

void accionaLed(int x, int y, bool statusDeseado)
{
    int cuadrante;

    if (x >= 0 && x <= 8)
    {
        cuadrante = 0;
    }

    if (x > 8 && x <= 16)
    {
        cuadrante = 1;
    }

    if (x > 16 && x <= 24)
    {
        cuadrante = 2;
    }

    if (x > 24)
    {
        cuadrante = 3;
    }

    lc.setLed(cuadrante, y - 1, arrayPosEquivX[x], statusDeseado);
}

LedControl is generally pretty slow updating the display. You could try using the MD_MAX72xx library instead and make sure you use the hardware SPI pins on the MEGA. You should find a huge increase in performance.

The delay could be in the calculations, of course, so you will need to determine where the latency is happening.

The ArduinoFFT library is also very slow. The OpenMusicLabs FFT library is highly optimized for AVR based Arduinos and is many times faster.

Some people get a pretty good spectrum analyzer effect. From what I understand, the FFT reads the audio and then pauses to analyze, and you probably have to pause the FFT while you update the display. But if you check YouTube some people get nice display. (You're not making an accurate spectrum analyzer measurement instrument... You're making a "fun display".)

Have you considered an MSGEQ7? It's only 7 bands so you'd have to re-think your design/display, but with the frequency filtering done in hardware your software can focus on the display. (I've never tried it.)

There are rumors of fake & reject MSGEQ7's floating around (and some people have trouble with this chip) so if you want to try one (or two) make sure to buy one from a reputable supplier. SparkFun sells a board with two of them and stereo pass-through audio connectors.

jremington:
The ArduinoFFT library is also very slow. The OpenMusicLabs FFT library is highly optimized for AVR based Arduinos and is many times faster.

Of course all libraries are limited by the speed of the hardware - run it on a Teensy 4.x for instance
and it won't matter which library! - the chip is uses can execute upto 1200MIPS, and has built in
floating point. For more DSP grunt it and the other Teensy's are interesting Arduino-compatible options.

There are multiple issues with your code and an apparent fundamental misunderstanding of the FFT.

It looks like you're doing data collection and an FFT for each display bar iteration, taking the 127th FFT bin for each of the iterations, which will perform a great deal of meaningless computation before arriving at the wrong answer. For example the code sets various incorrect values to the parameter "SAMPLING_RATE" and doesn't actually use it anywhere, both of which are problems.

Fundamentally what you want to do is

  • collect a sample of the signal
  • perform the FFT (window, compute, complexToMagnitude)
  • map the results to each display bar
  • update display

For step 3 you need to understand the FFT result. The array will contain the signal magnitude at frequency n/Fs/256 where n is the index (0 to 127) of the array bin and Fs is the sampling frequency which will be about 9600 Hz using analogRead() in a for loop. Thus the 0th bin is 0 x 9600 Hz/256 = 0 Hz or DC, the 1st pin is 0 x 9600 / 256 = 37.5 Hz, and so forth.

So one mapping might be bin 1 to bar 1 (37.5 Hz), bin 2 to bar 2 (75 Hz), bin 3&4 to bar 3 (150 Hz), bin 5 to 8 to bar 4 (300 Hz), etc. To combine multiple bins take the root sum of squares, e.g. bar 3 = sqrt(bin(3)^2 + bin(4)^2).

I'd suggest converting bins to power, not amplitude, (ie square them all). Then you can add bins
linearly to combine them as this represents signal power directly.

If you want to dig a lot deeper into FFT and spectral analysis, this is an excellent review paper: https://holometer.fnal.gov/GH_FFT.pdf

MarkT:
I'd suggest converting bins to power, not amplitude, (ie square them all). Then you can add bins
linearly to combine them as this represents signal power directly.

If you want to dig a lot deeper into FFT and spectral analysis, this is an excellent review paper: https://holometer.fnal.gov/GH_FFT.pdf

In addition, spectrum analyzers almost universally display a decibel scale, so one would take the logarithm of this sum of squared signal power for each bar and scale it such that "loud" peaks the bar display.

Have a look here...