Issue with FHT spectrum analyzer sound heavily biased towards low frequency

Hi! I hope I've got the right section, as this is most likely a programming issue, but it's also an audio issue for sure.

Lil' bit of background: I'm definitely a light intermediate skill level. I understand concepts pretty well, just don't have a ton of programming experience.

I'm making a neopixel spectrum analyzer based on this lovely project, and am struggling with a strange issue. All the audio that the analyzer displays is crammed over on the left side. Please observe this video demonstrating the issue. Apologies for no audio, but I don't have a sound splitter handy or virtual audio cable installed.

As you can see, it takes an excruciatingly(seriously, watch at 2x speed at least) long time to sweep from left to right. Realistically, that sweep should be more or less linear. I really don't know how the FHT library works at all, and it would be quite a bit of work to puzzle it out step by step and diagnose someone else's code.

I've got the hardware as shown in this project, but on a protoboard, and with 10k potentiometers replacing all 4.7k resistors (R1, R2, R3). Manipulating these potentiometers shows minimal change in behavior, and does not affect the main issue here.

Here is the code I'm using. It uses this library(V4). I have rewritten the GetLedFromMatrix(x,y) function, and disabled some of the character display and button watching code (both cause the arduino to freeze (I think) and I haven't gotten around to diagnosing them yet).
!!CHARACTER LIMIT! CODE TO FOLLOW IN SECOND POST!!
Please, if anyone can help it would be greatly appreciated. I'm happy to provide any information that might help.

P.S. I also have those two neopixels in the bottom left that stay on constantly. Is that just ground noise perhaps? audio ground is also connected to power input ground. I don't mind them that much and am happy to ignore them until the spectrum analyzer is functioning properly.

Thanks,
Trek.

#define LIN_OUT 1                //FHT linear output magnitude
#define FHT_N 128                //set SAMPLES for FHT, Must be a power of 2
#include <FHT.h>


#define xres 31                 //Total number of columns in the display, must be <= SAMPLES/2
#define yres 14                  //Total number of rows in the display
#define PIN 6                //out pint to control Leds
#define NUM_LEDS (xres * yres)  //total leds in Matrix
#include <Adafruit_NeoPixel.h>


#define colorPIN 5        //pin to change ledcolor
#define brightnessPIN 10  //pin to change brightness


byte displaycolor = 0;    //default color value
byte brightness = 1;      //default brightness level


#include <EEPROM.h>
#define CONFIG_START 32         //Memory start location
#define CONFIG_VERSION "VER01"  //Config version configuration


typedef struct {
  char version[6];
  byte displaycolor;
  byte brightness;
} configuration_type;


configuration_type CONFIGURATION = {
  CONFIG_VERSION,
  displaycolor,
  brightness
};


byte yvalue;
int peaks[xres];
byte state = HIGH;                    // the current reading from the input pin
byte previousState = LOW;             // the previous reading from the input pin
unsigned long lastDebounceTime = 0;   // the last time the output pin was toggled
unsigned long debounceDelay = 100;    // the debounce time; increase if the output flickers


byte data_avgs[xres]; //Array for samplig


// Parameter 1 = number of leds in matrix
// Parameter 2 = pin number
// Parameter 3 = pixel type flags, add together as needed:
//   NEO_KHZ800  800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
//   NEO_KHZ400  400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
//   NEO_GRB     Pixels are wired for GRB bitstream (most NeoPixel products)
//   NEO_RGB     Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUM_LEDS, PIN, NEO_GRB + NEO_KHZ800);


// EQ filter
byte eq[32] = {
  60, 65, 70, 75, 80, 85, 90, 95,
  100, 100, 100, 100, 100, 100, 100, 100,
  100, 100, 100, 100, 100, 100, 100, 100,
  115, 125, 140, 160, 185, 200, 200, 200
};


bool EQ_ON = true; // set to false to disable eq


