/*/////////////////////////////////////////////////////////////////////////////////
// ArduRITMICO v4.0g - Scanner DJ – FFT +28BYJ-48 +ULN2003 + KY037 (v1.0)
// ETI Patagonia - prof.martintorres@educ.ar - https://github.com/ETI-PATAGONIA-AR
///////////////////////////////////////////////////////////////////////////////////
- Arduino Nano
- ULN 2003
- 28BYJ-48
- KY-037 analog -> A0
- Pots: A1 (B/M) , A2 (M/A), A3 (gain RGB), A4 (maxTiltSteps), A5 (maxPanSteps)
- Vert: 2,3,4,5 Horiz: 6,7,8,12
- RGB MOSFET gates: D9 (R), D10 (G), D11 (B) (N-channel MOSFET low-side)
- Button mode / save: D13 (INPUT_PULLUP, press to GND)
- librerias: fix_fft, AccelStepper, EEPROM
*/
#include <fix_fft.h>
#include <AccelStepper.h>
#include <EEPROM.h>
#include <math.h>
// ---------------- CONFIG PINES ----------------
#define AUDIO_PIN A0
#define POT_BM A1
#define POT_MA A2
#define POT_GAIN A3
#define POT_TILT_MAX A4
#define POT_PAN_MAX A5
AccelStepper stepperVert(AccelStepper::FULL4WIRE, 2, 3, 4, 5);
AccelStepper stepperHoriz(AccelStepper::FULL4WIRE, 6, 7, 8, 12);
#define PIN_R 9
#define PIN_G 10
#define PIN_B 11
#define BUTTON_PIN 13 // INPUT_PULLUP -> press to GND
// ---------------- FFT / BUFFERS ----------------
#define MUESTRAS 128
#define LOGM 7
#define BIN_OFFSET 2 // ignorar DC / ruido
char vReal[MUESTRAS];
char vImag[MUESTRAS];
unsigned long magn[MUESTRAS/2];
// ---------------- MOTION / STEPS ----------------
// 28BYJ-48 typical half-step ~4096 steps/rev depending firmware; usamos 4096
const long STEPS_PER_REV = 4096L;
const int DEFAULT_PAN_STEPS = STEPS_PER_REV / 2; // 180° -> 2048
const int DEFAULT_TILT_STEPS = STEPS_PER_REV / 4; // 90° -> 1024
int maxStepsPan = DEFAULT_PAN_STEPS;
int maxStepsTilt = DEFAULT_TILT_STEPS;
long centerPan = 0;
long centerTilt = 0;
float phasePan = 0;
float phaseTilt = 0;
float basePhaseInc = 0.015; // base increment for smooth motion
// kick parameters
int kickStepsPanBase = 120;
int kickStepsTiltBase = 140;
int kickDecayMs = 120;
long kickTargetPan = 0;
unsigned long kickEndPan = 0;
long kickTargetTilt = 0;
unsigned long kickEndTilt = 0;
// ---------------- RGB / LIGHTS ----------------
float gainRGB = 1.0;
const unsigned long MAX_MAGN_EXPECTED = 12000UL; // ajustar si hace falta
// ---------------- LIMIT BINS (pots) ----------------
int binBMax = 7;
int binMMax = 35;
// ---------------- STROBE / MODES ----------------
bool strobeEnabled = true;
int strobeThreshold = 2500;
unsigned long lastStrobe = 0;
int strobeLenMs = 40;
enum MODE {MODE_SIN=0, MODE_BEAT=1, MODE_STROBE=2, MODE_RANDOM=3, MODE_IDLE=4};
MODE currentMode = MODE_SIN;
// ---------------- EEPROM ADDRESSES ----------------
const int EE_ADDR_VALID = 0;
const int EE_ADDR_MODE = 1;
const int EE_ADDR_PAN = 10; // 2 bytes
const int EE_ADDR_TILT = 20; // 2 bytes
const int EE_ADDR_GAIN = 30; //byte (0..255)
// ---------------- SMOOTHERS ----------------
float smoothB = 0, smoothM = 0, smoothA = 0;
// ---------------- UTILS ----------------
unsigned long lastMillis = 0;
unsigned long sampleDelayMs = 8;
void setup() {
pinMode(PIN_R, OUTPUT);
pinMode(PIN_G, OUTPUT);
pinMode(PIN_B, OUTPUT);
digitalWrite(PIN_R, LOW);
digitalWrite(PIN_G, LOW);
digitalWrite(PIN_B, LOW);
pinMode(BUTTON_PIN, INPUT);
stepperVert.setMaxSpeed(1200);
stepperVert.setAcceleration(2000);
stepperHoriz.setMaxSpeed(1200);
stepperHoriz.setAcceleration(2000);
stepperVert.setCurrentPosition(0);
stepperHoriz.setCurrentPosition(0);
centerPan = 0;
centerTilt = 0;
// ADC ref
analogReference(INTERNAL); // más sensible para mic con voltajes bajos (si KY-037 funciona ok)
delay(50);
loadConfigFromEEPROM();
}
void loop() {
unsigned long now = millis();
int pBM = analogRead(POT_BM);
int pMA = analogRead(POT_MA);
int pGain = analogRead(POT_GAIN);
int pTiltMax = analogRead(POT_TILT_MAX);
int pPanMax = analogRead(POT_PAN_MAX);
int minBin = BIN_OFFSET + 0;
binBMax = constrain(map(pBM, 0, 1023, minBin+1, 14), minBin+1, 24);
binMMax = constrain(map(pMA, 0, 1023, binBMax+1, MUESTRAS/2 - 1), binBMax+1, MUESTRAS/2 - 1);
gainRGB = constrain(map(pGain, 0, 1023, 50, 350) / 100.0, 0.5, 3.5);
maxStepsTilt = constrain(map(pTiltMax, 0, 1023, 64, DEFAULT_TILT_STEPS), 32, DEFAULT_TILT_STEPS);
maxStepsPan = constrain(map(pPanMax, 0, 1023, 64, DEFAULT_PAN_STEPS), 64, DEFAULT_PAN_STEPS);
for (int i = 0; i < MUESTRAS; i++) {
int raw = analogRead(AUDIO_PIN);
int v = raw / 4 - 128;
if (v < -128) v = -128;
if (v > 127) v = 127;
vReal[i] = (char)v;
vImag[i] = 0;
}
aplicaVentana(vReal);
fix_fft(vReal, vImag, LOGM, 0);
// magnitudes
for (int b = 0; b < MUESTRAS/2; b++) {
int re = (int)vReal[b];
int im = (int)vImag[b];
unsigned long mag = (unsigned long)(re * re) + (unsigned long)(im * im);
magn[b] = mag;
}
unsigned long sumB = 0;
for (int b = BIN_OFFSET; b <= binBMax; b++) sumB += magn[b];
unsigned long sumM = 0;
for (int b = binBMax + 1; b <= binMMax; b++) sumM += magn[b];
unsigned long sumA = 0;
for (int b = binMMax + 1; b < MUESTRAS/2; b++) sumA += magn[b];
// smoothing
smoothB = smoothB * 0.75 + sumB * 0.25;
smoothM = smoothM * 0.75 + sumM * 0.25;
smoothA = smoothA * 0.75 + sumA * 0.25;
float normB = constrain((sumB / (float)MAX_MAGN_EXPECTED) * gainRGB, 0.0, 1.0);
float normM = constrain((sumM / (float)MAX_MAGN_EXPECTED) * gainRGB, 0.0, 1.0);
float normA = constrain((sumA / (float)MAX_MAGN_EXPECTED) * gainRGB, 0.0, 1.0);
float totalEnergy = constrain((sumB + sumM + sumA) / (3.0 * (float)MAX_MAGN_EXPECTED) * gainRGB, 0.0, 1.0);
bool beatB = (sumB > smoothB * 1.6);
bool beatA = (sumA > smoothA * 1.6);
if (beatB) {
int dir = (random(0,2) ? 1 : -1);
kickTargetTilt = centerTilt + dir * (kickStepsTiltBase + (int)(normB * kickStepsTiltBase));
kickEndTilt = now + kickDecayMs;
}
if (beatA) {
int dir = (random(0,2) ? 1 : -1);
kickTargetPan = centerPan + dir * (kickStepsPanBase + (int)(normA * kickStepsPanBase));
kickEndPan = now + kickDecayMs;
}
bool strobeNow = false;
if (strobeEnabled && sumA > strobeThreshold && currentMode == MODE_STROBE) {
strobeNow = true;
lastStrobe = now;
}
if (strobeNow) {
flashRGB(255,255,255);
} else {
renderRGB(normB, normM, normA, totalEnergy, beatB, beatA);
}
float panSpeedFactor = 0.5 + totalEnergy * 3.0;
float tiltSpeedFactor = 0.5 + totalEnergy * 3.5;
phasePan += basePhaseInc * panSpeedFactor;
phaseTilt += basePhaseInc * tiltSpeedFactor;
long targetPan = centerPan + (long)( sin(phasePan) * maxStepsPan * totalEnergy );
long targetTilt = centerTilt + (long)( sin(phaseTilt) * maxStepsTilt * totalEnergy );
if (now < kickEndPan) {
float frac = float(kickEndPan - now) / float(kickDecayMs);
frac = constrain(frac, 0.0, 1.0);
targetPan = (long)( targetPan * (1.0 - frac) + kickTargetPan * frac );
} else {
kickTargetPan = centerPan;
}
if (now < kickEndTilt) {
float frac = float(kickEndTilt - now) / float(kickDecayMs);
frac = constrain(frac, 0.0, 1.0);
targetTilt = (long)( targetTilt * (1.0 - frac) + kickTargetTilt * frac );
} else {
kickTargetTilt = centerTilt;
}
if (targetPan > centerPan + maxStepsPan) targetPan = centerPan + maxStepsPan;
if (targetPan < centerPan - maxStepsPan) targetPan = centerPan - maxStepsPan;
if (targetTilt > centerTilt + maxStepsTilt) targetTilt = centerTilt + maxStepsTilt;
if (targetTilt < centerTilt - maxStepsTilt) targetTilt = centerTilt - maxStepsTilt;
switch (currentMode) {
case MODE_SIN:
break;
case MODE_BEAT:
if (!beatA && !beatB) {
targetPan = (long)(targetPan * 0.8 + centerPan * 0.2);
targetTilt = (long)(targetTilt * 0.8 + centerTilt * 0.2);
}
break;
case MODE_RANDOM:
targetPan += random(-20, 21);
targetTilt += random(-15, 16);
break;
case MODE_IDLE:
targetPan = (long)(centerPan + sin(phasePan) * (maxStepsPan * 0.15));
targetTilt = (long)(centerTilt + sin(phaseTilt) * (maxStepsTilt * 0.12));
break;
case MODE_STROBE:
break;
}
stepperHoriz.moveTo(targetPan);
stepperVert.moveTo(targetTilt);
stepperHoriz.run();
stepperVert.run();
handleButton();
delay(sampleDelayMs);
}
void aplicaVentana(char *vData) {
double n1 = double(MUESTRAS) - 1.0;
for (int i = 0; i < MUESTRAS/2; i++){
double r = double(i) / n1;
double w = 0.5 * (1.0 - cos(6.283185307179586 * r));
vData[i] = (char)((double)vData[i] * w);
vData[MUESTRAS - 1 - i] = (char)((double)vData[MUESTRAS - 1 - i] * w);
}
}
void renderRGB(float nB, float nM, float nA, float energy, bool beatB, bool beatA) {
// build components
float rf = nA; // agudos -> rojo
float gf = nM; // medios -> verde
float bf = nB; // bajos -> azul
rf = constrain(rf * 0.9 + energy * 0.4, 0.0, 1.0);
gf = constrain(gf * 0.9 + energy * 0.4, 0.0, 1.0);
bf = constrain(bf * 0.9 + energy * 0.4, 0.0, 1.0);
if (beatB) bf = min(1.0, bf + 0.45);
if (beatA) rf = min(1.0, rf + 0.45);
uint8_t R = (uint8_t)(rf * 255.0);
uint8_t G = (uint8_t)(gf * 255.0);
uint8_t B = (uint8_t)(bf * 255.0);
analogWrite(PIN_R, R);
analogWrite(PIN_G, G);
analogWrite(PIN_B, B);
}
void flashRGB(uint8_t R, uint8_t G, uint8_t B) {
analogWrite(PIN_R, R);
analogWrite(PIN_G, G);
analogWrite(PIN_B, B);
}
// ---------------- BUTTON / EEPROM ----------------
unsigned long btnPressStart = 0;
bool btnDown = false;
void handleButton() {
bool pressed = (digitalRead(BUTTON_PIN) == LOW);
unsigned long now = millis();
if (pressed && !btnDown) {
btnDown = true;
btnPressStart = now;
} else if (!pressed && btnDown) {
btnDown = false;
unsigned long held = now - btnPressStart;
if (held >= 2000) {
saveConfigToEEPROM();
flashRGB(255,255,255);
delay(80);
renderRGB(0,0,0,0,false,false);
} else {
int next = (int(currentMode) + 1) % 5;
currentMode = (MODE)next;
}
}
}
void saveConfigToEEPROM() {
EEPROM.update(EE_ADDR_VALID, 0xA5);
EEPROM.update(EE_ADDR_MODE, (uint8_t)currentMode);
EEPROM.update(EE_ADDR_PAN, (uint8_t)(maxStepsPan & 0xFF));
EEPROM.update(EE_ADDR_PAN+1, (uint8_t)((maxStepsPan >> 8) & 0xFF));
EEPROM.update(EE_ADDR_TILT, (uint8_t)(maxStepsTilt & 0xFF));
EEPROM.update(EE_ADDR_TILT+1, (uint8_t)((maxStepsTilt >> 8) & 0xFF));
EEPROM.update(EE_ADDR_GAIN, (uint8_t)constrain((int)(gainRGB*50), 0, 255));
}
void loadConfigFromEEPROM() {
uint8_t v = EEPROM.read(EE_ADDR_VALID);
if (v == 0xA5) {
uint8_t m = EEPROM.read(EE_ADDR_MODE);
if (m <= 4) currentMode = (MODE)m;
uint16_t pan = EEPROM.read(EE_ADDR_PAN) | (EEPROM.read(EE_ADDR_PAN+1) << 8);
uint16_t tilt = EEPROM.read(EE_ADDR_TILT) | (EEPROM.read(EE_ADDR_TILT+1) << 8);
if (pan >= 64 && pan <= DEFAULT_PAN_STEPS) maxStepsPan = pan;
if (tilt >= 32 && tilt <= DEFAULT_TILT_STEPS) maxStepsTilt = tilt;
uint8_t g = EEPROM.read(EE_ADDR_GAIN);
if (g > 0) gainRGB = constrain(g / 50.0, 0.5, 5.0);
}
}