[SOLVED] How to smoothen display of Neopixel spectrum analyzer

I have my Neopixel matrix spectrum analyzer functioning, but my main problem is that once the value in a bin is displayed on the LED matrix column, it disappears instantly once there is no longer a value in that bin. This results in a really "snappy" display, if you played a certain frequency for a split second, the corresponding column would briefly light up then completely turn off.

I'm hoping for some suggestions for my code such that in a column, once the LEDs turn on to display a value in a frequency bin, the LEDs will then turn off one at a time down the column. For example, if 8 LEDs in a column are turned on, and the next value in the bin says 5 LEDs should be turned on, then the LEDs will turn off one by one until 5 LEDs are left on.

This is exactly what I'm trying to replicate: if the video is slowed down, when observing any column, the LEDs do not turn off immediately, but rather the "height" of the column slowly decreases.

#define LIN_OUT 1 // use the log output function
#define FFT_N 256 // set to 256 point fft
#define DC_OFFSET 511
#define SAMPLING_FREQ (16000000/(13*32))

#include <FFT.h>  //FFT library
#include <FastLED.h>  //FastLED library

#define LED_DT 6  //LED data pin
#define COLOR_ORDER GRB //WS2812b specific
#define LED_TYPE WS2812B
#define NUM_LEDS 288  //Number of LEDs in project
#define COL_HEIGHT 9  //height of column

struct CRGB leds[NUM_LEDS];
float factors[32] = {0.65, 0.65, 0.7, 0.75, 0.75, 0.75, 
                     0.75, 0.75, 0.8, 0.8, 0.8, 0.9, 
                     1, 1, 1, 1, 1, 1, 
                     1.1, 1.1, 1.15, 1.15, 1.15, 1.15,
                     1.15, 1.2, 1.25, 1.25, 1.3, 1.3, 1.3, 1.3};

int adc_val;
const uint8_t Height = 32; //width
const uint8_t Width = 9; //height

void setup() {
  delay(1000);  //soft startup to ease flow of electrons

  LEDS.addLeds<LED_TYPE, LED_DT, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(100);

  TIMSK0 = 0; // turn off timer0 for lower jitter - delay() and millis() killed
  ADCSRA = 0xe5; // set the adc to free running mode
  ADMUX = 0x40; // use adc0
  DIDR0 = 0x01; // turn off the digital input for adc0
}

void loop() {
    while (1) { // reduces jitter
    cli();  // UDRE interrupt slows this way down on arduino1.0
    for (int i = 0 ; i < 512 ; i += 2) { // save 256 samples
      while (!(ADCSRA & 0x10)); // wait for adc to be ready
      ADCSRA = 0xf5; // restart adc
      byte m = ADCL; // fetch adc data
      byte j = ADCH;
      int k = (j << 8) | m; // form into an int
      k -= DC_OFFSET;
      adc_val = k<<6;
      fft_input[i] = adc_val; // put real data into even bins
      fft_input[i + 1] = 0; // set odd bins to 0
    }
    // window data, then reorder, then run, then take output
    fft_window(); // window the data for better frequency response
    fft_reorder(); // reorder the data before doing the fft
    fft_run(); // process the data in the fft
    fft_mag_lin(); // take the output of the fft
    sei(); // turn interrupts back on

    fft_lin_out[1] -= 10; //adjust for values in bin when silent (unknown cause)

    uint32_t ms = millis();
    int32_t yHueDelta32 = ((int32_t)cos16( ms * 27 ) * (350 / Width));
    int32_t xHueDelta32 = ((int32_t)cos16( ms * 39 ) * (310 / Height));
    fftDisplay(ms / 65536, yHueDelta32 / 32768, xHueDelta32 / 32768);
    FastLED.show();
   }
   
}


void fftDisplay(byte startHue8, int8_t yHueDelta8, int8_t xHueDelta8) {
  byte lineStartHue = startHue8;
  unsigned char i, yVal, colHeight;
  for(i = 0; i < 32; i++)
  {
    colHeight = map((unsigned char)fft_lin_out[i+1], 0, 235, 0, 9);
    colHeight = colHeight * factors[i];
    lineStartHue += yHueDelta8;
    byte pixelHue = lineStartHue;
    for(yVal = 0; yVal < COL_HEIGHT; yVal++) 
    {
      pixelHue += xHueDelta8;
      if(yVal <= colHeight && colHeight != 0)
        leds[XY(i, yVal)] = CHSV( pixelHue, 255, 255);
      else
        leds[XY(i, yVal)] = CRGB::Black;
    }
  }
}
uint16_t XY(uint8_t y, uint8_t x)
{
  uint16_t i;
  if(y & 0x01) {
      // Odd rows run backwards
      uint8_t reverseX = (Width - 1) - x;
      i = (y * Width) + reverseX;
    } else {
      // Even rows run forwards
      i = (y * Width) + x;
    }
   return i;
}

i'm fond of leaky integration.

A += (s - A) * K K < 1

K can have different attack (s > A) and decay (s <= A) rates. sounds like you would want a faster attack rate and slower decay rate. using powers of 2, the multiplication by K can be replaced with a shift operation.

you would do this for each bin in your display. (separate A for each bin: Ab)

gcjr:
i'm fond of leaky integration.

A += (s - A) * K K < 1

K can have different attack (s > A) and decay (s <= A) rates. sounds like you would want a faster attack rate and slower decay rate. using powers of 2, the multiplication by K can be replaced with a shift operation.

you would do this for each bin in your display. (separate A for each bin: Ab)

Hi, thanks for the response. I'm not familiar with this topic, would you mind explaining how this works and how to implement it?

