Hi,
I am trying to integrate an OLED display spectrum analyzer and UI control with three buttons and LED strip to display patterns. I am using an esp32 devkit, ssd1309 128x64 OLED display and WS2812B leds with 3 pushbuttons and microphone input.
I have fumbled my way through examples and got the UI controls and spectrum analyzer working fine but I am having trouble integrating patterns and the second part is to make them react to the beat using the values gathered from the FFT.
Basically I want to draw a horizontal line across the display and trigger a beat detection when the spectrum plots a value over the line. this beat detect will be used by the different pattern functions.
My first step is to integrate Fastled correctly and have basic patterns working before I try to get too fancy and have them react to sound.
Currently the OLED display doesn't work the moment I try to add a basic pattern MoveDotLeft() and I dont understand why.
Can anyone please help me get an understanding on:
- how to implement patterns correctly
- how to detect the "beat" across the frequency range
- use the beat to influence patterns
here is my code so far:
#include <FastLED.h>
#include <esp_task_wdt.h> //https://iotassistant.io/esp32/enable-hardware-watchdog-timer-esp32-arduino-ide/
#include <Wire.h>
#include "arduinoFFT.h" // Standard Arduino FFT library // https://github.com/kosme/arduinoFFT,
#include "SSD1306.h" // https://github.com/squix78/esp8266-oled-ssd1306
//#include "Button2.h"; // https://github.com/LennartHennigs/Button2
//#include "ESPRotary.h";
arduinoFFT FFT = arduinoFFT();
/////////////////////////////////////////////////////////////////////////
// OLED Display
#define SDA 21
#define SCL 22
#define OLED_i2c_address 0x3c
SSD1306 display(OLED_i2c_address,SDA,SCL); // 0.96" OLED display object definition (address, SDA, SCL) Connect OLED SDA , SCL pins to ESP SDA, SCL pins
// FFT
#define SAMPLES 512 // Must be a power of 2 // 512
#define SAMPLING_FREQUENCY 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.
//#define amplitude 800 // Depending on your audio source level, you may need to increase this value 200
// WDT
#define WDT_TIMEOUT 3 // 3 seconds WDT. If the code freezes for 3 seconds the hardware watchdog will reset the device.
// LEDS
#define OnboardLED 2 // Define the Onbaoard LED for debugging (most likely will blink for watchdog)
#define ExternalLED 23 // Define the External LED for debugging and fancy stuff
//FASTLED
#define LEDstripPin 4
#define NUM_LEDS 196
#define COLOR_ORDER GRB
#define CHIPSET WS2812B
CRGB leds[NUM_LEDS];
bool LEDpower = true ; // LED strip is on by default
// LED Brightness
int LED_Brightness = 50; // Stores the default brightness of the LED strip
int LED_BrightnessMin = 0 ; // let the brightness vary between 0 and 100%
int LED_BrightnessMax =100 ;
int ledDisplayBrihtnessValue = 0; // variable to store actual LED strip brightness that is mapped from 0-100%
// ANALOG INPUTS
#define MICROPHONE_INPUT_PIN A0 // Define microphone input pin ADC 0
#define POTENTIOMETER_INPUT_PIN A6 // Define microphone input pin ADC 0
// BUTTON INPUTS
#define IncreaseButtonPin 19
#define ModeButtonPin 18
#define DecreaseButtonPin 5
//// ROTARY ENCODER // rotary encoders suck for this application.
//#define ROTARY_PIN1 18
//#define ROTARY_PIN2 19
//#define BUTTON_PIN 5
//#define CLICKS_PER_STEP 2 // this number depends on your rotary encoder
//ESPRotary Rotary = ESPRotary(ROTARY_PIN1, ROTARY_PIN2, CLICKS_PER_STEP);
//Button2 Rotary_BTN = Button2(BUTTON_PIN);
// MODE
int Mode = 1; // stores the current mode
int ModeMax = 6; // 1 = sensitivity, 2 = Pattern, 3 = Brightness
// Pattern selection
int Pattern = 1; // Stores the current Pattern
int PatternMax = 5; // adjust this for number of patterns
int PatternMin = 1; // keep this 1 so patterns cycle in a loop
// timer for pattern mode change
int Pattern_period = 30000; // this is the time period between pattern changes
unsigned long time_now = 0; // variable for millis() to store the current time
// OLED Display brightness
int OLED_Brightness = 50; // sets the default brightness
int OLED_BrightnessMin = 0 ; // let the brightness vary between 0 and 100%
int OLED_BrightnessMax =100 ;
int OledDisplayBrihtnessValue = 0; // variable to store actual value that is mapped from 0-100%
int PatternMode = 1; // pattern mode, cycle, single, random
int PatternModeMax = 3;
int PatternModeMin = 1;
// WATCHDOG TIMER
int WDT_i = 0; // watchdog timer itteration counter for debug
int WDT_last = millis(); // watchdog timer save current value of millis to start WDT accuratly
//SENSITIVITY ADJUSTMENT FOR MIC INPUT
int amplitude = 70; // amplitude stores the multiplier for the microphone input signal start at 50% can be adjusted by 10K pot or mode button...
int amplitudeMax = 100; //
int amplitudeMin = 5; // defines the minimum value for amplitude... an attempt to stop it from flatlining
int amplitudePercentage = 50; // variables to calucalte ADC as a percentage
int amplitudePercentage_round = 0; // place to store the amplitude
int OledDisplayOffsetMin = 100;
int OledDisplayOffsetMax = 10000;
int OledDisplayOffset = 100;
// BEAT DETECTION
int BeatThreshold = 20;
unsigned int sampling_period_us; // variables for FFT calculations
unsigned long microseconds;
byte peak[] = {0,0,0,0,0,0,0};
double vReal[SAMPLES];
double vImag[SAMPLES];
unsigned long newTime, oldTime;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
Serial.begin(115200);
//Wire.begin(5,4); // SDA, SCL
pinMode(MICROPHONE_INPUT_PIN, INPUT);
pinMode(POTENTIOMETER_INPUT_PIN, INPUT);
pinMode(IncreaseButtonPin, INPUT_PULLUP);
pinMode(ModeButtonPin, INPUT_PULLUP);
pinMode(DecreaseButtonPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(IncreaseButtonPin), Increase, FALLING); //CHANGE , RISING , FALLING LOW HIGH
attachInterrupt(digitalPinToInterrupt(ModeButtonPin), ChangeMode, FALLING);
attachInterrupt(digitalPinToInterrupt(DecreaseButtonPin), Decrease, FALLING);
pinMode(OnboardLED, OUTPUT);
pinMode(ExternalLED, OUTPUT);
//pinMode(LEDstrip, OUTPUT);
//FastLED.addLeds<CHIPSET, LEDstripPin, COLOR_ORDER>(leds[0], leds.Size());
FastLED.addLeds<WS2812B,LEDstripPin , GRB>(leds, NUM_LEDS);
FastLED.setBrightness(100);
display.init();
display.setFont(ArialMT_Plain_10);
display.flipScreenVertically(); // Adjust to suit or remove
sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQUENCY));
//OledDisplayOffset = (OledDisplayOffsetMax - OledDisplayOffsetMin)/2 ;
StartupOK(); // acknowlege startup and blink onboard LED
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
ResetWatchdog();
digitalWrite(OnboardLED, LOW); // turn the LED off after watchdog reset turns it on
display.clear();
//Serial.print("amplitude = ");
//Serial.println(amplitude);
//display.setTextAlignment(TEXT_ALIGN_CENTER);
//display.drawHorizontalLine(10,30,40);
//setSensitivity(); // read pot input, calculate sensitity and display value. NOTE: only use this if 10K pot connected.
ChangePattern(); // select Cycle, single or random and
CalculateOLEDoffset(); // map display values 0-100% to
OLEDBrightness(); // set the brightness of the OLED display
display_mode(); //
sampleMicInput(); // read microphone input and populate vReal array with samples.
calculateFFT(); // perform FFT functions
drawFFTtoDisplay(); // draw the VU meter values to display
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS
//////////////////////////////////////////////////////////////////////////////
void calculateFFT(){
FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
}
//////////////////////////////////////////////////////////////////////////////
void drawFFTtoDisplay(){ // these values may not be accurate because sample frequency delay
for (int i = 2; i < (SAMPLES/2); i++){ // Don't use sample 0 and only first SAMPLES/2 are usable. Each array eleement represents a frequency and its value the amplitude.
if (vReal[i] > 2000) { // Add a crude noise filter, 10 x amplitude or more
if (i<=2 ) displayBand(0,(int)vReal[i]/OledDisplayOffset); // 125Hz
if (i >3 && i<=5 ) displayBand(1,(int)vReal[i]/OledDisplayOffset); // 250Hz
if (i >5 && i<=7 ) displayBand(2,(int)vReal[i]/OledDisplayOffset); // 500Hz
if (i >7 && i<=15 ) displayBand(3,(int)vReal[i]/OledDisplayOffset); // 1000Hz
if (i >15 && i<=30 ) displayBand(4,(int)vReal[i]/OledDisplayOffset); // 2000Hz
if (i >30 && i<=53 ) displayBand(5,(int)vReal[i]/OledDisplayOffset); // 4000Hz
if (i >53 && i<=200 ) displayBand(6,(int)vReal[i]/OledDisplayOffset); // 8000Hz
//if (i >200 ) displayBand(7,(int)vReal[i]/amplitude); // 16000Hz // this looks crap on the display so ignore.
//Serial.println(i);
}
for (byte band = 0; band <= 6; band++){
display.drawHorizontalLine(18*band,64-peak[band],14);
}
}
if (millis()%2 == 0) { // Decay the peak// if (millis()%4 == 0) {for (byte band = 0; band <= 6; band++) {if (peak[band] > 0) peak[band] -= 1;}} // Decay the peak
for (byte band = 0; band <= 6; band++) {
if (peak[band] > 0) peak[band] -= 1;
}
}
display.display();
}
//////////////////////////////////////////////////////////////////////////////
//void setSensitivity(){ //use this if 10K potentiometer added instead of using buttons...
//// amplitude = analogRead(POTENTIOMETER_INPUT_PIN); // read sensitivity value from 10K potentiometer NOTE: sensitivity could come from mode buttons so comment this out if no pot attached
//
// amplitudePercentage = 100-((amplitude/4095)*100); // calculate percentage
// Serial.print("amplitude Percentage = ");
// Serial.println(amplitudePercentage);
// amplitudePercentage_round = round(amplitudePercentage); //round value to integer
// display.drawString(0, 0, "Sensitivity: " + String(amplitudePercentage_round)+"%"); // print sensitivity
// }
//////////////////////////////////////////////////////////////////////////////
void sampleMicInput(){
for (int i = 0; i < SAMPLES; i++) {
newTime = micros()-oldTime;
oldTime = newTime;
vReal[i] = analogRead(MICROPHONE_INPUT_PIN); // A conversion takes about 1uS on an ESP32
vImag[i] = 0;
while (micros() < (newTime + sampling_period_us)) { /* do nothing to wait */ }
}
}
//////////////////////////////////////////////////////////////////////////////
void displayBand(int band, int dsize){
int dmax = 50;
if (dsize > dmax){
dsize = dmax;
}
if (band == 7){
display.drawHorizontalLine(18*6,0, 14);
}
for (int s = 0; s <= dsize; s=s+2){
display.drawHorizontalLine(18*band,64-s, 14);
}
if (dsize > peak[band]) {
peak[band] = dsize;
}
}
//////////////////////////////////////////////////////////////////////////////
void ResetWatchdog(){
if (millis() - WDT_last >= 2000) { // if (millis() - WDT_last >= 2000 && WDT_i < 5)
digitalWrite(OnboardLED, HIGH); // turn the LED ON
Serial.println("Resetting WDT... all OK"); // serial print for debug
esp_task_wdt_reset(); //reset the hardware watchdog timer
WDT_last = millis();
WDT_i++;
Serial.print("watchdog count = "); // serial print for debug
Serial.println(WDT_i); // serial print for debug
if (WDT_i >= 32767) { // prevent watchdog counter from overflowing and shooting itself in the foot.
WDT_i = 0; // reset to zero
}
}
}
//////////////////////////////////////////////////////////////////////////////
void StartupOK(){
for (int X = 0; X < 10; X++){
digitalWrite(OnboardLED, HIGH); // turn the INTERNAL LED on
digitalWrite(ExternalLED, HIGH); // turn the EXTERNAL LED on
delay(100); // delay
digitalWrite(OnboardLED, LOW); // turn the INTERNAL LED off
digitalWrite(ExternalLED, LOW); // turn the EXTERNAL LED off
delay(50); // delay
}
Serial.println("STARTUP OK"); // serial print for debug
}
//////////////////////////////////////////////////////////////////////////////
void Increase(){
//Serial.println("INCREASE"); // serial print for debug
switch (Mode){
case 1: //mode = sensitivity
Pattern++;
if (Pattern > PatternMax){ // dont let ADC = 0 mess up calculation
Pattern = PatternMin;
}
break;
case 2: //mode = Pattern
PatternMode++;
if (PatternMode > PatternModeMax){ // dont let ADC = 0 mess up calculation
PatternMode = PatternModeMin;
}
break;
case 3: //mode = Pattern
amplitude = amplitude +5;
if (amplitude > amplitudeMax){ // dont let ADC = 0 mess up calculation
amplitude = amplitudeMax;
}
break;
case 4: //mode = Display brightness
LED_Brightness = LED_Brightness +5;
if (LED_Brightness > 100){ //
LED_Brightness = 100;
}
break;
case 5: //mode = LED power
LEDpower = !LEDpower;
break;
case 6:
OLED_Brightness = OLED_Brightness +5;
if (OLED_Brightness > 100){ // dont let ADC = 0 mess up calculation
OLED_Brightness = 100;
}
break;
}
}
//////////////////////////////////////////////////////////////////////////////
void ChangePattern(){
switch (PatternMode){
case 1: //cycle
if(millis() >= time_now + Pattern_period){
time_now += Pattern_period;
//Serial.println("Cycle"); //serial print for debug
Pattern++;
if (Pattern > PatternMax){
Pattern = PatternMin;
}
}
break;
default:
case 2: //Single
//Serial.println("Single"); //serial print for debug
break;
case 3: // Random
if(millis() >= time_now + Pattern_period){
//Serial.println("Random"); //serial print for debug
time_now += Pattern_period;
Pattern = random(PatternMin,PatternMax);
}
break;
}
}
//////////////////////////////////////////////////////////////////////////////
void ChangeMode(){
Mode++ ;
}
//////////////////////////////////////////////////////////////////////////////
void Decrease(){ // DECREASE
//Serial.println("DECREASE"); // serial print for debug
switch (Mode){
case 1: //mode = Pattern
Pattern--;
if (Pattern < PatternMin){ // make sure pattern wraps around and doesnt go negative
Pattern = PatternMax;
}
break;
case 2: //mode =
PatternMode--;
if (PatternMode < PatternModeMin){ // make sure pattern wraps around and doesnt go negative
PatternMode = PatternModeMax;
}
break;
case 3: //mode = OLED brightness
amplitude = amplitude -5;
if (amplitude < amplitudeMin){ // dont let amplidute overflow to zero
amplitude = amplitudeMin;
}
break;
case 4: //mode = LED brightness
LED_Brightness = LED_Brightness -5;
if (LED_Brightness < 0){
LED_Brightness = 0;
}
break;
case 5: //mode = LED power
LEDpower = !LEDpower;
break;
case 6: //mode = Pattern mode
OLED_Brightness = OLED_Brightness -5;
if (OLED_Brightness < 0){
OLED_Brightness = 0;
}
break;
}
}
//////////////////////////////////////////////////////////////////////////////
void display_mode(){ // MODE
if (Mode > ModeMax) Mode = 1;
switch (Mode){
case 1: // CASE 1 - Microphone Sensitivity
//Serial.println("MODE = 2");
display.clear();
switch(Pattern){
case 1:
display.drawString(0, 0, "Pattern: MoveDotLeft");
//MoveDotLeft(); // call pattern
break;
case 2:
display.drawString(0, 0, "Pattern: Sparkle");
break;
case 3:
display.drawString(0, 0, "Pattern: Snake");
break;
case 4:
display.drawString(0, 0, "Pattern: Rainbow");
break;
case 5:
display.drawString(0, 0, "Pattern: Stuff");
break;
}
//display.drawString(0, 0, "Pattern: " + String(Pattern));
break;
default:
case 2: // CASE 2 - Pattern
//Serial.println("MODE = 6");
switch (PatternMode){
case 1:
display.clear();
display.drawString(0, 0, "Pattern Mode: Cycle");
break;
case 2:
display.clear();
display.drawString(0, 0, "Pattern Mode: Single");
break;
case 3:
display.clear();
display.drawString(0, 0, "Pattern Mode: Random");
break;
}
break;
case 3: // CASE 3 - Display Brightness
//Serial.println("MODE = 1");
display.clear();
display.drawString(0, 0, "Mic Sensitivity: " + String(amplitude)+"%");
break;
case 4: // CASE 4 - LED Brightness
//Serial.println("MODE = 4");
display.clear();
display.drawString(0, 0, "LED Brightness: " + String(LED_Brightness)+"%");
break;
case 5: // CASE 5 - LEDS ON/OFF
//Serial.println("MODE = 5");
display.clear();
if (LEDpower == true){
display.drawString(0, 0, "LEDS ON/OFF: ON");
}else{
display.drawString(0, 0, "LEDS ON/OFF: OFF");
}
break;
case 6:
//Serial.println("MODE = 3");
display.clear();
display.drawString(0, 0, "Display Brightness: " + String(OLED_Brightness)+"%");
break;
}
}
//////////////////////////////////////////////////////////////////////////////
void OLEDBrightness(){
OledDisplayBrihtnessValue = map(OLED_Brightness,OLED_BrightnessMin,OLED_BrightnessMax,0,255);
if (OledDisplayBrihtnessValue < 0){
OledDisplayBrihtnessValue = 0;
}
if (OledDisplayBrihtnessValue > 255){
OledDisplayBrihtnessValue = 255;
}
display.setBrightness(OledDisplayBrihtnessValue);
}
//////////////////////////////////////////////////////////////////////////////
void LEDBrightness(){
ledDisplayBrihtnessValue= map(100-LED_Brightness,LED_BrightnessMin,LED_BrightnessMax,0,255);
if (ledDisplayBrihtnessValue < 0){
ledDisplayBrihtnessValue = 0;
}
if (ledDisplayBrihtnessValue > 255){
ledDisplayBrihtnessValue = 255;
}
}
//////////////////////////////////////////////////////////////////////////////
void CalculateOLEDoffset(){
OledDisplayOffset = map(100-amplitude,amplitudeMin,amplitudeMax,OledDisplayOffsetMin,OledDisplayOffsetMax);
if (OledDisplayOffset < OledDisplayOffsetMin){
OledDisplayOffset = OledDisplayOffsetMin;
}
if (OledDisplayOffset > OledDisplayOffsetMax){
OledDisplayOffset = OledDisplayOffsetMax;
}
}
//////////////////////////////////////////////////////////////////////////////
void MoveDotLeft(){
for(int dot = NUM_LEDS; dot > 0; dot--) {
leds[dot] = CRGB::Green;
FastLED.show();
// clear this led for the next time around the loop
leds[dot] = CRGB::Black;
//delay(100);
}
}
//////////////////////////////////////////////////////////////////////////////