//Define 5 set of colors for leds, 0 for single custom color
byte colors[][8] = {
  {170, 160, 150, 140, 130, 120, 1, 1},
  {1, 5, 10, 15, 20, 25, 90, 90},
  {90, 85, 80, 75, 70, 65, 1, 1},
  {90, 90, 90, 30, 30, 30, 1, 1},
  {170, 160, 150, 140, 130, 120, 110, 0}
};
void setup() {
  pixel.begin();           //initialize Led Matrix

  //Begin FFT operations
  ADCSRA = 0b11100101;    // set ADC to free running mode and set pre-scaler to 32 (0xe5)
  ADMUX =  0b00000000;    // use pin A0 and external voltage reference

  // Read config data from EEPROM
//  if (loadConfig()) {
//    displaycolor = CONFIGURATION.displaycolor;
//    brightness = CONFIGURATION.brightness;
//  }

  //Set brightness loaded from EEPROM
  pixel.setBrightness(brightness * 24 + 8);

  //Show current config on start
  //change true to false if you don't want this
  //showSettings(3, false);

}


void loop() {
  while (1) {            // reduces jitter
    Sampling();          // FHT Library use only one data array
    RearrangeFHT();      // re-arrange FHT result to match with no. of display columns
    SendToDisplay();     // send to display according measured value
    
    delay(10);           // delay to reduce flickering (FHT is too fast :D)
  }
}


void Sampling() {
  for (int i = 0; i < FHT_N; i++) {
    while (!(ADCSRA & 0x10));   // wait for ADC to complete current conversion ie ADIF bit set
    ADCSRA = 0b11110101 ;       // clear ADIF bit so that ADC can do next operation (0xf5)
    //ADLAR bit is 0, so the 10 bits of ADC Data registers are right aligned
    byte m = ADCL;              // fetch adc data
    byte j = ADCH;
    int value = (j << 8) | m;   // form into an int
    value -= 0x0200;            // form into a signed int
    value <<= 6;                // form into a 16b signed int
    fht_input[i] = value / 8;   // copy to fht input array after compressing
  }
  // ++ begin FHT data process -+-+--+-+--+-+--+-+--+-+--+-+--+-+-
  fht_window();    // window the data for better frequency response
  fht_reorder();   // reorder the data before doing the fht
  fht_run();       // process the data in the fht
  fht_mag_lin();   // take the output of the fht
}


void RearrangeFHT() {
  // FHT return real value unsing only one array
  // after fht_mag_lin() calling the samples value are in
  // the first FHT_N/2 position of the array fht_lin_out[]
    
  int step = (FHT_N / 2) / xres;
  int c = 0;
  for (int i = 0; i < (FHT_N / 2); i += step) {
    data_avgs[c] = 0;
    for (int k = 0 ; k < step ; k++) {
      data_avgs[c] = data_avgs[c] + fht_lin_out[i + k];  // linear output magnitude
    }
    data_avgs[c] = data_avgs[c] / step ; // save avgs value
    c++;
  }
}


void SendToDisplay() {
  for (int i = 0; i < xres; i++) {
    if (EQ_ON)
      data_avgs[i] = data_avgs[i] * (float)(eq[i]) / 100; // apply eq filter
    data_avgs[i] = constrain(data_avgs[i], 0, 80);        // set max & min values for buckets to 0-80
    data_avgs[i] = map(data_avgs[i], 0, 80, 0, yres);     // remap averaged values to yres 0-8
    yvalue = data_avgs[i];
    peaks[i] = peaks[i] - 1;                              // decay by one light
    if (yvalue > peaks[i]) peaks[i] = yvalue;             // save peak if > previuos peak
    yvalue = peaks[i];                                    // pick peak to display
    setColumn(i, yvalue);                                 // draw columns
  }
  pixel.show();                                           // show column
}


