My project is a modified version of Scott Marley's excellent 16x16 FFT vu meter.
Here's my wiring diagram:
Here's my code:
#include <arduinoFFT.h>
#include <FastLED.h>
#include <FastLED_NeoMatrix.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
//#include <EEPROM.h>
#define SAMPLES 256 // Must be a power of 2
#define SAMPLING_FREQ 20000 // Hz, must be 40000 or less due to ADC conversion time. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2.
#define AUDIO_IN_PIN 35 // Signal in on this pin
#define LED_PIN 2 // LED strip data
#define COLOR_ORDER GRB // If colours look wrong, play with this
#define CHIPSET WS2812B // LED strip type
#define MAX_MILLIAMPS 2000 // Careful with the amount of power here if running off USB port
#define LED_VOLTS 5 // Usually 5 or 12
#define NUM_BANDS 32 // To change this, you will need to change the bunch of if statements describing the mapping from bins to bands
#define NOISE 400 // Used as a crude noise filter, values below this are ignored
const uint8_t kMatrixWidth = 32; // Matrix width
const uint8_t kMatrixHeight = 8; // Matrix height
#define MATRIX_TILE_H 1
#define MATRIX_TILE_V 1
#define NUM_LEDS (kMatrixWidth * kMatrixHeight * MATRIX_TILE_H * MATRIX_TILE_V) // Total number of LEDs
#define BAR_WIDTH (kMatrixWidth * MATRIX_TILE_H / (NUM_BANDS - 1)) // If width >= 8 light 1 LED width per bar, >= 16 light 2 LEDs width bar etc
#define TOP (kMatrixHeight) // Don't allow the bars to go offscreen
#define SQUARE_LENGTH (kMatrixHeight / 2)
uint8_t gHue = 0; // rotating "base color" used by many of the patterns
CRGB leds[NUM_LEDS];
FastLED_NeoMatrix *matrix = new FastLED_NeoMatrix(leds, kMatrixWidth * MATRIX_TILE_H, kMatrixHeight,
NEO_MATRIX_TOP + NEO_MATRIX_RIGHT +
NEO_MATRIX_COLUMNS + NEO_MATRIX_ZIGZAG +
NEO_TILE_TOP + NEO_TILE_LEFT + NEO_TILE_ROWS + NEO_TILE_PROGRESSIVE);
//AUDIO SAMPLING
unsigned int sampling_period_us;
int oldBarHeights[NUM_BANDS];
int oldSquareLevels[16];
int bandValues[NUM_BANDS];
double vReal[SAMPLES];
double vImag[SAMPLES];
unsigned long newTime;
arduinoFFT FFT = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ);
//BLUETOOTH
/*#include <BlynkSimpleEsp32_BLE.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#define BLYNK_USE_DIRECT_CONNECT*/
//WIFI
char auth[] = "blah"
char ssid[] = "blah";
char pass[] = "blah";
//BLYNK PARAMS
boolean showGlitter = false;
boolean paintRainbow = true;
boolean flip = false;
boolean invert = false;
byte patternNumber = 0;
byte brightness = 50;
int amp = 400;
void setup() {
Serial.begin(115200);
FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalSMD5050);
FastLED.setMaxPowerInVoltsAndMilliamps(LED_VOLTS, MAX_MILLIAMPS);
FastLED.setBrightness(brightness);
FastLED.clear();
FastLED.show();
delay(3000);
sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ));
//WIFI
Blynk.begin(auth, ssid, pass);
}
void loop() {
int t1 = millis();
processAudio();
int t2 = millis();
paintLEDs();
int t3 = millis();
}
void paintLEDs(){
if(patternNumber != 2 && patternNumber != 3){
FastLED.clear();
}
int AMPLITUDE = amp;
if(patternNumber == 0){
for (byte band = 0; band < NUM_BANDS; band++) {
// Scale the bars for the display
int barHeight = bandValues[band] / AMPLITUDE;
if (barHeight > TOP) barHeight = TOP;
// Small amount of averaging between frames
double avBarHeight = ((double)oldBarHeights[band] + (double) barHeight) / 2;
if(oldBarHeights[band] > 1){
barHeight = ceil(avBarHeight); // round up to get top bar height
}
drawBar(band, barHeight);
oldBarHeights[band] = barHeight;
}
}
else if (patternNumber == 1){
for (byte i = 0; i < 16; i++){
drawSquare(i);
}
}
else if (patternNumber == 2){
for (byte band = 0; band < NUM_BANDS; band++) {
drawRain(band);
}
}
/*else if (patternNumber == 3){
for (byte band = 0; band < NUM_BANDS; band++) {
drawPulse(band);
}
}*/
if(showGlitter){
addGlitter(80);
}
FastLED.show();
EVERY_N_MILLISECONDS( 200 ) { gHue++; }
Blynk.run();
}
void processAudio(){
for (int i = 0; i<NUM_BANDS; i++){
bandValues[i] = 0;
}
// Sample the audio pin
for (int i = 0; i < SAMPLES; i++) {
newTime = micros();
vReal[i] = analogRead(AUDIO_IN_PIN); // A conversion takes about 9.7uS on an ESP32
vImag[i] = 0;
//while ((micros() - newTime) < sampling_period_us) { /* chill */ }
}
// Compute FFT
FFT.DCRemoval();
FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(FFT_FORWARD);
FFT.ComplexToMagnitude();
// Analyse FFT results
for (int i = 1; i < (SAMPLES/2); i++){ // Don't use sample 0 and only first SAMPLES/2 are usable. Each array element represents a frequency bin and its value the amplitude.
if (vReal[i] > NOISE) { // Add a crude noise filter
//16 bands, 12kHz top band
if (i==2 ) bandValues[0] += (int)vReal[i];
if (i==3 ) bandValues[1] += (int)vReal[i];
if (i==4 ) bandValues[2] += (int)vReal[i];
if (i==5 ) bandValues[3] += (int)vReal[i];
if (i==6 ) bandValues[4] += (int)vReal[i];
if (i==7 ) bandValues[5] += (int)vReal[i];
if (i==8) bandValues[6] += (int)vReal[i];
if (i==9 ) bandValues[7] += (int)vReal[i];
if (i==10 ) bandValues[8] += (int)vReal[i];
if (i==11 ) bandValues[9] += (int)vReal[i];
if (i==12 ) bandValues[10] += (int)vReal[i];
if (i==13 ) bandValues[11] += (int)vReal[i];
if (i==14 ) bandValues[12] += (int)vReal[i];
if (i==15 ) bandValues[13] += (int)vReal[i];
if (i==16 ) bandValues[14] += (int)vReal[i];
if (i==17 ) bandValues[15] += (int)vReal[i];
if (i==18 ) bandValues[16] += (int)vReal[i];
if (i>18 && i <=20 ) bandValues[17] += (int)vReal[i];
if (i>20 && i <=24 ) bandValues[18] += (int)vReal[i];
if (i>24 && i <=29 ) bandValues[19] += (int)vReal[i];
if (i>29 && i <=34 ) bandValues[20] += (int)vReal[i];
if (i>34 && i <=41 ) bandValues[21] += (int)vReal[i];
if (i>41 && i <=48 ) bandValues[22] += (int)vReal[i];
if (i>48 && i <=57 ) bandValues[23] += (int)vReal[i];
if (i>57 && i <=68 ) bandValues[24] += (int)vReal[i];
if (i>68 && i <=80 ) bandValues[25] += (int)vReal[i];
if (i>80 && i <=95 ) bandValues[26] += (int)vReal[i];
if (i>95 && i <=113 ) bandValues[27] += (int)vReal[i];
if (i>113 && i <=134 ) bandValues[28] += (int)vReal[i];
if (i>134 && i <=159 ) bandValues[29] += (int)vReal[i];
if (i>159 && i <=189 ) bandValues[30] += (int)vReal[i];
if (i>189 && i <=224 ) bandValues[31] += (int)vReal[i];
}
}
}
/*void drawPulse(int band){
int value = bandValues[band];
byte x = random8(0,31);
byte y = random8(0,7);
if(paintRainbow){
matrix-> drawPixel(x, y, CHSV(band * (255 / 32) + gHue, 255, value));
}
else{
matrix-> drawPixel(x, y, CHSV(band * 2 + gHue, 255, value));
}
int centrePixel = matrix->XY(x, y);
int topPixel = matrix->XY(x, y+1);
int bottomPixel = matrix->XY(x, y-1);
int rightPixel = matrix->XY(x+1, y);
int leftPixel = matrix->XY(x-1, y);
leds[topPixel] = leds[centrePixel] + leds[topPixel];
leds[bottomPixel] = leds[centrePixel] + leds[bottomPixel];
leds[rightPixel] = leds[centrePixel] + leds[rightPixel];
leds[leftPixel] = leds[centrePixel] + leds[leftPixel];
leds[centrePixel].fadeToBlackBy(150);
leds[topPixel].fadeToBlackBy(150);
leds[bottomPixel].fadeToBlackBy(150);
leds[rightPixel].fadeToBlackBy(150);
leds[leftPixel].fadeToBlackBy(150);
}*/
void drawSquare(byte pos){
byte n = pos*2;
int level = (oldSquareLevels[pos] + (bandValues[n] + bandValues[n+1]) / 2) / 2;
level = level / (amp/50);
if (level > 255) level = 255;
double avLevel = ((double)oldSquareLevels[pos] + (double) level) / 2;
if(level > 0){
level = ceil(avLevel); // round up to get top bar height
}
oldSquareLevels[pos] = level;
byte xStart = (pos / 2) * 4; //multiply quotient
byte yStart = (pos % 2) * 4;
for (int x = xStart; x < xStart + SQUARE_LENGTH; x++){
for (int y = yStart; y < yStart + SQUARE_LENGTH; y++){
if(invert){
matrix-> drawPixel(x, y, CHSV(pos * (255 / 16) + gHue, 255, 255-level));
}
else{
matrix-> drawPixel(x, y, CHSV(pos * (255 / 16) + gHue, 255, level));
}
}
}
}
void drawBar(int band, int barHeight) {
int xStart = BAR_WIDTH * band;
for (int x = xStart; x < xStart + BAR_WIDTH; x++) {
if(flip && !invert){
for (int y = TOP; y >= TOP-barHeight; y--) {
if(barHeight > 0) draw(x,y);
}
}
else if(flip && invert){
for (int y = TOP; y >= barHeight; y--) {
draw(x,y);
}
}
else if(!flip && invert){
for (int y = 0; y <= TOP-barHeight; y++) {
draw(x,y);
}
}
else{
for (int y = 0; y < barHeight; y++) {
if(barHeight > 0) draw(x,y);
}
}
}
}
void drawRain(int band){
int value = bandValues[band];
if(paintRainbow){
matrix-> drawPixel(band, kMatrixHeight-1, CHSV(band * (255 / 32) + gHue, 255, value));
}
else{
matrix-> drawPixel(band, kMatrixHeight-1, CHSV(band * 2 + gHue, 255, value));
}
if (band == NUM_BANDS - 1){
//EVERY_N_MILLISECONDS (50){
for (int y = 0; y < kMatrixHeight-1; y++) {
for (int x = 0; x < kMatrixWidth; x++) {
int newPixelIndex = matrix->XY(x, y);
int oldPixelIndex = matrix->XY(x, y + 1);
leds[newPixelIndex] = leds[oldPixelIndex];
leds[newPixelIndex].nscale8(180);
if(leds[newPixelIndex].getAverageLight() < 5){
leds[newPixelIndex] = CRGB::Black;
}
}
}
//}
}
}
void draw(int x, int y){
if(paintRainbow){
matrix-> drawPixel(x, y, CHSV((x / BAR_WIDTH) * (255 / NUM_BANDS) + gHue, 255, 255));
}
else{
matrix-> drawPixel(x, y, CHSV(gHue+(x+y), 255, 255));
}
}
void addGlitter(fract8 chanceOfGlitter)
{
if( random8() < chanceOfGlitter) {
leds[ random16(NUM_LEDS) ] += CRGB::White;
}
}
BLYNK_WRITE(V0)
{
brightness = param.asInt();
FastLED.setBrightness(brightness);
}
BLYNK_WRITE(V1)
{
showGlitter = param.asInt();
}
BLYNK_WRITE(V2)
{
paintRainbow = param.asInt();
}
BLYNK_WRITE(V3)
{
patternNumber = param.asInt();
}
BLYNK_WRITE(V4)
{
amp = param.asInt();
}
BLYNK_WRITE(V5)
{
flip = param.asInt();
}
BLYNK_WRITE(V6)
{
invert = param.asInt();
}