Music visualizer — improving performance and features

Good day, everyone. Over the past couple weeks I’ve been tinkering with my Arduino Mega to make a music visualizer. You can see it in action here.

Using the FFT library, the Arduino takes in sound from an AUX cord and finds the dominant frequency. Depending on the frequency, a color is assigned; e.g. 55-100 Hz is blue, 100-175 Hz is purple, 175-250 Hz is red. Eight LEDs are set to that color and then shifted down the strip. So far I like how it looks and after posting it online, I went back and added in a feature to cycle between different color palettes.

I’m posting this because I have several things I’d like to discuss:

  1. I’d like to speed up the movement of the lights. If I remove the fourier transforms, I can get the lights to travel much faster. So I know the FFT is taking a lot of computing time, which slows down the propagation of the lights. But I need to find the dominant frequency somehow. I’ve heard that autocorrelation is another method of doing so. Would it be dramatically faster?

  2. After watching the video, do you have any ideas/tips/suggestions for additional features, movements, etc.?

  3. Is there any way to optimize my own code so it runs faster?

I conduct 64 samples per second (sps) across 1000 Hz. It’s precise enough, but increasing this to 128 sps improves the precision for sure. Increasing any higher yields negligible results and is simply overkill. Decreasing the sps to 32 reduces the accuracy drastically. That’s not a viable option for improving performance.

If this is the wrong forum for this then I apologize.

Here’s my code if anyone’s interested:

#include "arduinoFFT.h"
#include "FastLED.h"

#define SAMPLES 64 //must be a power of 2
#define SAMPLING_FREQUENCY 1000 //Hz, must be less than 9615Hz due to ADC

#define NUM_LEDS 300
#define LED_PIN 6
#define updateLEDS 8        // How many do you want to update every millisecond?
CRGB leds[NUM_LEDS];
CRGBPalette16 currentPalette;
TBlendType    currentBlending;     //Optional

arduinoFFT FFT=arduinoFFT();

unsigned int sampling_period_us;
unsigned long microseconds;
double vReal[SAMPLES];
double vImag[SAMPLES];
 
void setup() {
    //Serial.begin(115200);
    sampling_period_us = round(1000000*(1.0/SAMPLING_FREQUENCY));
    FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
}
 
void loop() {
   
    /*SAMPLING*/
    for(int i=0; i<SAMPLES; i++){
        microseconds = micros();    //Overflows after ~70 minutes
        vReal[i] = analogRead(0);
        vImag[i] = 0;
        while(micros() < (microseconds + sampling_period_us)){
        }
    }

    //FFT
    FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
    FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
    double peak = FFT.MajorPeak(vReal, SAMPLES, SAMPLING_FREQUENCY);
    //Serial.println(peak);     //Print out what frequency is the most dominant. Use this to test accurate sampling

    //Rotate through different palettes so people don't get bored
    ChangePalettePeriodically();
    //SetupSimulationTheoryPalette(); //Or uncomment this line to get the Muse palette

    // Shift all LEDs to the right by updateLEDS number each time
    for(int i = NUM_LEDS - 1; i >= updateLEDS; i--) {
        leds[i] = leds[i - updateLEDS];
    }

    // Set the left-most updateLEDs with the new color
    uint8_t colorIndex=255;

    if(peak>55 && peak<100){
        colorIndex=0;
    }
    else if(peak>100 && peak<175){
        colorIndex=16;
    }
    else if(peak>175 && peak<250){
        colorIndex=32;
    }
    else if(peak>250 && peak<325){
        colorIndex=48;
    }
    else if(peak>325 && peak<400){
        colorIndex=64;    
    }
    else if(peak>400 && peak<475){
        colorIndex=80;    
    }
    else if(peak>475){
        colorIndex=96;    
    }
    else{
        colorIndex=255;     //turn off LEDs, i.e. make color black
    }

    FillLEDsFromPaletteColors(colorIndex);
    FastLED.show();
}