// Light up leds of x column according to y value
void setColumn(int x, int y) {
  int led, i;


  for (i = 0; i < yres; i++) {
    led = GetLedFromMatrix(x, i); //retrieve current led by x,y coordinates
    if (peaks[x] > i) {


      switch (displaycolor) {
        case 4:
          if (colors[displaycolor][i] == 0) {
            // show custom color with zero value in array
            pixel.setPixelColor(led, 255, 255, 255); //withe
          }
          else {
            // standard color defined in colors array
            pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
          }
          break;


        case 5:
          //change color by column
          pixel.setPixelColor(led, Wheel(x * 16));
          break;


        case 6:
          //change color by row
          pixel.setPixelColor(led, Wheel(i * y * 3));
          break;


        case 7:
          //change color by... country :D


          //Italy flagh
          //if (x < 11) pixel.setPixelColor(led, 0, 255, 0);
          //if (x > 10 && x < 21) pixel.setPixelColor(led, 255, 255, 255);
          //if (x > 20) pixel.setPixelColor(led, 255, 0, 0);


          //stars and stripes
          if (i < yres - 2) {
            if (x & 0x01) {
              pixel.setPixelColor(led, 0, 0, 255);
            }
            else {
              pixel.setPixelColor(led, 255, 0, 0);
            }
          }
          else {
            pixel.setPixelColor(led, 255, 255, 255);
          }


          break;


        default:
          //display colors defined in color array
          pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
      }   //END SWITCH
    }
    else {
      //Light off leds
      pixel.setPixelColor(led, pixel.Color(0, 0, 0));
    }
  }
}


//================================================================
// Calculate a led number by x,y coordinates
// valid for WS2812B with serpentine layout placed in horizzontal
// and zero led at bottom right (DIN connector on the right side)
// input value: x= 0 to xres-1 , y= 0 to yres-1
// return a led number from 0 to NUM_LED
//================================================================
int GetLedFromMatrix(int x, int y) {
  int led;
  if (y & 0x01) {
    //odd columns increase backwards
    led = xres*(y+1)-x-1;
  }
  else {
    //even columns increase normally
    led = x+xres*(y);
  }
  return constrain(led, 0, NUM_LEDS);
}
//================================================================

