// =========================================================================
// === LIBRERÍAS ===
// =========================================================================
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Encoder.h>
#include <math.h>
#include <BluetoothSerial.h>
#include <ArduinoJson.h>
#include "Update.h"
#include "esp_ota_ops.h"
#include <arduinoFFT.h>
#include <Adafruit_NeoPixel.h>
#include "BluetoothA2DP.h"
#include "esp_bt.h"
// =========================================================================
// === *** ARRAY DEL LOGO PERSONALIZADO *** ===
// =========================================================================
// Este array de bytes representa una imagen de 128x64 píxeles.
// Fue generado con una herramienta online (image2cpp) a partir de una imagen BMP.
const unsigned char patro_dj_logo_bmp[] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xf0, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x0f, 0xff,
0xff, 0xf0, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x0f, 0xff,
0xff, 0xf0, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x0f, 0xff,
0xff, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff,
0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff,
0xff, 0xf8, 0x00, 0x7f, 0xc3, 0x8f, 0x0c, 0x3c, 0x71, 0xe0, 0x0e, 0x00, 0x7c, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x7f, 0xc3, 0x8f, 0x0c, 0x0c, 0x71, 0xc7, 0x86, 0x1c, 0x3c, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x7f, 0xc3, 0x8f, 0x0c, 0x04, 0x71, 0x87, 0xc6, 0x00, 0x7c, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x78, 0xc3, 0x8f, 0x1c, 0x60, 0x71, 0xc7, 0xc6, 0x18, 0x3c, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x78, 0x07, 0x80, 0x1c, 0x70, 0x71, 0xc0, 0x0e, 0x1c, 0x3c, 0x00, 0x1f, 0xff,
0xff, 0xf8, 0x00, 0x7f, 0x1f, 0xf0, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xfc, 0x00, 0x1f, 0xff,
0xff, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff,
0xff, 0xfc, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x3f, 0xff,
0xff, 0xfc, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x3f, 0xff,
0xff, 0xfe, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x7f, 0xff,
0xff, 0xfe, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0x7f, 0xff,
0xff, 0xff, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0xff, 0xff,
0xff, 0xff, 0x00, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x00, 0xff, 0xff,
0xff, 0xff, 0x80, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x01, 0xff, 0xff,
0xff, 0xff, 0xc0, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x03, 0xff, 0xff,
0xff, 0xff, 0xe0, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x07, 0xff, 0xff,
0xff, 0xff, 0xf0, 0xff, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfe, 0x0f, 0xff, 0xff,
0xff, 0xff, 0xf8, 0x7f, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xfc, 0x1f, 0xff, 0xff,
0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xf8, 0x7f, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0f, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xf0, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xc7, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0xc3, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xe1, 0xe0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x07, 0x07, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf8, 0x60, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x06, 0x1f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0x20, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xe0, 0x03, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xe0, 0x03, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0xff, 0xe0, 0x03, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x1f, 0xe0, 0x03, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x60, 0x02, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
// =========================================================================
// === CONFIGURACIONES GLOBALES ===
// =========================================================================
// --- ESTADOS DEL PROGRAMA ---
enum MainProgramMode { MODE_PREAMP, MODE_VISUALIZER };
MainProgramMode mainProgramMode = MODE_VISUALIZER;
int visualizerSubMode = 1;
const int TOTAL_VISUALIZER_MODES = 5;
unsigned long lastVisualizerActivity = 0;
#define VISUALIZER_TIMEOUT_MS 5000
enum OledParamState { OLED_SHOWING_MAIN, OLED_SHOWING_PARAM, OLED_SHOWING_SOURCE };
OledParamState oledParamState = OLED_SHOWING_MAIN;
// --- SELECTOR DE ENTRADA ---
enum AudioSource { SOURCE_BLUETOOTH, SOURCE_MIXER, SOURCE_AUX };
AudioSource currentAudioSource = SOURCE_BLUETOOTH;
#define SELECT_BUTTON_PIN 15
#define LONG_PRESS_HOLD_MS 3000
#define DOUBLE_PRESS_WINDOW_MS 400
//#define BOOT_PIN 0
// --- BLUETOOTH ---
BluetoothSerial SerialBT;
const char* BT_DEVICE_NAME = "ControlPatroDj";
const char* BT_AUDIO_DEVICE_NAME = "ControlPatroDj";
// --- PANTALLA OLED (I2C) ---
// D21 = SDA - D22 = SCL
#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);
unsigned long lastParamInteractionTime = 0;
#define OLED_PARAM_TIMEOUT_MS 3000
// --- TIRA LED ---
#define LED_PIN 4
#define NUM_LEDS 30
#define LED_BRIGHTNESS 180
//#define LED_BUILTIN 2
// --- ENCODERS ---
// Encoders principales
#define VOL_CLK_PIN 12
#define VOL_DT_PIN 13
#define GAIN_CLK_PIN 34
#define GAIN_DT_PIN 35
#define HIGH_CLK_PIN 1
#define HIGH_DT_PIN 3
#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// Pin TX
#define LOW_DT_PIN 14// Pin RX
// --- MULTIPLEXOR DE ENCODERS ---
// SECCIÓN ELIMINADA. Los encoders ahora están conectados directamente.
// --- SALIDA DE AUDIO I2S ---
#define I2S_DOUT_PIN 19
#define I2S_BCLK_PIN 16
#define I2S_LRCK_PIN 17
// --- REGISTROS DE DESPLAZAMIENTO 74HC595 (VSPI) ---
// CONEXIÓN:
// ESP32 MOSI (GPIO 23) -> IC_1 (Control) Pin 14 (DS)
// IC_1 Pin 9 (Q7S) -> IC_2 (Potenciómetros) Pin 14 (DS)
// Ambos ICs comparten el reloj (SCK, GPIO 18) y el latch (GPIO 5).
#define SHIFT_REG_LATCH_PIN 5 // Pin 12 (ST_CP) de AMBOS 74HC595.
// MOSI (GPIO 23) va al pin 14 (DS) del PRIMER registro.
// SCK (GPIO 18) va al pin 11 (SH_CP) de AMBOS 74HC595.
// --- BITS DEL PRIMER 74HC595 (IC_1 - Control) ---
// Este IC maneja las señales de control generales.
const byte SELECT_IN_A1_BIT = (1 << 0); // Salida Q0 controla: Selector de entrada A1
const byte SELECT_IN_A0_BIT = (1 << 1); // Salida Q1 controla: Selector de entrada A0
const byte LED_BT_AUDIO_BIT = (1 << 2); // Salida Q2 controla: LED de estado del Bluetooth de audio (A2DP)
const byte LED_BT_BIT = (1 << 3); // Salida Q3 controla: LED indicador de fuente BT seleccionada
const byte LED_MIXER_BIT = (1 << 4); // Salida Q4 controla: LED indicador de fuente MIXER seleccionada
const byte LED_AUX_BIT = (1 << 5); // Salida Q5 controla: LED indicador de fuente AUX seleccionada
// --- BITS DEL SEGUNDO 74HC595 (IC_2 - Potenciómetros) ---
// Este IC se dedica exclusivamente a habilitar (CS) los potenciómetros.
const byte POT_VOL_BIT = (1 << 0);
const byte POT_GAIN_BIT = (1 << 1);
const byte POT_HIGH_BIT = (1 << 2);
const byte POT_MID_HIGH_BIT = (1 << 3);
const byte POT_MID_LOW_BIT = (1 << 4);
const byte POT_LOW_BIT = (1 << 5);
const byte POT_DBASS_BIT = (1 << 6);
const byte MUTE_BIT = (1 << 7); // Salida Q7 controla: MUTE
const byte DESELECT_ALL_POTS = 0x00;
// --- ENTRADAS DE AUDIO ---
#define AUDIO_IN_L 36
#define AUDIO_IN_R 39
// --- PARÁMETROS VÚMETRO ---
#define VUMETER_SAMPLES_AVG 64
#define VUMETER_ADC_REF_0DB 1106.0
#define VUMETER_DB_MIN -30.0
#define VUMETER_DB_MAX 11.4
#define VUMETER_PEAK_HOLD_MS 500
#define VUMETER_PEAK_DECAY_RATE 0.75
// --- PARÁMETROS FFT ---
#define FFT_SAMPLES 256
#define FFT_SAMPLING_FREQUENCY 40000.0
#define FFT_AMPLITUDE 900.0
const int VIS_PEAK_FALL_DELAY = 50;
// --- PARÁMETROS DE PREAMP ---
const int POT_MAX_STEPS = 255;
#define ENCODER_STEPS_PER_DB 2
const int VOL_MIN = -70, VOL_MAX = 10, GAIN_MIN = -20, GAIN_MAX = 20, EQ_MIN = -15, EQ_MAX = 15;
const int DBASS_MIN = -50, DBASS_MAX = 0;
const int DBASS_WIPER_MAX = 192;
// --- IDENTIFICADORES JSON ---
const char* PARAM_M_VOL = "m_vol"; const char* PARAM_S_VOL = "s_vol";
const char* PARAM_M_GAIN = "m_gain"; const char* PARAM_S_GAIN = "s_gain";
const char* PARAM_M_HIGH = "m_high"; const char* PARAM_S_HIGH = "s_high";
const char* PARAM_M_MID_HIGH = "m_mid_high"; const char* PARAM_S_MID_HIGH = "s_mid_high";
const char* PARAM_M_MID_LOW = "m_mid_low"; const char* PARAM_S_MID_LOW = "s_mid_low";
const char* PARAM_M_LOW = "m_low"; const char* PARAM_S_LOW = "s_low";
const char* PARAM_M_DBASS = "m_dbass"; const char* PARAM_S_DBASS = "s_dbass";
const char* PARAM_M_MUTE = "m_mute"; const char* PARAM_S_MUTE = "s_mute";
const char* PARAM_M_SOURCE = "m_source";
const char* PARAM_M_VISMODE = "m_vismode";
const char* CMD_GET_STATUS = "get_status";
// =========================================================================
// === OBJETOS Y VARIABLES GLOBALES ===
// =========================================================================
BluetoothA2DPSink a2dp_sink;
ESP32Encoder encoderVol, encoderGain;
ESP32Encoder encoderHigh, encoderMidHigh, encoderMidLow, encoderLow;
int currentVolDB = 0, currentGainDB = 0, currentHighDB = 0, currentMidHighDB = 0, currentMidLowDB = 0, currentLowDB = 0;
int currentDbassDB = 0;
int volumeBeforeMute = VOL_MIN;
double peakL = VUMETER_DB_MIN, peakR = VUMETER_DB_MIN;
unsigned long lastPeakTimeL = 0, lastPeakTimeR = 0;
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
double vRealL[FFT_SAMPLES], vImagL[FFT_SAMPLES], vRealR[FFT_SAMPLES], vImagR[FFT_SAMPLES];
ArduinoFFT<double> fftL = ArduinoFFT<double>(vRealL, vImagL, FFT_SAMPLES, FFT_SAMPLING_FREQUENCY);
ArduinoFFT<double> fftR = ArduinoFFT<double>(vRealR, vImagR, FFT_SAMPLES, FFT_SAMPLING_FREQUENCY);
int vis_peakL = 0, vis_peakR = 0;
unsigned long last_vis_peak_time = 0;
bool systemIsMuted = true;
byte shiftRegControlState = 0;
byte currentPotSelection = DESELECT_ALL_POTS;
// =========================================================================
// === PROTOTIPOS DE FUNCIONES ===
// =========================================================================
void handleSelectButton();
void handlePreampControls();
void runPreampDisplay();
void runVisualizer();
void switchToOtaUpdater();
void updateParamDisplay(const char *paramName, int currentValue, int minValue, int maxValue);
void updateVumeterDisplay();
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue);
void updateOledVisualizerModeDisplay();
void selectPotentiometer(byte potSelectByte);
void writeToPot(byte potBit, byte address, byte value);
void setPotValue(byte potBit, int dbValue, int dbMin, int dbMax);
void sendEncoderUpdate(const char* param, int value);
void handleIncomingData();
void performFFT();
void visualizerSpectrum();
void visualizerVUMeter();
void visualizerBassPulse();
void visualizerPeakMeter();
void updateParameter(const char* param, int value, bool isRemote);
void selectAudioSource(AudioSource source);
void bt_audio_connection_state_callback(esp_a2d_connection_state_t state, void *object);
void commitShiftRegisterState();
void setControlOutput(byte pinBit, bool state);
void sendFullStatusUpdate();
// =========================================================================
// === SETUP ===
// =========================================================================
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.clearDisplay();
Wire.begin();
SPI.begin();
strip.begin(); strip.setBrightness(LED_BRIGHTNESS); strip.show();
display.drawBitmap(0, 0, patro_dj_logo_bmp, 128, 64, SSD1306_WHITE);
display.display();
SerialBT.begin(BT_DEVICE_NAME);
delay(1000);
i2s_pin_config_t my_pin_config = {
.bck_io_num = I2S_BCLK_PIN,
.ws_io_num = I2S_LRCK_PIN,
.data_out_num = I2S_DOUT_PIN,
.data_in_num = I2S_PIN_NO_CHANGE
};
a2dp_sink.set_pin_config(my_pin_config);
a2dp_sink.set_on_connection_state_changed(bt_audio_connection_state_callback);
a2dp_sink.start(BT_AUDIO_DEVICE_NAME);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
//pinMode(BOOT_PIN, INPUT);
pinMode(SELECT_BUTTON_PIN, INPUT_PULLUP);
pinMode(SHIFT_REG_LATCH_PIN, OUTPUT);
digitalWrite(SHIFT_REG_LATCH_PIN, HIGH);
currentPotSelection = DESELECT_ALL_POTS;
shiftRegControlState = 0;
commitShiftRegisterState();
// Configuración de encoders
currentVolDB = VOL_MIN;
currentDbassDB = DBASS_MIN;
encoderVol.attachHalfQuad(VOL_DT_PIN, VOL_CLK_PIN);
encoderVol.setCount(VOL_MIN * ENCODER_STEPS_PER_DB);
currentGainDB = 0;
encoderGain.attachHalfQuad(GAIN_DT_PIN, GAIN_CLK_PIN);
encoderGain.setCount(0 * ENCODER_STEPS_PER_DB);
// Inicialización de los 4 encoders de EQ
currentHighDB = 0;
encoderHigh.attachHalfQuad(HIGH_DT_PIN, HIGH_CLK_PIN);
encoderHigh.setCount(0);
currentMidHighDB = 0;
encoderMidHigh.attachHalfQuad(MID_HIGH_DT_PIN, MID_HIGH_CLK_PIN);
encoderMidHigh.setCount(0);
currentMidLowDB = 0;
encoderMidLow.attachHalfQuad(MID_LOW_DT_PIN, MID_LOW_CLK_PIN);
encoderMidLow.setCount(0);
currentLowDB = 0;
encoderLow.attachHalfQuad(LOW_DT_PIN, LOW_CLK_PIN);
encoderLow.setCount(0);
long initialDbassWiper = map(currentDbassDB, DBASS_MIN, DBASS_MAX, DBASS_WIPER_MAX, 0);
writeToPot(POT_DBASS_BIT, 0x00, (byte)initialDbassWiper);
writeToPot(POT_DBASS_BIT, 0x10, (byte)initialDbassWiper);
setPotValue(POT_VOL_BIT, currentVolDB, VOL_MIN, VOL_MAX);
setPotValue(POT_GAIN_BIT, currentGainDB, GAIN_MIN, GAIN_MAX);
setPotValue(POT_HIGH_BIT, currentHighDB, EQ_MIN, EQ_MAX);
setPotValue(POT_MID_HIGH_BIT, currentMidHighDB, EQ_MIN, EQ_MAX);
setPotValue(POT_MID_LOW_BIT, currentMidLowDB, EQ_MIN, EQ_MAX);
setPotValue(POT_LOW_BIT, currentLowDB, EQ_MIN, EQ_MAX);
selectPotentiometer(DESELECT_ALL_POTS);
delay(1000);
selectAudioSource(currentAudioSource);
delay(1000);
systemIsMuted = false;
setPotValue(POT_VOL_BIT, currentVolDB, VOL_MIN, VOL_MAX);
updateOledVisualizerModeDisplay();
if (mainProgramMode == MODE_VISUALIZER) {
lastVisualizerActivity = millis();
}
delay(1000);
oledParamState = OLED_SHOWING_MAIN;
}
// =========================================================================
// === LOOP ===
// =========================================================================
void loop() {
handleSelectButton();
handlePreampControls();
runPreampDisplay();
if (mainProgramMode == MODE_VISUALIZER) {
runVisualizer();
}
}
// =========================================================================
// === FUNCIONES DE CONTROL PRINCIPAL ===
// =========================================================================
void handleSelectButton() {
static unsigned long buttonDownTime = 0;
static unsigned long lastReleaseTime = 0;
static bool buttonWasDown = false;
static bool longPressActionDone = false;
static bool singleClickWaiting = false;
bool isDown = (digitalRead(SELECT_BUTTON_PIN) == LOW);
if (singleClickWaiting && (millis() - lastReleaseTime > DOUBLE_PRESS_WINDOW_MS)) {
if (mainProgramMode == MODE_PREAMP) {
mainProgramMode = MODE_VISUALIZER;
visualizerSubMode = 0;
}
else {
visualizerSubMode++;
if (visualizerSubMode >= TOTAL_VISUALIZER_MODES) {
visualizerSubMode = 0;
}
}
vis_peakL = 0; vis_peakR = 0;
updateOledVisualizerModeDisplay();
lastVisualizerActivity = millis();
if (SerialBT.connected()) sendEncoderUpdate(PARAM_M_VISMODE, visualizerSubMode);
singleClickWaiting = false;
}
if (isDown && !buttonWasDown) {
buttonDownTime = millis();
longPressActionDone = false;
}
if (!isDown && buttonWasDown) {
unsigned long pressDuration = millis() - buttonDownTime;
if (pressDuration < LONG_PRESS_HOLD_MS) {
if (singleClickWaiting && (millis() - lastReleaseTime < DOUBLE_PRESS_WINDOW_MS)) {
singleClickWaiting = false;
if (currentAudioSource == SOURCE_BLUETOOTH) {
currentAudioSource = SOURCE_MIXER;
}
else if (currentAudioSource == SOURCE_MIXER) {
currentAudioSource = SOURCE_AUX;
}
else {
currentAudioSource = SOURCE_BLUETOOTH;
}
selectAudioSource(currentAudioSource);
}
else {
singleClickWaiting = true;
lastReleaseTime = millis();
}
}
}
if (isDown && (millis() - buttonDownTime > LONG_PRESS_HOLD_MS)) {
if (!longPressActionDone) {
switchToOtaUpdater();
longPressActionDone = true;
}
}
buttonWasDown = isDown;
}
void handlePreampControls() {
static bool wasConnected = false;
if (SerialBT.connected()) {
if (!wasConnected) {
delay(500);
sendFullStatusUpdate();
digitalWrite(LED_BUILTIN, HIGH);
wasConnected = true;
}
handleIncomingData();
}
else {
if (wasConnected) {
digitalWrite(LED_BUILTIN, LOW);
wasConnected = false;
}
lastVisualizerActivity = millis();
}
long newEncoderVolCount = encoderVol.getCount();
long newVolDB = newEncoderVolCount / ENCODER_STEPS_PER_DB;
if (newEncoderVolCount != (long)currentVolDB * ENCODER_STEPS_PER_DB) {
if (systemIsMuted) {
systemIsMuted = false;
}
updateParameter(PARAM_M_VOL, newVolDB, false);
}
long newGain = encoderGain.getCount() / ENCODER_STEPS_PER_DB;
if (newGain > GAIN_MAX) { newGain = GAIN_MAX; encoderGain.setCount(newGain * ENCODER_STEPS_PER_DB); }
if (newGain < GAIN_MIN) { newGain = GAIN_MIN; encoderGain.setCount(newGain * ENCODER_STEPS_PER_DB); }
if (newGain != currentGainDB) {
updateParameter(PARAM_M_GAIN, newGain, false);
}
// Encoder HIGH
long newHigh = encoderHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newHigh > EQ_MAX) { newHigh = EQ_MAX; encoderHigh.setCount(newHigh * ENCODER_STEPS_PER_DB); }
if (newHigh < EQ_MIN) { newHigh = EQ_MIN; encoderHigh.setCount(newHigh * ENCODER_STEPS_PER_DB); }
if (newHigh != currentHighDB) {
updateParameter(PARAM_M_HIGH, newHigh, false);
}
// Encoder MID_HIGH
long newMidHigh = encoderMidHigh.getCount() / ENCODER_STEPS_PER_DB;
if (newMidHigh > EQ_MAX) { newMidHigh = EQ_MAX; encoderMidHigh.setCount(newMidHigh * ENCODER_STEPS_PER_DB); }
if (newMidHigh < EQ_MIN) { newMidHigh = EQ_MIN; encoderMidHigh.setCount(newMidHigh * ENCODER_STEPS_PER_DB); }
if (newMidHigh != currentMidHighDB) {
updateParameter(PARAM_M_MID_HIGH, newMidHigh, false);
}
// Encoder MID_LOW
long newMidLow = encoderMidLow.getCount() / ENCODER_STEPS_PER_DB;
if (newMidLow > EQ_MAX) { newMidLow = EQ_MAX; encoderMidLow.setCount(newMidLow * ENCODER_STEPS_PER_DB); }
if (newMidLow < EQ_MIN) { newMidLow = EQ_MIN; encoderMidLow.setCount(newMidLow * ENCODER_STEPS_PER_DB); }
if (newMidLow != currentMidLowDB) {
updateParameter(PARAM_M_MID_LOW, newMidLow, false);
}
// Encoder LOW
long newLow = encoderLow.getCount() / ENCODER_STEPS_PER_DB;
if (newLow > EQ_MAX) { newLow = EQ_MAX; encoderLow.setCount(newLow * ENCODER_STEPS_PER_DB); }
if (newLow < EQ_MIN) { newLow = EQ_MIN; encoderLow.setCount(newLow * ENCODER_STEPS_PER_DB); }
if (newLow != currentLowDB) {
updateParameter(PARAM_M_LOW, newLow, false);
}
}
void runPreampDisplay() {
if (oledParamState == OLED_SHOWING_PARAM && millis() - lastParamInteractionTime > OLED_PARAM_TIMEOUT_MS) {
oledParamState = OLED_SHOWING_MAIN;
}
if (oledParamState == OLED_SHOWING_SOURCE && millis() - lastParamInteractionTime > OLED_PARAM_TIMEOUT_MS) {
oledParamState = OLED_SHOWING_MAIN;
}
if (oledParamState == OLED_SHOWING_MAIN) {
updateVumeterDisplay();
}
delay(20);
}
void bt_audio_connection_state_callback(esp_a2d_connection_state_t state, void *object) {
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
setControlOutput(LED_BT_AUDIO_BIT, HIGH);
}
else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
setControlOutput(LED_BT_AUDIO_BIT, LOW);
}
}
void sendFullStatusUpdate() {
sendEncoderUpdate(PARAM_M_VOL, currentVolDB);
sendEncoderUpdate(PARAM_M_GAIN, currentGainDB);
sendEncoderUpdate(PARAM_M_HIGH, currentHighDB);
sendEncoderUpdate(PARAM_M_MID_HIGH, currentMidHighDB);
sendEncoderUpdate(PARAM_M_MID_LOW, currentMidLowDB);
sendEncoderUpdate(PARAM_M_LOW, currentLowDB);
sendEncoderUpdate(PARAM_M_MUTE, systemIsMuted ? 1 : 0);
sendEncoderUpdate(PARAM_M_SOURCE, (int)currentAudioSource);
int currentVisMode = (mainProgramMode == MODE_PREAMP) ? 4 : visualizerSubMode;
sendEncoderUpdate(PARAM_M_VISMODE, currentVisMode);
}
void runVisualizer() {
if (millis() - lastVisualizerActivity > VISUALIZER_TIMEOUT_MS) { mainProgramMode = MODE_PREAMP; strip.clear(); strip.show(); return; }
switch (visualizerSubMode) {
case 0: visualizerSpectrum(); break;
case 1: visualizerVUMeter(); break;
case 2: visualizerBassPulse(); break;
case 3: visualizerPeakMeter(); break;
case 4:
strip.clear();
strip.show();
break;
}
}
void switchToOtaUpdater() {
display.clearDisplay(); display.setTextSize(1); display.setCursor(14, 25); display.println("REINICIANDO....."); display.display();
delay(1000);
const esp_partition_t* update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 10); display.println("ERROR: No se encontro"); display.println("la particion OTA."); display.display(); delay(3000); ESP.restart(); return;
}
esp_err_t err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 10); display.println("ERROR: Fallo al"); display.println("cambiar particion."); display.display(); delay(3000);
}
ESP.restart();
}
void updateParamDisplay(const char *paramName, int currentValue, int minValue, int maxValue) {
oledParamState = OLED_SHOWING_PARAM;
lastParamInteractionTime = millis();
display.clearDisplay(); 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);
int barWidth = map(currentValue, minValue, maxValue, 0, SCREEN_WIDTH);
display.drawRect(0, 42, SCREEN_WIDTH, 16, SSD1306_WHITE);
if (minValue < 0 && maxValue > 0) {
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 { display.fillRect(0, 42, barWidth, 16, SSD1306_WHITE); }
display.display();
}
void updateVumeterDisplay() {
unsigned long totalL = 0; unsigned long totalR = 0;
for (int i = 0; i < VUMETER_SAMPLES_AVG; i++) { totalL += analogRead(AUDIO_IN_L); totalR += analogRead(AUDIO_IN_R); }
double dbL = (totalL == 0) ? VUMETER_DB_MIN : 20 * log10((double)(totalL / VUMETER_SAMPLES_AVG) / VUMETER_ADC_REF_0DB);
double dbR = (totalR == 0) ? VUMETER_DB_MIN : 20 * log10((double)(totalR / VUMETER_SAMPLES_AVG) / VUMETER_ADC_REF_0DB);
unsigned long currentTime = millis();
if (dbL >= peakL) { peakL = dbL; lastPeakTimeL = currentTime; } else if (currentTime - lastPeakTimeL > VUMETER_PEAK_HOLD_MS) { peakL -= VUMETER_PEAK_DECAY_RATE; if (peakL < dbL) peakL = dbL; if (peakL < VUMETER_DB_MIN) peakL = VUMETER_DB_MIN; }
if (dbR >= peakR) { peakR = dbR; lastPeakTimeR = currentTime; } else if (currentTime - lastPeakTimeR > VUMETER_PEAK_HOLD_MS) { peakR -= VUMETER_PEAK_DECAY_RATE; if (peakR < dbR) peakR = dbR; if (peakR < VUMETER_DB_MIN) peakR = VUMETER_DB_MIN; }
display.clearDisplay();
int label_x = 2; int bar_x_start = 12;
display.setTextSize(1); display.setTextColor(SSD1306_WHITE);
drawVUMeterBar(bar_x_start, 4, dbL, peakL);
display.setCursor(label_x, 12); display.print("L");
drawVUMeterBar(bar_x_start, 39, dbR, peakR);
display.setCursor(label_x, 47); display.print("R");
const int MARKER_Y_START = 24; const int MARKER_HEIGHT = 14; const double BAR_LENGTH_D = 110.0;
int x_marker_neg30 = bar_x_start + (int)(((-30.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
int x_marker_0 = bar_x_start + (int)(((0.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
int x_marker_10 = bar_x_start + (int)(((10.0 - VUMETER_DB_MIN) / (VUMETER_DB_MAX - VUMETER_DB_MIN)) * BAR_LENGTH_D);
display.drawFastVLine(x_marker_neg30, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.drawFastVLine(x_marker_0, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.drawFastVLine(x_marker_10, MARKER_Y_START, MARKER_HEIGHT, SSD1306_WHITE);
display.setCursor(15, 28);
display.print("-30dB 0dB 10dB");
display.display();
}
void drawVUMeterBar(int x_start, int y_start, double dbValue, double peakDbValue) {
#define BAR_LENGTH 110
#define BAR_HEIGHT 20
#define SEGMENT_WIDTH 3
#define SEGMENT_SPACING 1
int totalSegments = BAR_LENGTH / (SEGMENT_WIDTH + SEGMENT_SPACING);
display.drawRect(x_start - 1, y_start - 1, BAR_LENGTH + 2, BAR_HEIGHT + 2, SSD1306_WHITE);
int activeSegments = map(dbValue, VUMETER_DB_MIN, VUMETER_DB_MAX, 0, totalSegments);
activeSegments = constrain(activeSegments, 0, totalSegments);
for (int i = 0; i < activeSegments; i++) { int xPos = x_start + (i * (SEGMENT_WIDTH + SEGMENT_SPACING)); display.fillRect(xPos, y_start, SEGMENT_WIDTH, BAR_HEIGHT, SSD1306_WHITE); }
int peakSegment = map(peakDbValue, VUMETER_DB_MIN, VUMETER_DB_MAX, 0, totalSegments);
peakSegment = constrain(peakSegment, 0, totalSegments);
int peakXPos = x_start + (peakSegment * (SEGMENT_WIDTH + SEGMENT_SPACING));
if (peakXPos < x_start + BAR_LENGTH) { display.fillRect(peakXPos, y_start, SEGMENT_WIDTH, BAR_HEIGHT, SSD1306_WHITE); }
}
void updateOledVisualizerModeDisplay() {
oledParamState = OLED_SHOWING_PARAM;
lastParamInteractionTime = millis();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
const char* modeName;
switch (visualizerSubMode) {
case 0: modeName = "ESPECTRO"; break;
case 1: modeName = "VUMETRO"; break;
case 2: modeName = "BASS PULSE"; break;
case 3: modeName = "PEAK METER"; break;
case 4: modeName = "LEDS OFF"; break;
default: modeName = "MODO ???"; break;
}
display.setTextSize(2);
display.setCursor(2, 28);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(modeName, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, display.getCursorY());
display.println(modeName);
display.display();
}
void selectAudioSource(AudioSource source) {
systemIsMuted = true;
selectPotentiometer(DESELECT_ALL_POTS);
oledParamState = OLED_SHOWING_SOURCE;
lastParamInteractionTime = millis();
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(36, 5);
display.println("MODO:");
display.setTextSize(3);
const char* sourceName = "";
setControlOutput(LED_BT_BIT, LOW);
setControlOutput(LED_MIXER_BIT, LOW);
setControlOutput(LED_AUX_BIT, LOW);
if (source == SOURCE_BLUETOOTH) {
// Para la fuente 0: A1=LOW, A0=LOW
setControlOutput(SELECT_IN_A1_BIT, LOW);
setControlOutput(SELECT_IN_A0_BIT, LOW);
setControlOutput(LED_BT_BIT, HIGH); // Encender LED de BT
sourceName = "BT";
} else if (source == SOURCE_MIXER) {
// Para la fuente 1: A1=LOW, A0=HIGH
setControlOutput(SELECT_IN_A1_BIT, LOW);
setControlOutput(SELECT_IN_A0_BIT, HIGH);
setControlOutput(LED_MIXER_BIT, HIGH); // Encender LED de Mixer
sourceName = "MIXER";
} else { // SOURCE_AUX
// Para la fuente 2: A1=HIGH, A0=LOW
setControlOutput(SELECT_IN_A1_BIT, HIGH);
setControlOutput(SELECT_IN_A0_BIT, LOW);
setControlOutput(LED_AUX_BIT, HIGH); // Encender LED de Aux
sourceName = "AUX";
}
if (SerialBT.connected()) {
sendEncoderUpdate(PARAM_M_SOURCE, (int)source);
}
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(sourceName, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w) / 2, 35);
display.println(sourceName);
display.display();
delay(50);
systemIsMuted = false;
selectPotentiometer(DESELECT_ALL_POTS);
}
void sendEncoderUpdate(const char* param, int value) {
JsonDocument doc; doc["param"] = param; doc["value"] = value;
String output; serializeJson(doc, output); SerialBT.println(output);
}
void handleIncomingData() {
if (SerialBT.available()) {
String incomingData = SerialBT.readStringUntil('\n');
JsonDocument doc; DeserializationError error = deserializeJson(doc, incomingData);
if (error) return;
const char* param = doc["param"];
int value = doc["value"];
if (strcmp(param, CMD_GET_STATUS) == 0) {
sendFullStatusUpdate();
}
else {
updateParameter(param, value, true);
}
}
}
void updateParameter(const char* param, int value, bool isRemote) {
long potStep;
const char* displayName = "";
byte potBit = 0;
int minVal=0, maxVal=0;
const char* masterParam = "";
if (strcmp(param, PARAM_M_VOL) == 0 || strcmp(param, PARAM_S_VOL) == 0) {
currentVolDB = value;
displayName = "VOLUMEN";
potBit = POT_VOL_BIT;
minVal = VOL_MIN; maxVal = VOL_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderVol.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_VOL;
int oldDbassDB = currentDbassDB;
if (currentVolDB <= -10) { currentDbassDB = map(currentVolDB, VOL_MIN, -10, DBASS_MIN, DBASS_MAX); } else { currentDbassDB = DBASS_MAX; }
currentDbassDB = constrain(currentDbassDB, DBASS_MIN, DBASS_MAX);
if (currentDbassDB != oldDbassDB) {
long dbassPotStep = constrain(map(currentDbassDB, DBASS_MIN, DBASS_MAX, DBASS_WIPER_MAX, 0), 0, DBASS_WIPER_MAX);
writeToPot(POT_DBASS_BIT, 0x00, (byte)dbassPotStep);
writeToPot(POT_DBASS_BIT, 0x10, (byte)dbassPotStep);
if (SerialBT.connected()) { sendEncoderUpdate(PARAM_M_DBASS, currentDbassDB); }
}
}
else if (strcmp(param, PARAM_M_DBASS) == 0 || strcmp(param, PARAM_S_DBASS) == 0) {
currentDbassDB = value;
displayName = "DBASS";
potBit = POT_DBASS_BIT;
minVal = DBASS_MIN; maxVal = DBASS_MAX;
potStep = constrain(map(value, minVal, maxVal, DBASS_WIPER_MAX, 0), 0, DBASS_WIPER_MAX);
masterParam = PARAM_M_DBASS;
}
else if (strcmp(param, PARAM_M_GAIN) == 0 || strcmp(param, PARAM_S_GAIN) == 0) {
currentGainDB = value;
displayName = "GAIN";
potBit = POT_GAIN_BIT;
minVal = GAIN_MIN; maxVal = GAIN_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderGain.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_GAIN;
}
else if (strcmp(param, PARAM_M_HIGH) == 0 || strcmp(param, PARAM_S_HIGH) == 0) {
currentHighDB = value;
displayName = "HIGH";
potBit = POT_HIGH_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderHigh.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_HIGH;
}
else if (strcmp(param, PARAM_M_MID_HIGH) == 0 || strcmp(param, PARAM_S_MID_HIGH) == 0) {
currentMidHighDB = value;
displayName = "MID HIGH";
potBit = POT_MID_HIGH_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderMidHigh.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_MID_HIGH;
}
else if (strcmp(param, PARAM_M_MID_LOW) == 0 || strcmp(param, PARAM_S_MID_LOW) == 0) {
currentMidLowDB = value;
displayName = "MID LOW";
potBit = POT_MID_LOW_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderMidLow.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_MID_LOW;
}
else if (strcmp(param, PARAM_M_LOW) == 0 || strcmp(param, PARAM_S_LOW) == 0) {
currentLowDB = value;
displayName = "LOW";
potBit = POT_LOW_BIT;
minVal = EQ_MIN; maxVal = EQ_MAX;
potStep = constrain(map(value, minVal, maxVal, POT_MAX_STEPS, 0), 0, POT_MAX_STEPS);
encoderLow.setCount(value * ENCODER_STEPS_PER_DB);
masterParam = PARAM_M_LOW;
}
else if (strcmp(param, PARAM_M_MUTE) == 0 || strcmp(param, PARAM_S_MUTE) == 0) {
bool newMuteState = (value == 1); // 1 = Mute ON, 0 = Mute OFF
if (newMuteState != systemIsMuted) {
systemIsMuted = newMuteState;
if (systemIsMuted) {
volumeBeforeMute = currentVolDB;
updateParameter(PARAM_M_VOL, VOL_MIN, true);
}
else {
updateParameter(PARAM_M_VOL, volumeBeforeMute, true);
}
}
selectPotentiometer(DESELECT_ALL_POTS);
if (SerialBT.connected()) {
sendEncoderUpdate(PARAM_M_MUTE, systemIsMuted ? 1 : 0);
}
return;
}
else {
return;
}
if (strcmp(param, PARAM_M_DBASS) != 0 && strcmp(param, PARAM_S_DBASS) != 0) {
updateParamDisplay(displayName, value, minVal, maxVal);
}
writeToPot(potBit, 0x00, (byte)potStep);
writeToPot(potBit, 0x10, (byte)potStep);
if (SerialBT.connected() && strcmp(masterParam, "") != 0) {
sendEncoderUpdate(masterParam, value);
}
}
void commitShiftRegisterState() {
byte controlData = shiftRegControlState;
byte potData = 0xFF;
if (systemIsMuted) {
potData &= ~MUTE_BIT;
}
else {
potData |= MUTE_BIT;
}
potData &= ~currentPotSelection;
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWrite(SHIFT_REG_LATCH_PIN, LOW);
SPI.transfer(potData);
SPI.transfer(controlData);
digitalWrite(SHIFT_REG_LATCH_PIN, HIGH);
SPI.endTransaction();
}
void setControlOutput(byte pinBit, bool state) {
if (state) {
shiftRegControlState |= pinBit;
}
else {
shiftRegControlState &= ~pinBit;
}
commitShiftRegisterState();
}
void selectPotentiometer(byte potSelectByte) {
currentPotSelection = potSelectByte;
commitShiftRegisterState();
}
void writeToPot(byte potBit, byte address, byte value) {
selectPotentiometer(potBit);
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
SPI.transfer(address);
SPI.transfer(value);
SPI.endTransaction();
selectPotentiometer(DESELECT_ALL_POTS);
}
void setPotValue(byte potBit, int dbValue, int dbMin, int dbMax) {
long potStep = map(dbValue, dbMin, dbMax, POT_MAX_STEPS, 0);
potStep = constrain(potStep, 0, POT_MAX_STEPS);
writeToPot(potBit, 0x00, (byte)potStep);
writeToPot(potBit, 0x10, (byte)potStep);
}
void performFFT() {
for (int i = 0; i < FFT_SAMPLES; i++) {
vRealL[i] = (double)analogRead(AUDIO_IN_L) - 2048.0; vImagL[i] = 0.0;
vRealR[i] = (double)analogRead(AUDIO_IN_R) - 2048.0; vImagR[i] = 0.0;
delayMicroseconds(20);
}
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();
}
void visualizerSpectrum() {
performFFT(); strip.clear();
int leds_per_channel = NUM_LEDS / 2;
for (int i = 0; i < leds_per_channel; i++) {
int bin = map(i, 0, leds_per_channel - 1, 2, FFT_SAMPLES / 4);
int brightnessL = constrain((int)(vRealL[bin] / FFT_AMPLITUDE * 255.0), 0, 255);
uint16_t hueL = map(i, 0, leds_per_channel - 1, 22000, 0);
strip.setPixelColor(i, strip.ColorHSV(hueL, 255, brightnessL));
int brightnessR = constrain((int)(vRealR[bin] / FFT_AMPLITUDE * 255.0), 0, 255);
uint16_t hueR = map(i, 0, leds_per_channel - 1, 22000, 0);
strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(hueR, 255, brightnessR));
}
strip.show();
}
void visualizerVUMeter() {
performFFT(); strip.clear();
int leds_per_channel = NUM_LEDS / 2;
double totalMagL = 0, totalMagR = 0;
for(int i = 2; i < FFT_SAMPLES / 2; i++) { totalMagL += vRealL[i]; totalMagR += vRealR[i]; }
int heightL = constrain(map((long)totalMagL, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
int heightR = constrain(map((long)totalMagR, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
for(int i = 0; i < leds_per_channel; i++) {
if (i < heightL) strip.setPixelColor(i, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
if (i < heightR) strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
}
strip.show();
}
void visualizerBassPulse() {
performFFT(); strip.clear();
double bassL = 0, bassR = 0;
for (int i = 2; i < 7; i++) { bassL += vRealL[i]; bassR += vRealR[i]; }
int brightnessL = constrain(map((long)bassL, 0, FFT_AMPLITUDE * 5, 0, 255), 0, 255);
int brightnessR = constrain(map((long)bassR, 0, FFT_AMPLITUDE * 5, 0, 255), 0, 255);
uint32_t colorL = strip.Color(0, 0, brightnessL);
uint32_t colorR = strip.Color(brightnessR, 0, 0);
for(int i = 0; i < NUM_LEDS / 2; i++) {
strip.setPixelColor(i, colorL); strip.setPixelColor(i + (NUM_LEDS / 2), colorR);
}
strip.show();
}
void visualizerPeakMeter() {
performFFT();
strip.clear();
int leds_per_channel = NUM_LEDS / 2;
double totalMagL = 0, totalMagR = 0;
for(int i = 2; i < FFT_SAMPLES / 2; i++) { totalMagL += vRealL[i]; totalMagR += vRealR[i]; }
int heightL = constrain(map((long)totalMagL, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
int heightR = constrain(map((long)totalMagR, 0, FFT_AMPLITUDE * 40, 0, leds_per_channel), 0, leds_per_channel);
if (heightL > vis_peakL) { vis_peakL = heightL; }
if (heightR > vis_peakR) { vis_peakR = heightR; }
for(int i = 0; i < leds_per_channel; i++) {
if (i < heightL) strip.setPixelColor(i, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
if (i < heightR) strip.setPixelColor(i + leds_per_channel, strip.ColorHSV(map(i, 0, leds_per_channel - 1, 22000, 0), 255, 255));
}
if (vis_peakL > 0) { strip.setPixelColor(vis_peakL - 1, strip.Color(255, 255, 255)); }
if (vis_peakR > 0) { strip.setPixelColor((vis_peakR - 1) + leds_per_channel, strip.Color(255, 255, 255)); }
strip.show();
if (millis() - last_vis_peak_time > VIS_PEAK_FALL_DELAY) {
if (vis_peakL > 0) vis_peakL--;
if (vis_peakR > 0) vis_peakR--;
last_vis_peak_time = millis();
}
}