I am referring to the code below which is the code from the Youtube video in my post. I've been studying the code and I don't know what allows the LEDs to decay.

/*
Written by FischiMc and SupaStefe

This sketch uses a 10x10 RGB LED-Matrix as a spectrum analyzer
It uses a FTT Library to analyze an audio signal connected to the
pin A7 of an Arduino nano. Everytime a column gets higher than
10 pixels the color of each column changes.
*/

#define LOG_OUT 0         //set output of FFT library to linear not logarithmical
#define LIN_OUT 1
#define FFT_N 256         //set to 256 point fft

#include <FFT.h>          //include the FFT library
#include <FastLED.h>      //include the FastLED Library
#include <math.h>         //include library for mathematic funcions
#define DATA_PIN 3        //DATA PIN WHERE YOUR LEDS ARE CONNECTED
#define NUM_LEDS 100      //amount of LEDs in your matrix
CRGB leds[NUM_LEDS];
float factors[10] = {1, 1.1, 1.15, 1.25, 1.45, 1.55, 1.75, 1.8, 2, 3};       //factors to increase the height of each column
unsigned char hs[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};                        //height of each column
float hue = 0;                                                                //hue value of the colors

void setBalken(unsigned char column, unsigned char height){                   //calculation of the height of each column
    unsigned char h = (unsigned char)map(height, 0, 255, 0, 10);
    h = (unsigned char)(h * factors[column]);
    if (h < hs[column]){
        hs[column]--;
    }
    else if (h > hs[column]){
        hs[column] = h;
    }
   if (height > 250){
      hue+=2;                     //CHANGE THIS VALUE IF YOU WANT THE DIFFERENCE BETWEEN THE COLORS TO BE BIGGER
      if(hue > 25) hue=0;
   }

    for(unsigned char y = 0; y < 10; y++){                          //set colors of pixels according to column and hue
       if(hs[column] > y){ 
        leds[y+(column*10)] = CHSV((hue*10)+(column*10), 255, 200);
       } else {
        leds[y+(column*10)] = CRGB::Black;
       }
    }
}

unsigned char limit[11] = {0,3,5,7,9,11,13,16,24,32,69};          //choose which bins to display.

void setup() {
  FastLED.addLeds<WS2812B, DATA_PIN, GRB> (leds, NUM_LEDS);
  Serial.begin(115200);                                             //use the serial port
  TIMSK0 = 0;                                                       //turn off timer0 for lower jitter
  ADCSRA = 0xe5;                                                    //set the adc to free running mode
  ADMUX = 0b01000111;                                               //use pin A7
  DIDR0 = 0x01;                                                     //turn off the digital input for 
  analogReference(EXTERNAL);                                        //set aref to external
}

void loop() {
  while(1) {                                                        //reduces jitter
    cli();                                                          //UDRE interrupt slows this way down on arduino1.0
    for (int i = 0 ; i < 512 ; i += 2) {                            //save 256 samples
      while(!(ADCSRA & 0x10));                                      //wait for adc to be ready
      ADCSRA = 0xf5;                                                //restart adc
      byte m = ADCL;                                                //fetch adc data
      byte j = ADCH;
      int k = (j << 8) | m;                                         //form into an int
      k -= 0x0200;                                                  //form into a signed int
      k <<= 6;                                                      //form into a 16b signed int
      fft_input[i] = k;                                             //put real data into even bins
    }

    fft_window();                                                   // window the data for better frequency response
    fft_reorder();                                                  // reorder the data before doing the fft
    fft_run();                                                      // process the data in the fft
    fft_mag_lin();                                                  // take the output of the fft
    sei();

    fft_lin_out[0] = 0;
    fft_lin_out[1] = 0;

	

    for(unsigned char i = 0; i < 11; i++){
      unsigned char maxW = 0;
        for(unsigned char x = limit[i]; x < limit[i+1];x++){
 
           if((unsigned char)fft_lin_out[x] > maxW){
            maxW = (unsigned char)fft_lin_out[x];
           }
        }

      setBalken(i, maxW);
      Serial.print(maxW);
      Serial.print(" ");
    }
    Serial.println("");
    TIMSK0 = 1;
    FastLED.show();
    TIMSK0 = 0;
  }
}

matledoublev:
I'm not familiar with this topic, would you mind explaining how this works and how to implement it?

the equation is a 1st order low-pass filter. The larger K is, the faster the response. making K larger when sig > avg means avg increases fast than it decreases when sig < avg, which means it holds peaks longer.

float average (int sig)
{
    static float avg = 0;
    if (sig < avg)
        avg += (sig - avg) * 0.0625;    // decay
    else
        avg += (sig - avg) * 0.25;      // attack

    return avg;
}

void setup () {
    Serial.begin (115200);

    for (int n = 0; n < 100; n++)  {
        float avg;
        int   sig;

        if (n < 20)
            sig = 1000;
        else
            sig = 0;

        avg = average (sig);

        char s [40];
        sprintf (s, " %4d %6d %6d", n, int(avg), sig);
        Serial.println (s);
    }
}

void loop() {
}

It can also be done very efficiently using integer math:

unsigned int smooth(unsigned int newVal) {
  static unsigned int oldVal = 0;
  unsigned long sum;
  // sum = (oldVal * 7 + newVal) / 8;
  sum = ( (oldVal << 3) - oldVal + newVal) >> 3;
  oldVal = sum;
  return oldVal;
}

Thanks for the replies everyone. I just solved my problem, and the solution was the hs[] array. It would store the previous height of each column, and the line "if(h < hs[column]) {hs[column]--}" would compare subtract the height of the column if there is no "louder" frequency. That way, each time the loop iterates, if there are no louder frequencies, the height of the column will drop by 1 each time until a louder sound.