void FillLEDsFromPaletteColors(uint8_t colorIndex){
    //Brightness can go up to 255, but then needs more current
    uint8_t brightness=64;

    for(int i=0; i<updateLEDS; i++) {
        if(colorIndex==255){
            leds[i]=CRGB::Black;
        }
        else{
            leds[i]=ColorFromPalette(currentPalette, colorIndex, brightness, currentBlending);
        }
    }
}

void ChangePalettePeriodically(){
    uint8_t secondHand = (millis() / 1000) % 60;
    static uint8_t lastSecond = 99;
    
    if(lastSecond != secondHand) {
        lastSecond = secondHand;
        if(secondHand ==  0)  { SetupSimulationTheoryPalette();      }
        if(secondHand == 15)  { SetupPurpleAndGreenPalette();        }
        if(secondHand == 30)  { SetupBlackAndWhiteStripedPalette();  }
        if(secondHand == 45)  { SetupBlueAndYellowPalette();     }
    }
}

//CAREFUL with this one. White LEDs consume much more current
//The LEDs at the end of the strip aren't fully white due to the voltage drop along the strip
//This might occur with all color schemes, but it's very noticeable with the color white
void SetupBlackAndWhiteStripedPalette(){
    // 'white out' all 16 palette entries...
    fill_solid(currentPalette, 16, CRGB::White);
    currentPalette[3] = CRGB::Black;  //so not all 300 LEDs are white at the same time
}

void SetupPurpleAndGreenPalette(){
    CRGB purple = CHSV(HUE_PURPLE, 255, 255);
    CRGB green  = CHSV(HUE_GREEN, 255, 255);
    CRGB black  = CRGB::Black;
    
    currentPalette = CRGBPalette16(
                                   purple,  green,  purple,  green,
                                   purple, green, purple,  green,
                                   green,  purple,  green,  purple,
                                   green, purple, black,  black );
}

void SetupSimulationTheoryPalette(){
    CRGB blue = 0x2F00D0;
    CRGB blueViolet  = 0x5F00A1;
    CRGB brightBlue  = 0x0007F9;
    CRGB clearViolet  = 0x5500AB;
    CRGB violetPink  = 0x84007C;
    CRGB pink  = 0xB5004B;
    CRGB pinkViolet  = 0xC2003E;
    CRGB black  = CRGB::Black;
    
    currentPalette = CRGBPalette16(
                                   blue,  blueViolet,  brightBlue,  clearViolet,
                                   violetPink, pink, pinkViolet,  black,
                                   black,  black,  black,  black,
                                   black, black, black,  black);
}

void SetupBlueAndYellowPalette(){
    CRGB blue = CRGB::Blue;
    CRGB yellow  = CRGB::Yellow;
    CRGB black  = CRGB::Black;
    
    currentPalette = CRGBPalette16(
                                   yellow,  blue,  yellow,  blue,
                                   yellow, blue, yellow,  blue,
                                   yellow,  blue,  yellow,  blue,
                                   yellow, blue, black,  black );
}

Maybe try an [u]MSGEQ7[/u] chip. Filtering is done in hardware and it gives you a 7 (multiplexed) varying DC voltages representing 7 frequency bands.

It has one (multiplexed) output that connects to an Arduino analog input and 2 control inputs that need to be connected to Arduino digital outputs.

Place the LED update code into a timer ISR. Use 2 arrays for display: old and new. Let the ISR move smoothly between the two. Make FFT in loop() and write the result to the “new” array. e.g. for one value in ISR (pseudocode, just to gice the idea):

#define MAX_DIFF 10
int diff=new-old;
diff=(diff<-MAX_DIFF)?-MAX_DIFF:(diff>MAX_DIFF)?MAX_DIFF:diff;
old+=diff;

and in loop():

volatile int new;     // volatile is crutial !

loop() {
....
do_fft();
new=new_value_of_fft;
....
}

MAX_DIFF + frequency of ISR gives max. movement of bar.

DVDdoug, thanks for the suggestion! I’ve ordered the MSGEQ7 chip and will give that a try.

zwieblum, thanks for the idea! I’ve never used interrupts before so I’m a little confused by this:

#define MAX_DIFF 10
int diff=new-old;
diff=(diff<-MAX_DIFF)?-MAX_DIFF:(diff>MAX_DIFF)?MAX_DIFF:diff;
old+=diff;

Would you mind elaborating on what this is supposed to do and how it will speed up the circuit?

It will not speed up anything, it will just dampen the speed at which values change - which gives the illusion of continousity. TO get things faster, you need to make the sampling of data interruptdriven, the rest stays the same.

So I got the MSGEQ7 chip and built the circuit in the datasheet on page 4. The issue is a certain if-statement that messes things up.

#include "FastLED.h"

#define NUM_LEDS 300        //How many leds in the strip?
#define LED_PIN 6
#define updateLEDS 8        //How many do you want to update?
#define BRIGHTNESS 64

#define STROBE_PIN 4
#define RESET_PIN 7
#define MSGEQ7_OUT A1

CRGB leds[NUM_LEDS];
CRGBPalette16 currentPalette;

int freq_bands[7];  //63Hz, 160Hz, 400Hz, 1kHz, 2.5kHz, 6.25kHz, 16kHz
uint8_t mapValue[7];
int filter = 0;
 
void setup() {
    Serial.begin(115200);
    FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
    pinMode(MSGEQ7_OUT, INPUT);
    pinMode(STROBE_PIN, OUTPUT);
    pinMode(RESET_PIN, OUTPUT);
    
    digitalWrite(RESET_PIN, LOW);   //reset the mux just to be safe
    digitalWrite(STROBE_PIN, HIGH);
    SetupCyanPalette();
    FastLED.show();
}
 
void loop() {
    digitalWrite(RESET_PIN, HIGH);   //this enables the strobe pin; might want to put inside the for-loop
    digitalWrite(RESET_PIN, LOW);
    delayMicroseconds(75);
    int peak = 0;
    int peak_index = 0;
    
    for(int i = 0; i<7; i++){
        digitalWrite(STROBE_PIN, LOW);
        delayMicroseconds(40);
        freq_bands[i] = analogRead(MSGEQ7_OUT);    //MSGEQ7 output connected to pin A0
        freq_bands[i] = constrain(freq_bands[i], filter, 1023);
        
        digitalWrite(STROBE_PIN, HIGH);
        mapValue[i] = map(freq_bands[i], filter, 1023, 0, 255);
        if (mapValue[i] < 50){
            mapValue[i] = 0;
        }
        if (mapValue[i] > peak){                                      <---------------------------------------------------------------------------
            peak = mapValue[i];                                       <---------------------------------------------------------------------------
            peak_index = i;                                           <---------------------------------------------------------------------------
        }
        delayMicroseconds(40);
    }

    // Shift all LEDs to the right by updateLEDS number each time
    for(int i = NUM_LEDS-1; i >= updateLEDS; i--) {
        leds[i] = leds[i-updateLEDS];
    }

    // Set the left-most updateLEDs with the new color
    FillLEDsFromPaletteColors(peak);
    FastLED.show();
    delay(13);
}

void FillLEDsFromPaletteColors(uint8_t colorIndex){
    for(int i = 0; i<updateLEDS; i++) {
        if(colorIndex == 0){
            leds[i] = CRGB::Black;
        }
        else{
            leds[i] = ColorFromPalette(currentPalette, colorIndex, BRIGHTNESS);
        }
    }
}

Since the MSGEQ7 analyzes 7 bands, I read in the voltage of each band, and look at which one is the maximum. I figure that’s the dominant frequency, and then assign a color to it. The issue is that the output of the MSGEQ7, which is inputted to the Arduino, changes drastically depending on whether or not this if-statement is in the code.

For instance, if the if-statement isn’t there, the voltage levels for each band will be in the 80-100 range. If it is there, these values drop to the 18-25 range, and the lights never turn on. I don’t understand why changing the “peak” variable affects the output of the chip. What I do in the code shouldn’t change the external inputs from the environment. What is going on here?