#include <Arduino.h>
#include <Wire.h>
#include <EEPROM.h>
#include <SPI.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include "TDA7419.hpp" // Pastikan namanya sesuai (hpp)
// --- KONFIGURASI PIN ESP32-C3 ---
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define DATA_PIN 10
#define CLK_PIN 1
#define CS_PIN 0
#define SAIN_PIN 7
#define SAOUT_PIN 2
#define ENC_CLK 4
#define ENC_DT 5
#define ENC_SW 6
#define EEPROM_SIZE 64
MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
TDA7419::TDA7419 tda(Wire);
// --- PARAMETER & OPSI MENU ---
// Index: 0:VOL, 1:BAS, 2:MID, 3:TRE, 4:B-F, 5:M-F, 6:T-F, 7:LOD, 8:SUB, 9:S-F, 10:B-Q, 11:M-Q, 12:SPE-Q
int param[13] = {0,0,0,0,0,0,0,0,0,0,0,0,0};
const String menuName[13] = {"VOL","BAS","MID","TRE","B-F","M-F","T-F","LOD","SUB","S-F","B-Q","M-Q","SP-Q"};
const String bassFreq[4] = {"60","80","100","200"};
const String midFreq[4] = {"500","1K","1K5","2K5"};
const String treFreq[4] = {"10K","12K","15K","17K"};
const String subFreq[4] = {"FLT","80","120","160"};
const String qFactor[4] = {"1.0","1.25","1.5","2.0"};
int menu = 0;
bool edit = false;
int lastCLK;
unsigned long idleTimer = 0;
unsigned long animTimer = 0;
unsigned long swTimer = 0;
// Spectrum Analyzer Variables
int spectrumValues[7];
float peakMirror[7];
int smooth[7];
int autoGain[7] = {150, 150, 150, 150, 150, 150, 150};
int visualMode = 0;
enum State {MENU, EQ};
State currentState = MENU;
// --- FUNGSI AUDIO ---
void applyAudio() {
// 1. Soft Mute aktifkan lewat library
tda.setSoftMute(true);
// 2. Volume & EQ (Menggunakan range yang diizinkan library)
tda.setMasterVolume(constrain(param[0], -80, 15));
tda.setBassLevel(constrain(param[1], -15, 15));
tda.setMiddleLevel(constrain(param[2], -15, 15));
tda.setTrebleLevel(constrain(param[3], -15, 15));
tda.setSubwooferVolume(constrain(param[8], -80, 15));
// 3. Frekuensi Center (Casting ke Enum yang ada di hpp)
tda.setBassCenterFreq(static_cast<TDA7419::BassCenterFreq>(param[4]));
tda.setMiddleCenterFreq(static_cast<TDA7419::MiddleCenterFreq>(param[5]));
tda.setTrebleCenterFreq(static_cast<TDA7419::TrebleCenterFreq>(param[6]));
tda.setSubCutoffFreq(static_cast<TDA7419::SubCutoffFreq>(param[9]));
// 4. Q-Factor (Hanya Bass & Mid yang punya Q Factor di TDA7419)
tda.setBassQFactor(static_cast<TDA7419::BassQFactor>(param[10]));
tda.setMiddleQFactor(static_cast<TDA7419::MiddleQFactor>(param[11]));
// 5. Loudness & Subwoofer Enable
tda.setLoudnessAttenuation(param[7] ? 0 : 15);
tda.setSubwooferEnable(true); // Pastikan Subwoofer ON
tda.setBassDcMode(true); // DC Mode ON agar bass lebih deep
// 6. Spectrum Analyzer Q (Khusus menu ke-12)
tda.setSpectrumFilterQ(param[12] == 0 ? TDA7419::SpectrumFilterQ::Q3_5 : TDA7419::SpectrumFilterQ::Q1_75);
// 7. Kirim semua perubahan ke chip
tda.sendAllRegisters();
// 8. Lepas Soft Mute
tda.setSoftMute(false);
tda.sendChangedRegisters();
}
// --- VISUALIZER ---
void readSpectrum() {
digitalWrite(SAIN_PIN, HIGH); delay(2); digitalWrite(SAIN_PIN, LOW); delayMicroseconds(150);
for(int i=0; i<7; i++){
digitalWrite(SAIN_PIN, HIGH); delayMicroseconds(120);
long val = 0; for(int s=0; s<8; s++) val += analogRead(SAOUT_PIN);
val = val / 8; digitalWrite(SAIN_PIN, LOW); delayMicroseconds(60);
int noiseGate = 70; if(val < noiseGate) val = 0; else val = val - noiseGate;
if(i < 3) val = val * 2; else val = val * 4;
smooth[i] = (smooth[i] * 3 + val) / 4; val = smooth[i];
if(val > autoGain[i]) autoGain[i] = (int)val;
int currentMax = (autoGain[i] < 200) ? 200 : autoGain[i];
spectrumValues[i] = constrain(map(val, 0, currentMax, 0, 15), 0, 15);
if((float)spectrumValues[i] >= peakMirror[i]) peakMirror[i] = (float)spectrumValues[i];
else { peakMirror[i] -= 0.4; if(peakMirror[i] < 0) peakMirror[i] = 0; }
if(autoGain[i] > 200) autoGain[i] -= 2;
}
}
void drawVisuals() {
MD_MAX72XX *mx = P.getGraphicObject();
readSpectrum();
mx->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
mx->clear();
for(int i=0; i<7; i++){
int h = spectrumValues[i]; int p = (int)peakMirror[i];
if(visualMode == 0){
for(int d=0; d<h; d++){ mx->setPoint(i, 16+d, true); mx->setPoint(i, 15-d, true); }
if(p>0){ mx->setPoint(i, 16+p, true); mx->setPoint(i, 15-p, true); }
} else if(visualMode == 1){
for(int d=0; d<h; d++){ mx->setPoint(i, d, true); mx->setPoint(i, 31-d, true); }
if(p>0){ mx->setPoint(i, p, true); mx->setPoint(i, 31-p, true); }
} else {
int startCol = i * 4 + 2;
for(int w=0; w<3; w++){
for(int d=0; d<h; d++) mx->setPoint(d/2, startCol+w, true);
if(p>0) mx->setPoint(p/2, startCol+w, true);
}
}
}
mx->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}
// --- UI LOGIC ---
void showMenu() {
currentState = MENU; P.displayClear();
// Proteksi: Gunakan index yang valid 0-3 untuk array freq/q
int val = param[menu];
int idx = constrain(val, 0, 3);
if(menu == 4) P.print("B-F " + bassFreq[idx]);
else if(menu == 5) P.print("M-F " + midFreq[idx]);
else if(menu == 6) P.print("T-F " + treFreq[idx]);
else if(menu == 7) P.print("LOD " + String(param[7] ? "ON" : "OFF"));
else if(menu == 8) P.print("SUB " + String(param[8]));
else if(menu == 9) P.print("S-F " + subFreq[idx]);
else if(menu == 10) P.print("B-Q " + qFactor[idx]);
else if(menu == 11) P.print("M-Q " + qFactor[idx]);
else if(menu == 12) P.print("SP-Q " + String(param[12] == 0 ? "3.5" : "1.75"));
else P.print(menuName[menu] + " " + String(param[menu]));
idleTimer = millis();
}
void processInput(bool up) {
if(edit){
if(up) param[menu]++; else param[menu]--;
// Batasan nilai per menu
if(menu == 0 || menu == 8) param[menu] = constrain(param[menu], -80, 15);
else if(menu >= 1 && menu <= 3) param[menu] = constrain(param[menu], -15, 15);
else if(menu == 7) param[7] = constrain(param[7], 0, 1);
else param[menu] = constrain(param[menu], 0, 3);
applyAudio();
} else {
if(up) menu++; else menu--;
menu = constrain(menu, 0, 12);
}
showMenu();
}
void setup() {
Wire.begin(8, 9);
EEPROM.begin(EEPROM_SIZE);
P.begin(); P.setIntensity(2);
pinMode(SAIN_PIN, OUTPUT); digitalWrite(SAIN_PIN, LOW);
pinMode(SAOUT_PIN, INPUT);
tda.begin();
tda.setMainSource(TDA7419::InputSource::SE3);
tda.setInputGain(15);
// Load EEPROM
for(int i=0; i<13; i++){
int stored = EEPROM.read(i);
if(stored == 255) param[i] = 0;
else param[i] = stored - 80;
// Validasi awal agar tidak crash
if(i >= 4 && i != 7 && i != 8) param[i] = constrain(param[i], 0, 3);
}
visualMode = EEPROM.read(15); if(visualMode > 2) visualMode = 0;
applyAudio();
pinMode(ENC_CLK, INPUT); pinMode(ENC_DT, INPUT); pinMode(ENC_SW, INPUT_PULLUP);
lastCLK = digitalRead(ENC_CLK);
P.print("OK"); delay(800); showMenu();
}
void loop() {
int clk = digitalRead(ENC_CLK);
if(clk != lastCLK && clk == LOW){
idleTimer = millis();
processInput(digitalRead(ENC_DT) != clk);
}
lastCLK = clk;
if(digitalRead(ENC_SW) == LOW){
if(swTimer == 0) swTimer = millis();
if(millis() - swTimer > 1500){
visualMode = (visualMode + 1) % 3;
EEPROM.write(15, visualMode); EEPROM.commit();
P.print("MODE " + String(visualMode + 1));
delay(800); swTimer = 0; idleTimer = millis();
}
} else {
if(swTimer > 0 && millis() - swTimer < 1500){
edit = !edit;
if(!edit){
for(int i=0; i<13; i++) EEPROM.write(i, param[i] + 80);
EEPROM.commit();
}
showMenu();
}
swTimer = 0;
}
if(millis() - idleTimer > 6000){
if(currentState == MENU) { currentState = EQ; P.displayClear(); }
if(millis() - animTimer > 40){ animTimer = millis(); drawVisuals(); }
} else {
P.displayAnimate();
}
}