├── ESP32_FFT_VU └── ESP32_FFT_VU.ino ├── FFT.xlsx ├── LICENSE ├── Linein_bb.png ├── Microphone_bb.png ├── README.md └── Sample_test └── Sample_test.ino /ESP32_FFT_VU/ESP32_FFT_VU.ino: -------------------------------------------------------------------------------- 1 | // (Heavily) adapted from https://github.com/G6EJD/ESP32-8266-Audio-Spectrum-Display/blob/master/ESP32_Spectrum_Display_02.ino 2 | // Adjusted to allow brightness changes on press+hold, Auto-cycle for 3 button presses within 2 seconds 3 | // Edited to add Neomatrix support for easier compatibility with different layouts. 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #define SAMPLES 1024 // Must be a power of 2 10 | #define SAMPLING_FREQ 40000 // Hz, must be 40000 or less due to ADC conversion time. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2. 11 | #define AMPLITUDE 1000 // Depending on your audio source level, you may need to alter this value. Can be used as a 'sensitivity' control. 12 | #define AUDIO_IN_PIN 35 // Signal in on this pin 13 | #define LED_PIN 5 // LED strip data 14 | #define BTN_PIN 4 // Connect a push button to this pin to change patterns 15 | #define LONG_PRESS_MS 200 // Number of ms to count as a long press 16 | #define COLOR_ORDER GRB // If colours look wrong, play with this 17 | #define CHIPSET WS2812B // LED strip type 18 | #define MAX_MILLIAMPS 2000 // Careful with the amount of power here if running off USB port 19 | const int BRIGHTNESS_SETTINGS[3] = {5, 70, 200}; // 3 Integer array for 3 brightness settings (based on pressing+holding BTN_PIN) 20 | #define LED_VOLTS 5 // Usually 5 or 12 21 | #define NUM_BANDS 16 // To change this, you will need to change the bunch of if statements describing the mapping from bins to bands 22 | #define NOISE 500 // Used as a crude noise filter, values below this are ignored 23 | const uint8_t kMatrixWidth = 16; // Matrix width 24 | const uint8_t kMatrixHeight = 16; // Matrix height 25 | #define NUM_LEDS (kMatrixWidth * kMatrixHeight) // Total number of LEDs 26 | #define BAR_WIDTH (kMatrixWidth / (NUM_BANDS - 1)) // If width >= 8 light 1 LED width per bar, >= 16 light 2 LEDs width bar etc 27 | #define TOP (kMatrixHeight - 0) // Don't allow the bars to go offscreen 28 | #define SERPENTINE true // Set to false if you're LEDS are connected end to end, true if serpentine 29 | 30 | // Sampling and FFT stuff 31 | unsigned int sampling_period_us; 32 | byte peak[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // The length of these arrays must be >= NUM_BANDS 33 | int oldBarHeights[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; 34 | int bandValues[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; 35 | double vReal[SAMPLES]; 36 | double vImag[SAMPLES]; 37 | unsigned long newTime; 38 | arduinoFFT FFT = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ); 39 | 40 | // Button stuff 41 | int buttonPushCounter = 0; 42 | bool autoChangePatterns = false; 43 | EasyButton modeBtn(BTN_PIN); 44 | 45 | // FastLED stuff 46 | CRGB leds[NUM_LEDS]; 47 | DEFINE_GRADIENT_PALETTE( purple_gp ) { 48 | 0, 0, 212, 255, //blue 49 | 255, 179, 0, 255 }; //purple 50 | DEFINE_GRADIENT_PALETTE( outrun_gp ) { 51 | 0, 141, 0, 100, //purple 52 | 127, 255, 192, 0, //yellow 53 | 255, 0, 5, 255 }; //blue 54 | DEFINE_GRADIENT_PALETTE( greenblue_gp ) { 55 | 0, 0, 255, 60, //green 56 | 64, 0, 236, 255, //cyan 57 | 128, 0, 5, 255, //blue 58 | 192, 0, 236, 255, //cyan 59 | 255, 0, 255, 60 }; //green 60 | DEFINE_GRADIENT_PALETTE( redyellow_gp ) { 61 | 0, 200, 200, 200, //white 62 | 64, 255, 218, 0, //yellow 63 | 128, 231, 0, 0, //red 64 | 192, 255, 218, 0, //yellow 65 | 255, 200, 200, 200 }; //white 66 | CRGBPalette16 purplePal = purple_gp; 67 | CRGBPalette16 outrunPal = outrun_gp; 68 | CRGBPalette16 greenbluePal = greenblue_gp; 69 | CRGBPalette16 heatPal = redyellow_gp; 70 | uint8_t colorTimer = 0; 71 | 72 | // FastLED_NeoMaxtrix - see https://github.com/marcmerlin/FastLED_NeoMatrix for Tiled Matrixes, Zig-Zag and so forth 73 | FastLED_NeoMatrix *matrix = new FastLED_NeoMatrix(leds, kMatrixWidth, kMatrixHeight, 74 | NEO_MATRIX_TOP + NEO_MATRIX_LEFT + 75 | NEO_MATRIX_ROWS + NEO_MATRIX_ZIGZAG + 76 | NEO_TILE_TOP + NEO_TILE_LEFT + NEO_TILE_ROWS); 77 | 78 | void setup() { 79 | Serial.begin(115200); 80 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalSMD5050); 81 | FastLED.setMaxPowerInVoltsAndMilliamps(LED_VOLTS, MAX_MILLIAMPS); 82 | FastLED.setBrightness(BRIGHTNESS_SETTINGS[1]); 83 | FastLED.clear(); 84 | 85 | modeBtn.begin(); 86 | modeBtn.onPressed(changeMode); 87 | modeBtn.onPressedFor(LONG_PRESS_MS, brightnessButton); 88 | modeBtn.onSequence(3, 2000, startAutoMode); 89 | modeBtn.onSequence(5, 2000, brightnessOff); 90 | sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ)); 91 | } 92 | 93 | void changeMode() { 94 | Serial.println("Button pressed"); 95 | if (FastLED.getBrightness() == 0) FastLED.setBrightness(BRIGHTNESS_SETTINGS[0]); //Re-enable if lights are "off" 96 | autoChangePatterns = false; 97 | buttonPushCounter = (buttonPushCounter + 1) % 6; 98 | } 99 | 100 | void startAutoMode() { 101 | autoChangePatterns = true; 102 | } 103 | 104 | void brightnessButton() { 105 | if (FastLED.getBrightness() == BRIGHTNESS_SETTINGS[2]) FastLED.setBrightness(BRIGHTNESS_SETTINGS[0]); 106 | else if (FastLED.getBrightness() == BRIGHTNESS_SETTINGS[0]) FastLED.setBrightness(BRIGHTNESS_SETTINGS[1]); 107 | else if (FastLED.getBrightness() == BRIGHTNESS_SETTINGS[1]) FastLED.setBrightness(BRIGHTNESS_SETTINGS[2]); 108 | else if (FastLED.getBrightness() == 0) FastLED.setBrightness(BRIGHTNESS_SETTINGS[0]); //Re-enable if lights are "off" 109 | } 110 | 111 | void brightnessOff(){ 112 | FastLED.setBrightness(0); //Lights out 113 | } 114 | 115 | void loop() { 116 | 117 | // Don't clear screen if waterfall pattern, be sure to change this is you change the patterns / order 118 | if (buttonPushCounter != 5) FastLED.clear(); 119 | 120 | modeBtn.read(); 121 | 122 | // Reset bandValues[] 123 | for (int i = 0; i NOISE) { // Add a crude noise filter 144 | 145 | /*8 bands, 12kHz top band 146 | if (i<=3 ) bandValues[0] += (int)vReal[i]; 147 | if (i>3 && i<=6 ) bandValues[1] += (int)vReal[i]; 148 | if (i>6 && i<=13 ) bandValues[2] += (int)vReal[i]; 149 | if (i>13 && i<=27 ) bandValues[3] += (int)vReal[i]; 150 | if (i>27 && i<=55 ) bandValues[4] += (int)vReal[i]; 151 | if (i>55 && i<=112) bandValues[5] += (int)vReal[i]; 152 | if (i>112 && i<=229) bandValues[6] += (int)vReal[i]; 153 | if (i>229 ) bandValues[7] += (int)vReal[i];*/ 154 | 155 | //16 bands, 12kHz top band 156 | if (i<=2 ) bandValues[0] += (int)vReal[i]; 157 | if (i>2 && i<=3 ) bandValues[1] += (int)vReal[i]; 158 | if (i>3 && i<=5 ) bandValues[2] += (int)vReal[i]; 159 | if (i>5 && i<=7 ) bandValues[3] += (int)vReal[i]; 160 | if (i>7 && i<=9 ) bandValues[4] += (int)vReal[i]; 161 | if (i>9 && i<=13 ) bandValues[5] += (int)vReal[i]; 162 | if (i>13 && i<=18 ) bandValues[6] += (int)vReal[i]; 163 | if (i>18 && i<=25 ) bandValues[7] += (int)vReal[i]; 164 | if (i>25 && i<=36 ) bandValues[8] += (int)vReal[i]; 165 | if (i>36 && i<=50 ) bandValues[9] += (int)vReal[i]; 166 | if (i>50 && i<=69 ) bandValues[10] += (int)vReal[i]; 167 | if (i>69 && i<=97 ) bandValues[11] += (int)vReal[i]; 168 | if (i>97 && i<=135) bandValues[12] += (int)vReal[i]; 169 | if (i>135 && i<=189) bandValues[13] += (int)vReal[i]; 170 | if (i>189 && i<=264) bandValues[14] += (int)vReal[i]; 171 | if (i>264 ) bandValues[15] += (int)vReal[i]; 172 | } 173 | } 174 | 175 | // Process the FFT data into bar heights 176 | for (byte band = 0; band < NUM_BANDS; band++) { 177 | 178 | // Scale the bars for the display 179 | int barHeight = bandValues[band] / AMPLITUDE; 180 | if (barHeight > TOP) barHeight = TOP; 181 | 182 | // Small amount of averaging between frames 183 | barHeight = ((oldBarHeights[band] * 1) + barHeight) / 2; 184 | 185 | // Move peak up 186 | if (barHeight > peak[band]) { 187 | peak[band] = min(TOP, barHeight); 188 | } 189 | 190 | // Draw bars 191 | switch (buttonPushCounter) { 192 | case 0: 193 | rainbowBars(band, barHeight); 194 | break; 195 | case 1: 196 | // No bars on this one 197 | break; 198 | case 2: 199 | purpleBars(band, barHeight); 200 | break; 201 | case 3: 202 | centerBars(band, barHeight); 203 | break; 204 | case 4: 205 | changingBars(band, barHeight); 206 | break; 207 | case 5: 208 | waterfall(band); 209 | break; 210 | } 211 | 212 | // Draw peaks 213 | switch (buttonPushCounter) { 214 | case 0: 215 | whitePeak(band); 216 | break; 217 | case 1: 218 | outrunPeak(band); 219 | break; 220 | case 2: 221 | whitePeak(band); 222 | break; 223 | case 3: 224 | // No peaks 225 | break; 226 | case 4: 227 | // No peaks 228 | break; 229 | case 5: 230 | // No peaks 231 | break; 232 | } 233 | 234 | // Save oldBarHeights for averaging later 235 | oldBarHeights[band] = barHeight; 236 | } 237 | 238 | // Decay peak 239 | EVERY_N_MILLISECONDS(60) { 240 | for (byte band = 0; band < NUM_BANDS; band++) 241 | if (peak[band] > 0) peak[band] -= 1; 242 | colorTimer++; 243 | } 244 | 245 | // Used in some of the patterns 246 | EVERY_N_MILLISECONDS(10) { 247 | colorTimer++; 248 | } 249 | 250 | EVERY_N_SECONDS(10) { 251 | if (autoChangePatterns) buttonPushCounter = (buttonPushCounter + 1) % 6; 252 | } 253 | 254 | FastLED.show(); 255 | } 256 | 257 | // PATTERNS BELOW // 258 | 259 | void rainbowBars(int band, int barHeight) { 260 | int xStart = BAR_WIDTH * band; 261 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 262 | for (int y = TOP; y >= TOP - barHeight; y--) { 263 | matrix->drawPixel(x, y, CHSV((x / BAR_WIDTH) * (255 / NUM_BANDS), 255, 255)); 264 | } 265 | } 266 | } 267 | 268 | void purpleBars(int band, int barHeight) { 269 | int xStart = BAR_WIDTH * band; 270 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 271 | for (int y = TOP; y >= TOP - barHeight; y--) { 272 | matrix->drawPixel(x, y, ColorFromPalette(purplePal, y * (255 / (barHeight + 1)))); 273 | } 274 | } 275 | } 276 | 277 | void changingBars(int band, int barHeight) { 278 | int xStart = BAR_WIDTH * band; 279 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 280 | for (int y = TOP; y >= TOP - barHeight; y--) { 281 | matrix->drawPixel(x, y, CHSV(y * (255 / kMatrixHeight) + colorTimer, 255, 255)); 282 | } 283 | } 284 | } 285 | 286 | void centerBars(int band, int barHeight) { 287 | int xStart = BAR_WIDTH * band; 288 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 289 | if (barHeight % 2 == 0) barHeight--; 290 | int yStart = ((kMatrixHeight - barHeight) / 2 ); 291 | for (int y = yStart; y <= (yStart + barHeight); y++) { 292 | int colorIndex = constrain((y - yStart) * (255 / barHeight), 0, 255); 293 | matrix->drawPixel(x, y, ColorFromPalette(heatPal, colorIndex)); 294 | } 295 | } 296 | } 297 | 298 | void whitePeak(int band) { 299 | int xStart = BAR_WIDTH * band; 300 | int peakHeight = TOP - peak[band] - 1; 301 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 302 | matrix->drawPixel(x, peakHeight, CHSV(0,0,255)); 303 | } 304 | } 305 | 306 | void outrunPeak(int band) { 307 | int xStart = BAR_WIDTH * band; 308 | int peakHeight = TOP - peak[band] - 1; 309 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 310 | matrix->drawPixel(x, peakHeight, ColorFromPalette(outrunPal, peakHeight * (255 / kMatrixHeight))); 311 | } 312 | } 313 | 314 | void waterfall(int band) { 315 | int xStart = BAR_WIDTH * band; 316 | double highestBandValue = 60000; // Set this to calibrate your waterfall 317 | 318 | // Draw bottom line 319 | for (int x = xStart; x < xStart + BAR_WIDTH; x++) { 320 | matrix->drawPixel(x, 0, CHSV(constrain(map(bandValues[band],0,highestBandValue,160,0),0,160), 255, 255)); 321 | } 322 | 323 | // Move screen up starting at 2nd row from top 324 | if (band == NUM_BANDS - 1){ 325 | for (int y = kMatrixHeight - 2; y >= 0; y--) { 326 | for (int x = 0; x < kMatrixWidth; x++) { 327 | int pixelIndexY = matrix->XY(x, y + 1); 328 | int pixelIndex = matrix->XY(x, y); 329 | leds[pixelIndexY] = leds[pixelIndex]; 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /FFT.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-marley/ESP32_FFT_VU/4e02f27e51151c9b6750c5825d8f17e1a19116d1/FFT.xlsx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software, the ideas and concepts is Copyright (c) David Bird 2014 and beyond. 2 | 3 | All rights to this software are reserved. 4 | It is prohibited to redistribute or reproduce of any part or all of the software contents in any form other than the following: 5 | 6 | You may print or download to a local hard disk extracts for your personal and non-commercial use only. 7 | You may copy the content to individual third parties for their personal use, but only if you acknowledge the author David Bird as the source of the material. 8 | You may not, except with my express written permission, distribute or commercially exploit the content. 9 | You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 10 | 5. You MUST include all of this copyright and permission notice ('as annotated') and this shall be included in all copies or substantial or minor portions of the software and where the software use is visible to an end-user. 11 | THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. 12 | 13 | FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | 15 | IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /Linein_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-marley/ESP32_FFT_VU/4e02f27e51151c9b6750c5825d8f17e1a19116d1/Linein_bb.png -------------------------------------------------------------------------------- /Microphone_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-marley/ESP32_FFT_VU/4e02f27e51151c9b6750c5825d8f17e1a19116d1/Microphone_bb.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32_FFT_VU 2 | A spectrum analyzer VU meter running a 40kHz FFT on an ESP32 and outputting to a 16 x 16 FastLED matrix. The code is heavily modified from [this example](https://github.com/G6EJD/ESP32-8266-Audio-Spectrum-Display/blob/master/ESP32_Spectrum_Display_02.ino) originally written by G6EJD. 3 | 4 | ## Demo 5 | If you are planning on using this code, it is advisable to watch the YouTube video below explaining how it works. Please note, the code has changed to use FastLED Neomatrix since the video was produced thanks to [VonHirsch](https://github.com/VonHirsch/). See 'Installation and code usage' below. 6 | 7 | [![Youtube video of VU meter in action](http://img.youtube.com/vi/Mgh2WblO5_c/0.jpg)](https://www.youtube.com/watch?v=Mgh2WblO5_c) 8 | 9 | ## Setting up the circuit 10 | The matrix will run from both a line input or microphone, so choose whichever setup works for you. When choosing the pin for your button, be careful. Some pins on the ESP32 don't have pullup resistors, and if you choose one of these the button won't work! When I was testing, I found that D2 didn't work, but D4 did so I went with that one. 11 | 12 | ### Line in 13 | ![Circuit for using a line in](Linein_bb.png) 14 | 15 | The incoming stereo signal is first converted to mono using two 10K resistors, then through a 100nF capacitor to block DC. The signal is then biased by two 100k resistors to 3.3V / 2 = 1.65V to be read by the ADC. None of these values are critical. The resistors should be 10k or above and each pair should match. I've tried capacitors from 100nF to 10uF and haven't noticed any difference in the results. Pin D5 is the LED data pin and is connected to the first LED in the matrix. Pin D2 is connected to a momentary push button which is used to change display modes. 16 | 17 | ### Microphone 18 | ![Circuit for using a microphone](Microphone_bb.png) 19 | 20 | This is much simpler than the line in method, but you will be limited to the frequencies that you can detect by the sensititivy of the microphone. You need a mic with on-board amplification, commonly something like the MAX4466 which can be bought cheaply from eBay or alixepress. You would get better results with an I2S microphone, but that is beyond the scope of this project. Turn up the gain on whatever microphone you have. The mic should be connected to GND, 3V3 and the output pin to D35. Pin D5 is the LED data pin and is connected to the first LED in the matrix. Pin D2 is connected to a momentary push button which is used to change display modes. 21 | 22 | ## Installation and code usage 23 | 1. Download this repository and open ESP32_FFT_VU.ino. 24 | 2. You will need the FastLED Neomatrix and EasyButton libraries from the Arduino library manager. Youw will also need the arduinoFFT library. At the time of writing, the library manager version arduinoFFT has a bug which prevents `DCRemoval()` from working, so download it from the [GitHub repository](https://github.com/kosme/arduinoFFT) and install it from a zip file. 25 | 3. Watch the video to see how to use it. 26 | 4. To customsise it to your own matrix layout, read about Neomatrix layouts at [Adafruit](https://learn.adafruit.com/adafruit-neopixel-uberguide/neomatrix-library#layouts-2894555-5). 27 | 28 | ## Controls 29 | The matrix is controlled from a single button. The functions are: 30 | - Single press: Change pattern 31 | - Long press: Change brightness 32 | - 3 button presses in 2 seconds: Set to auto-change pattern 33 | - 5 button presses in 2 seconds: Turn off display 34 | 35 | ## Licence 36 | Due to a [copyright claim](https://github.com/s-marley/ESP32_FFT_VU/issues/12) by G6EJD, the MIT license for this project has been withdrawn, and his licence now applies. This can be found in LICENCE.txt. As I would like this code to be MIT licenced going forward, any code that originated with G6EJD will be replaced at a future date and the information on this page updated to reflect this. 37 | -------------------------------------------------------------------------------- /Sample_test/Sample_test.ino: -------------------------------------------------------------------------------- 1 | #define AUDIO_IN_PIN 35 2 | 3 | int analogValue; 4 | unsigned long newTime; 5 | 6 | void setup() { 7 | Serial.begin(115200); 8 | } 9 | 10 | void loop() { 11 | newTime = micros(); 12 | 13 | // Do 1 million reads and record time taken 14 | for (int i = 0; i < 1000000; i++) { 15 | analogValue = analogRead(AUDIO_IN_PIN); 16 | } 17 | 18 | float conversionTime = (micros() - newTime) / 1000000.0; 19 | 20 | Serial.print("Conversion time: "); 21 | Serial.print(conversionTime); 22 | Serial.println(" uS"); 23 | 24 | Serial.print("Max sampling frequency: "); 25 | Serial.print((1.0 / conversionTime) * 1000000); 26 | Serial.println(" Hz"); 27 | } 28 | --------------------------------------------------------------------------------