// Utility from Adafruit Neopixel demo sketch
// Input a value 0 to 255 to get a color value.
// The colours are a transition R - G - B - back to R.
unsigned long Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if (WheelPos < 85) {
    return pixel.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if (WheelPos < 170) {
    WheelPos -= 85;
    return pixel.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return pixel.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

//by Janux®, Last version on 28/06/2020.

I know a lot about audio but I've never used FFT so I didn't try studying your code. (And your YouTube link isn't working for me),

With real world sounds it's normal to have more energy in the low & mid frequencies. (Woofers are bigger than tweeters, etc.) But, you shouldn't see that with a sweep tone.

And, a regular spectrum analyzer is logarithmic, which is how our hearing works. An octave is a doubling of frequencies, so there's an octave from 20-40Hz and there's an octave from 2000-4000Hz. But FFT is linear so if you have a bin from 20-40Hz you'll have a bin from 2000Hz to 2020Hz. So those higher-frequency bins have to be combined to get a "normal" spectrum analyzer display. (Maybe your software does that already.)

DVDdoug:
I know a lot about audio but I've never used FFT so I didn't try studying your code. (And your YouTube link isn't working for me),

With real world sounds it's normal to have more energy in the low & mid frequencies. (Woofers are bigger than tweeters, etc.) But, you shouldn't see that with a sweep tone.

And, a regular spectrum analyzer is logarithmic, which is how our hearing works. An octave is a doubling of frequencies, so there's an octave from 20-40Hz and there's an octave from 2000-4000Hz. But FFT is linear so if you have a bin from 20-40Hz you'll have a bin from 2000Hz to 2020Hz. So those higher-frequency bins have to be combined to get a "normal" spectrum analyzer display. (Maybe your software does that already.)

Thank you very much for the information, and for letting me know my video link was broken. I’ve fixed it in the first post, and here it is again: https://youtu.be/QwI_ao9mPkE
I understand that most sound is in the low range, but this is a bit ridiculous. If I put music into this, it’s very much clustered over on the left, with almost nothing in the right, even if I feed it white or pink noise.
This is FHT, not FFT, I’m not sure if that’s going to make a difference. If one looks at the example image for the project I’m adapting, then it shows a fairly normal looking spectrum analyzer, certainly not the behavior I’m seeing.
I’m convinced something is cattywampus in the code and is making everything cluster to one side. I’m just unsure where to start looking as I’ve never used this library before.

For audio you want your analyzer display to be logarithm frequency scale, but you
use a linear frequency scale. You have to do that mapping when you drive the display,
each bar in the display should be mapped to a range of frequencies such that the frequencies
increase exponentially as you step to the next bar.

MarkT:
For audio you want your analyzer display to be logarithm frequency scale, but you
use a linear frequency scale. You have to do that mapping when you drive the display,
each bar in the display should be mapped to a range of frequencies such that the frequencies
increase exponentially as you step to the next bar.

Thanks! That's the issue I was having with it all clustered to one side. Now I just need to figure out why the first column has a constant half-full bar,, and then add a filter to reduce the noise. Once I knew where to look it was easy to find what to change.

A constant in the first column (the first FHT bin) is almost certainly an uncompensated DC offset. In the sampling routine rather than "value -= 0x0200; // form into a signed int", you might try subtracting a running average of the ADC value or collecting the full sample buffer, calculating an average and subtracting that from each value.

Hm, quote feature doesn’t seem to work on my mobile browser...

Thanks for the info. I’ll be honest, I don’t understand what that line actually does. I know 0x0200 is 512 in hex but I’m not sure why wrote it as 0x0200 instead of 512. But what you’re saying is instead collect an average of the ADC every run through the sampling and then subtract that on that line as opposed to subtracting 0x0200?

Would this also help with the noise I’m getting? Currently I’ve added a simple function to only show bars with 3 or more segments but it’s not perfect.

The range of the ADC is 0-1023. The voltage divider formed by R4 & R5 sets the ADC input voltage with no signal to the midpoint of that range, nominally 512 or 0x0200, except the resistor tolerance is such that the midpoint might be a few digits higher or lower, so the constant value 0x0200 should be 0x0201 or 0x01FF, for instance. In any case, the current code subtracts the center point of the ADC value to get a nominally zero-centered number for the input signal.

If there is an error in this centering process, it will show up as energy in the first FHT bin. Options to get rid of this include at least the following: 1) Trim the voltage divider resistor values to zero the input, 2) Adjust the constant value subtracted from the ADC value to zero center the ADC output, 3) Dynamically calculate the average offset and subtract that from the ADC values. Options 1 & 2 may drift over time and/or temperature. Option 3 is self-calibrating, but requires more computation. A low computational cost variant of option 3 might be to simply not include the index 0 bin of the FHT output in summing the power for the first display bar.

Reference schematic from link in original post:

You ignore the zeroth bin typically if interested in a spectrum, as its pure DC.

MarkT:
You ignore the zeroth bin typically if interested in a spectrum, as its pure DC.

To be more precise each bin represents a band of frequencies.

In this case the sampling rate (about 38k samples/sec) divided by the number of FHT bins (128) gives a bandwidth per bin of about 300 Hz. Thus the zero frequency bin nominally covers -150 to 150 Hz. The window function smears the band edges so the spectrum analyzer would have some response to frequencies lower than 150 Hz, but they'd be attenuated.

Thank you very much both Mark's! Every loop I grab the average of all the bins and use that for the subtraction instead of a fixed value. I've also implemented a filter that compares the peak levels this sample to the ones from the previous sample, and ignores single-sample bursts to help remove the noise.

However I'm still not satisfied with the overall performance of this version on the Pro Mini, so instead of continuing to tinker with it, I'm starting over with Dave's Garage's ESP32 implementation of a spectrum analyzer instead. This has been a tremendous learning experience and again I thank everyone who helped.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.