#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Encoder.h>
#include <arduinoFFT.h>
// --- CONFIGURACIÓN DE PANTALLA OLED ---
// --- SCL=22, SDA=21
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- CONFIGURACIÓN DE PINES DE ENCODERS ---
#define VOL_CLK_PIN 12
#define VOL_DT_PIN 13
#define GAIN_CLK_PIN 1 //TX
#define GAIN_DT_PIN 3 //RX
#define HIGH_CLK_PIN 34
#define HIGH_DT_PIN 35
#define MID_HIGH_CLK_PIN 32
#define MID_HIGH_DT_PIN 33
#define MID_LOW_CLK_PIN 25
#define MID_LOW_DT_PIN 26
#define LOW_CLK_PIN 27
#define LOW_DT_PIN 14
// --- PINES CHIP SELECT (CS) PARA POTENCIÓMETROS MCP4231 ---
// --- Pines SPI: MOSI=23, MISO=19, SCK=18
#define VOL_CS_PIN 15
#define GAIN_CS_PIN 5 //CS/SS
#define HIGH_CS_PIN 17 //TX2
#define MID_HIGH_CS_PIN 16 //RX2
#define MID_LOW_CS_PIN 4
#define LOW_CS_PIN 2
// --- PINES DE ENTRADA DE AUDIO PARA FFT (ANALIZADOR DE ESPECTRO) ---
#define AUDIO_IN_L 36 // Canal Izquierdo (ADC1_CH0)
#define AUDIO_IN_R 39 // Canal Derecho (ADC1_CH3)
// --- RANGOS DE DECIBELIOS (dB) ---
const int VOL_MIN = -80, VOL_MAX = 0;
const int GAIN_MIN = -20, GAIN_MAX = 20;
const int EQ_MIN = -15, EQ_MAX = 15;
// --- CONSTANTES DE POTENCIÓMETRO DIGITAL ---
const int POT_MAX_STEPS = 255; // MCP4231 tiene 256 pasos (0-255)
// --- CONSTANTES DEL ENCODER ---
#define ENCODER_STEPS_PER_DB 2
// --- OBJETOS ENCODER ---
ESP32Encoder encoderVol;
ESP32Encoder encoderGain;
ESP32Encoder encoderHigh;
ESP32Encoder encoderMidHigh;
ESP32Encoder encoderMidLow;
ESP32Encoder encoderLow;
// --- VARIABLES DE ESTADO PARA CADA PARÁMETRO (en dB) ---
int currentVolDB = 0;
int currentGainDB = 0;
int currentHighDB = 0;
int currentMidHighDB = 0;
int currentMidLowDB = 0;
int currentLowDB = 0;
// --- PARÁMETROS DE LA FFT ---
#define SAMPLES 128
#define SAMPLING_FREQUENCY 40000.0
#define AMPLITUDE 1500
double vRealL[SAMPLES], vImagL[SAMPLES];
double vRealR[SAMPLES], vImagR[SAMPLES];
ArduinoFFT<double> fftL = ArduinoFFT<double>(vRealL, vImagL, SAMPLES, SAMPLING_FREQUENCY);
ArduinoFFT<double> fftR = ArduinoFFT<double>(vRealR, vImagR, SAMPLES, SAMPLING_FREQUENCY);
// --- VARIABLES PARA CAMBIO DE MODO DE PANTALLA ---
#define DISPLAY_TIMEOUT_MS 3000 // 3 segundos para volver al analizador
unsigned long lastInteractionTime = 0; // Tiempo de la última interacción
bool isInParameterMode = true; // Flag: true=muestra parámetros, false=muestra analizador
// --- PROTOTIPOS DE FUNCIONES ---
void writeToPot(int csPin, byte address, byte value);
void setPotValue(int csPin, int dbValue, int dbMin, int dbMax);
void displayParameter(const char *paramName, int currentValue, int minValue, int maxValue);
void displaySpectrumAnalyzer();
void setup() {
//Serial.begin(115200);
SPI.begin();
// --- INICIALIZACIÓN DE PINES CS PARA POTENCIÓMETROS ---
pinMode(VOL_CS_PIN, OUTPUT);
pinMode(GAIN_CS_PIN, OUTPUT);
pinMode(HIGH_CS_PIN, OUTPUT);
pinMode(MID_HIGH_CS_PIN, OUTPUT);
pinMode(MID_LOW_CS_PIN, OUTPUT);
pinMode(LOW_CS_PIN, OUTPUT);
digitalWrite(VOL_CS_PIN, HIGH);
digitalWrite(GAIN_CS_PIN, HIGH);
digitalWrite(HIGH_CS_PIN, HIGH);
digitalWrite(MID_HIGH_CS_PIN, HIGH);
digitalWrite(MID_LOW_CS_PIN, HIGH);
digitalWrite(LOW_CS_PIN, HIGH);
// --- INICIALIZACIÓN DE ENCODERS ---
encoderVol.attachHalfQuad(VOL_DT_PIN, VOL_CLK_PIN);
encoderVol.setCount(VOL_MIN * ENCODER_STEPS_PER_DB);
currentVolDB = VOL_MIN;
encoderGain.attachHalfQuad(GAIN_DT_PIN, GAIN_CLK_PIN);
encoderGain.setCount(0 * ENCODER_STEPS_PER_DB);
currentGainDB = 0;
encoderHigh.attachHalfQuad(HIGH_DT_PIN, HIGH_CLK_PIN);
encoderHigh.setCount(0 * ENCODER_STEPS_PER_DB);
currentHighDB = 0;
encoderMidHigh.attachHalfQuad(MID_HIGH_DT_PIN, MID_HIGH_CLK_PIN);
encoderMidHigh.setCount(0 * ENCODER_STEPS_PER_DB);
currentMidHighDB = 0;
encoderMidLow.attachHalfQuad(MID_LOW_DT_PIN, MID_LOW_CLK_PIN);
encoderMidLow.setCount(0 * ENCODER_STEPS_PER_DB);
currentMidLowDB = 0;
encoderLow.attachHalfQuad(LOW_DT_PIN, LOW_CLK_PIN);
encoderLow.setCount(0 * ENCODER_STEPS_PER_DB);
currentLowDB = 0;
// --- CONFIGURAR ESTADO INICIAL DE POTENCIÓMETROS ---
setPotValue(VOL_CS_PIN, currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(GAIN_CS_PIN, currentGainDB, GAIN_MIN, GAIN_MAX);
setPotValue(HIGH_CS_PIN, currentHighDB, EQ_MIN, EQ_MAX);
setPotValue(MID_HIGH_CS_PIN, currentMidHighDB, EQ_MIN, EQ_MAX);
setPotValue(MID_LOW_CS_PIN, currentMidLowDB, EQ_MIN, EQ_MAX);
setPotValue(LOW_CS_PIN, currentLowDB, EQ_MIN, EQ_MAX);
// --- INICIALIZACIÓN DE LA PANTALLA OLED ---
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(15, 24);
display.println("PATRO DJ");
display.display();
delay(1000);
// Forzar primera actualización y arrancar en modo parámetros (mostrando volumen)
displayParameter("VOLUMEN", currentVolDB, VOL_MIN, VOL_MAX);
isInParameterMode = true;
lastInteractionTime = millis();
}
void loop() {
bool interactionDetected = false;
// --- LECTURA DE ENCODERS ---
// 1. VOLUMEN
long newVol = encoderVol.getCount() / ENCODER_STEPS_PER_DB;
newVol = constrain(newVol, VOL_MIN, VOL_MAX);
if (newVol != currentVolDB) {
currentVolDB = newVol;
encoderVol.setCount(currentVolDB * ENCODER_STEPS_PER_DB);
displayParameter("VOLUMEN", currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(VOL_CS_PIN, currentVolDB, VOL_MIN, VOL_MAX);
interactionDetected = true;
}
// 2. GAIN
long newGain = encoderGain.getCount() / ENCODER_STEPS_PER_DB;
newGain = constrain(newGain, GAIN_MIN, GAIN_MAX);
if (newGain != currentGainDB) {
currentGainDB = newGain;
encoderGain.setCount(currentGainDB * ENCODER_STEPS_PER_DB);
displayParameter("GAIN", currentGainDB, GAIN_MIN, GAIN_MAX);
setPotValue(GAIN_CS_PIN, currentGainDB, GAIN_MIN, GAIN_MAX);
interactionDetected = true;
}
// 3. HIGH
long newHigh = encoderHigh.getCount() / ENCODER_STEPS_PER_DB;
newHigh = constrain(newHigh, EQ_MIN, EQ_MAX);
if (newHigh != currentHighDB) {
currentHighDB = newHigh;
encoderHigh.setCount(currentHighDB * ENCODER_STEPS_PER_DB);
displayParameter("HIGH", currentHighDB, EQ_MIN, EQ_MAX);
setPotValue(HIGH_CS_PIN, currentHighDB, EQ_MIN, EQ_MAX);
interactionDetected = true;
}
// 4. MID HIGH
long newMidHigh = encoderMidHigh.getCount() / ENCODER_STEPS_PER_DB;
newMidHigh = constrain(newMidHigh, EQ_MIN, EQ_MAX);
if (newMidHigh != currentMidHighDB) {
currentMidHighDB = newMidHigh;
encoderMidHigh.setCount(currentMidHighDB * ENCODER_STEPS_PER_DB);
displayParameter("MID HIGH", currentMidHighDB, EQ_MIN, EQ_MAX);
setPotValue(MID_HIGH_CS_PIN, currentMidHighDB, EQ_MIN, EQ_MAX);
interactionDetected = true;
}
// 5. MID LOW
long newMidLow = encoderMidLow.getCount() / ENCODER_STEPS_PER_DB;
newMidLow = constrain(newMidLow, EQ_MIN, EQ_MAX);
if (newMidLow != currentMidLowDB) {
currentMidLowDB = newMidLow;
encoderMidLow.setCount(currentMidLowDB * ENCODER_STEPS_PER_DB);
displayParameter("MID LOW", currentMidLowDB, EQ_MIN, EQ_MAX);
setPotValue(MID_LOW_CS_PIN, currentMidLowDB, EQ_MIN, EQ_MAX);
interactionDetected = true;
}
// 6. LOW
long newLow = encoderLow.getCount() / ENCODER_STEPS_PER_DB;
newLow = constrain(newLow, EQ_MIN, EQ_MAX);
if (newLow != currentLowDB) {
currentLowDB = newLow;
encoderLow.setCount(currentLowDB * ENCODER_STEPS_PER_DB);
displayParameter("LOW", currentLowDB, EQ_MIN, EQ_MAX);
setPotValue(LOW_CS_PIN, currentLowDB, EQ_MIN, EQ_MAX);
interactionDetected = true;
}
// --- GESTIÓN DE MODOS DE PANTALLA ---
// Si hubo alguna interacción, reinicia el temporizador y activa el modo de parámetros
if (interactionDetected) {
lastInteractionTime = millis();
if (!isInParameterMode) {
isInParameterMode = true;
// La pantalla ya fue actualizada al detectar el cambio en el encoder
}
}
// Comprueba si ha pasado el tiempo de espera para cambiar a modo analizador
if (isInParameterMode && (millis() - lastInteractionTime > DISPLAY_TIMEOUT_MS)) {
isInParameterMode = false;
}
// Dibuja en la pantalla según el modo actual
if (isInParameterMode) {
// No hacer nada, la pantalla ya muestra el último parámetro modificado.
} else {
// Si no estamos en modo parámetros, muestra el analizador de espectro.
displaySpectrumAnalyzer();
}
delay(10); // Pequeño delay para estabilidad
}
// Envía un comando de escritura al MCP4231
void writeToPot(int csPin, byte address, byte value) {
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
SPI.transfer(address);
SPI.transfer(value);
digitalWrite(csPin, HIGH);
SPI.endTransaction();
}
// Convierte dB a valor de potenciómetro y lo envía
void setPotValue(int csPin, int dbValue, int dbMin, int dbMax) {
long potStep = map(dbValue, dbMin, dbMax, POT_MAX_STEPS, 0);
potStep = constrain(potStep, 0, POT_MAX_STEPS);
// Escribir en ambos potenciómetros del chip (wiper 0 y wiper 1)
writeToPot(csPin, 0x00, (byte)potStep); // Dirección del wiper 0
writeToPot(csPin, 0x10, (byte)potStep); // Dirección del wiper 1
}
// Dibuja la pantalla de un parámetro (Vol, Gain, EQ)
void displayParameter(const char *paramName, int currentValue, int minValue, int maxValue) {
display.clearDisplay();
// 1. Dibuja el nombre y el valor numérico
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(paramName, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 2);
display.println(paramName);
char dbStr[10];
sprintf(dbStr, "%+d dB", currentValue);
display.getTextBounds(dbStr, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 20);
display.print(dbStr);
// 2. Dibuja la barra de progreso
int barWidth = map(currentValue, minValue, maxValue, 0, SCREEN_WIDTH);
display.drawRect(0, 42, SCREEN_WIDTH, 16, SSD1306_WHITE);
if (minValue < 0 && maxValue > 0) { // Para barras con centro en 0 (Gain, EQ)
int zeroPoint = map(0, minValue, maxValue, 0, SCREEN_WIDTH);
display.drawFastVLine(zeroPoint, 40, 20, SSD1306_WHITE);
if (barWidth > zeroPoint) {
display.fillRect(zeroPoint, 42, barWidth - zeroPoint, 16, SSD1306_WHITE);
} else {
display.fillRect(barWidth, 42, zeroPoint - barWidth, 16, SSD1306_WHITE);
}
} else { // Para barras que empiezan en un mínimo (Volumen)
display.fillRect(0, 42, barWidth, 16, SSD1306_WHITE);
}
// 3. Muestra todo en la pantalla
display.display();
}
// Realiza la FFT y dibuja el analizador de espectro estéreo
void displaySpectrumAnalyzer() {
// 1. Muestreo de los dos canales de audio
for (int i = 0; i < SAMPLES; i++) {
vRealL[i] = (double)analogRead(AUDIO_IN_L) - 2048.0; // Restar offset DC (asumiendo VCC/2)
vImagL[i] = 0.0;
vRealR[i] = (double)analogRead(AUDIO_IN_R) - 2048.0;
vImagR[i] = 0.0;
}
// 2. Procesamiento FFT para ambos canales
fftL.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
fftL.compute(FFT_FORWARD);
fftL.complexToMagnitude();
fftR.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
fftR.compute(FFT_FORWARD);
fftR.complexToMagnitude();
// 3. Visualización en la Pantalla OLED
display.clearDisplay();
int displayBins = SAMPLES / 2; // Solo se muestra la primera mitad de los resultados de la FFT
int barHeight = (SCREEN_HEIGHT / 2) - 1; // Altura máxima para cada canal
for (int i = 2; i < displayBins; i++) { // Empezamos en i=2 para ignorar el componente DC
// Canal Izquierdo (arriba)
int magnitudeL = vRealL[i] / AMPLITUDE;
magnitudeL = constrain(magnitudeL, 0, barHeight);
display.drawLine(i * 2, barHeight, i * 2, barHeight - magnitudeL, SSD1306_WHITE);
// Canal Derecho (abajo)
int magnitudeR = vRealR[i] / AMPLITUDE;
magnitudeR = constrain(magnitudeR, 0, barHeight);
display.drawLine(i * 2, barHeight + 1, i * 2, barHeight + 1 + magnitudeR, SSD1306_WHITE);
}
display.display();
}