Light Storm 289

Difficulty: ● ● ● ● ●

  • Did you know ?

    Did you know the human eye can perceive over 10 million colors, but your brain processes synchronized light and sound up to 60% faster when they’re in rhythm? That’s why audio‑reactive LEDs feel so hypnotic.

No products found in parts_required metafield

How It Works ?

Sound detection: MEMS mic on GPIO4 → captures ambient music
Frequency analysis: FFT splits bass, mids, highs → removes noise
Color mapping: Bass = red, mids = green, highs = blue
LED output: 289‑pixel matrix lights with ripples → off when silent

GENESIS

Connection Guide

Snap your modules as shown above, copy the code into the Arduino IDE and Click upload

example.ino
#include <WiFi.h>
#include <WebServer.h>
#include <arduinoFFT.h>
#include <FastLED.h>
#include <math.h>

// ======= WIFI & WEB SERVER =======
const char* ssid = "NOS-EA84";
const char* password = "M3FMPC3E31";
IPAddress local_IP(192,168,1,27);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);
WebServer server(80);

// ======= AUDIO & FFT SETUP =======
#define SAMPLES 256
#define SAMPLING_FREQUENCY 16000
const int micPin = 4;  // MEMS Mic on GPIO4

double vReal[SAMPLES];
double vImag[SAMPLES];
ArduinoFFT<double> FFT(vReal, vImag, SAMPLES, SAMPLING_FREQUENCY);

const int noiseThresholdBass  = 350;
const int noiseThresholdMid   = 80;
const int noiseThresholdHigh  = 50;

int bassLevel = 100, midsLevel = 100, highsLevel = 100;
double peakAmplitudeBass = 300, peakAmplitudeMid = 300, peakAmplitudeHigh = 300;
unsigned long lastLoudBass = 0, lastLoudMid = 0, lastLoudHigh = 0;
const unsigned long resetDelay = 2000;

// ======= LED MATRIX SETUP =======
#define MATRIX_PIN 10
#define NUM_LEDS 289
#define BRIGHTNESS 40
CRGB leds[NUM_LEDS];

// Matrix mapping constants from your original code
#define NUM_ROWS 21
const uint8_t rowCount[NUM_ROWS] = { 5, 9, 11, 13, 15, 15, 17, 17, 17, 17, 17, 17, 17, 17, 17, 15, 15, 13, 11, 9, 5 };
const uint8_t rowOffset[NUM_ROWS] = { 6, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 6 };
const uint16_t prefix[NUM_ROWS] PROGMEM = { 0, 5, 14, 25, 38, 53, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 236, 251, 264, 275, 284 };
#define INVALID 0xFFFF
uint16_t XY(uint8_t x, uint8_t y) {
  if (y >= NUM_ROWS || x < rowOffset[y] || x >= rowOffset[y] + rowCount[y]) return INVALID;
  return pgm_read_word_near(prefix + y) + (x - rowOffset[y]);
}

const float CENTER_X = 8.0;
const float CENTER_Y = 10.0;

// ======= ANIMATION MODES =======
enum Mode { AMBIENT, SWIRL, RIPPLE };
Mode currentMode = AMBIENT;

// ======= WEB CONTROL VARIABLES =======
bool patternChanged = false;

// ======= HTML PAGE with sliders + pattern buttons =======
String htmlPage() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; text-align:center; background:#111; color:#fff; }
.container { width:90%; max-width:500px; margin:auto; }
h2 { margin-bottom:20px; font-weight:bold; font-size:24px; }
.slider-row, .button-row { display:flex; justify-content:center; align-items:center; margin:15px 0; }
.label { width:120px; text-align:right; margin-right:10px; font-size:18px; }
input[type=range] {
  -webkit-appearance:none; width:100%; height:15px; border-radius:5px;
  background:#444; outline:none; opacity:0.8; transition:opacity .2s;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance:none; width:25px; height:25px; border-radius:50%;
  background:#fff; cursor:pointer;
}
input[type=range].bass::-webkit-slider-runnable-track { background: linear-gradient(to right, red 0%, red 100%); }
input[type=range].mids::-webkit-slider-runnable-track { background: linear-gradient(to right, green 0%, green 100%); }
input[type=range].highs::-webkit-slider-runnable-track { background: linear-gradient(to right, blue 0%, blue 100%); }

button {
  background:#222; border:none; color:#fff; padding:10px 20px; margin: 0 10px;
  font-size:18px; border-radius:8px; cursor:pointer;
  transition: background-color 0.3s ease;
}
button:hover { background:#555; }
button.selected { background:#0f0; color:#000; font-weight:bold; }
</style>
</head>
<body>
<div class="container">
  <h2>Light Music on GENESIS</h2>
  <div class="slider-row">
    <div class="label">Bass</div>
    <input type="range" min="0" max="100" value="%BASS%" class="bass" oninput="updateSlider('bass',this.value)">
  </div>
  <div class="slider-row">
    <div class="label">Mids</div>
    <input type="range" min="0" max="100" value="%MIDS%" class="mids" oninput="updateSlider('mids',this.value)">
  </div>
  <div class="slider-row">
    <div class="label">Highs</div>
    <input type="range" min="0" max="100" value="%HIGHS%" class="highs" oninput="updateSlider('highs',this.value)">
  </div>

  <div class="button-row">
    <button id="btnAmbient" onclick="setPattern('0')">Ambient</button>
    <button id="btnSwirl" onclick="setPattern('1')">Swirl</button>
    <button id="btnRipple" onclick="setPattern('2')">Ripple</button>
  </div>
</div>

<script>
function updateSlider(type, value) {
  fetch('/set?type='+type+'&value='+value);
}

function setPattern(mode) {
  fetch('/setpattern?mode='+mode).then(() => {
    updateButtons(mode);
  });
}

function updateButtons(selectedMode) {
  document.getElementById('btnAmbient').classList.remove('selected');
  document.getElementById('btnSwirl').classList.remove('selected');
  document.getElementById('btnRipple').classList.remove('selected');

  if(selectedMode == '0') document.getElementById('btnAmbient').classList.add('selected');
  else if(selectedMode == '1') document.getElementById('btnSwirl').classList.add('selected');
  else if(selectedMode == '2') document.getElementById('btnRipple').classList.add('selected');
}

// Initialize selected button on page load
window.onload = () => {
  updateButtons("%MODE%");
};
</script>

</body>
</html>
)rawliteral";

  html.replace("%BASS%", String(bassLevel));
  html.replace("%MIDS%", String(midsLevel));
  html.replace("%HIGHS%", String(highsLevel));
  html.replace("%MODE%", String((int)currentMode));
  return html;
}

// ======= WEB HANDLERS =======
void handleRoot() { server.send(200, "text/html", htmlPage()); }
void handleSet() {
  if (server.hasArg("type") && server.hasArg("value")) {
    String type = server.arg("type");
    int value = server.arg("value").toInt();
    if (type == "bass") bassLevel = value;
    else if (type == "mids") midsLevel = value;
    else if (type == "highs") highsLevel = value;
  }
  server.send(200, "text/plain", "OK");
}

void handleSetPattern() {
  if (server.hasArg("mode")) {
    int mode = server.arg("mode").toInt();
    if (mode >= 0 && mode <= 2) {
      currentMode = (Mode)mode;
      patternChanged = true;
    }
  }
  server.send(200, "text/plain", "OK");
}

// ======= UTILITY =======
int getEffectiveThreshold(int sliderValue, int baseThreshold) {
  return baseThreshold + (sliderValue * baseThreshold * 2) / 100;
}

// ======= MATRIX PATTERNS =======
void drawAmbient() {
  static uint8_t hue = 0;
  hue += 2;
  CHSV color(hue, 255, 255);
  for (int y = 0; y < NUM_ROWS; y++) {
    for (int x = rowOffset[y]; x < rowOffset[y] + rowCount[y]; x++) {
      uint16_t i = XY(x, y);
      if (i != INVALID) leds[i] = color;
    }
  }
}

void drawSwirl() {
  static float swirlAngle = 0;
  swirlAngle += 2.0;
  if (swirlAngle >= 360.0) swirlAngle -= 360.0;
  for (int y = 0; y < NUM_ROWS; y++) {
    for (int x = rowOffset[y]; x < rowOffset[y] + rowCount[y]; x++) {
      uint16_t i = XY(x, y);
      if (i == INVALID) continue;
      float dx = x - CENTER_X;
      float dy = y - CENTER_Y;
      float angle = atan2(dy, dx) * 180.0 / PI;
      if (angle < 0) angle += 360.0;
      float dist = sqrt(dx * dx + dy * dy);
      float h = fmod(angle + swirlAngle + dist * 10, 360.0);
      leds[i] = CHSV(h / 360.0 * 255, 255, 255);
    }
  }
}

void drawRipple() {
  static float rippleTime = 0;
  rippleTime += 0.05;
  for (int y = 0; y < NUM_ROWS; y++) {
    for (int x = rowOffset[y]; x < rowOffset[y] + rowCount[y]; x++) {
      uint16_t i = XY(x, y);
      if (i == INVALID) continue;
      float dx = x - CENTER_X;
      float dy = y - CENTER_Y;
      float dist = sqrt(dx * dx + dy * dy);
      float h = fmod((dist * 30.0) - (rippleTime * 100), 360.0);
      if (h < 0) h += 360.0;
      leds[i] = CHSV(h / 360.0 * 255, 255, 255);
    }
  }
}

// ======= FFT TASK PINNED TO CORE 0 =======
void sampleAndProcessFFT(void * parameter) {
  for (;;) {
    unsigned long startMicros = micros();
    for (int i = 0; i < SAMPLES; i++) {
      vReal[i] = analogRead(micPin) - 2048;
      vImag[i] = 0;
      while (micros() - startMicros < (i + 1) * (1000000UL / SAMPLING_FREQUENCY)) {}
    }

    FFT.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.compute(FFT_FORWARD);
    FFT.complexToMagnitude();

    double bassEnergy = 0, midEnergy = 0, highEnergy = 0;
    int bassBins = 0, midBins = 0, highBins = 0;

    for (int i = 1; i < (SAMPLES / 2); i++) {
      double freq = (i * SAMPLING_FREQUENCY) / SAMPLES;
      if (freq >= 150 && freq <= 250) {
        bassEnergy += vReal[i];
        bassBins++;
      }
      else if (freq > 250 && freq <= 2000) {
        midEnergy += vReal[i];
        midBins++;
      }
      else if (freq > 2000 && freq <= 18000) {
        highEnergy += vReal[i];
        highBins++;
      }
    }

    int effBassThresh = getEffectiveThreshold(bassLevel, noiseThresholdBass);
    int effMidThresh = getEffectiveThreshold(midsLevel, noiseThresholdMid);
    int effHighThresh = getEffectiveThreshold(highsLevel, noiseThresholdHigh);

    double avgBass = bassBins ? bassEnergy / bassBins : 0;
    double avgMid = midBins ? midEnergy / midBins : 0;
    double avgHigh = highBins ? highEnergy / highBins : 0;

    double signalBass = max(avgBass - effBassThresh, 0.0);
    double signalMid = max(avgMid - effMidThresh, 0.0);
    double signalHigh = max(avgHigh - effHighThresh, 0.0);
    signalHigh *= 2.0;

    if (signalBass < 20) signalBass = 0;
    if (signalMid < 20) signalMid = 0;
    if (signalHigh < 20) signalHigh = 0;

    unsigned long now = millis();
    if (signalBass > peakAmplitudeBass) peakAmplitudeBass = signalBass;
    if (signalMid > peakAmplitudeMid) peakAmplitudeMid = signalMid;
    if (signalHigh > peakAmplitudeHigh) peakAmplitudeHigh = signalHigh;
    if (now - lastLoudBass > resetDelay) peakAmplitudeBass = 300;
    if (now - lastLoudMid > resetDelay) peakAmplitudeMid = 300;
    if (now - lastLoudHigh > resetDelay) peakAmplitudeHigh = 300;
    if (signalBass > 0) lastLoudBass = now;
    if (signalMid > 0) lastLoudMid = now;
    if (signalHigh > 0) lastLoudHigh = now;

    // Calculate combined signal presence for lighting matrix ON/OFF
    double combinedSignal = signalBass + signalMid + signalHigh;

    if (combinedSignal > 0) {
      // Light ON - run selected pattern
      switch (currentMode) {
        case AMBIENT: drawAmbient(); break;
        case SWIRL: drawSwirl(); break;
        case RIPPLE: drawRipple(); break;
      }
    } else {
      // No sound - clear LEDs
      fill_solid(leds, NUM_LEDS, CRGB::Black);
    }

    FastLED.setBrightness(BRIGHTNESS);
    FastLED.show();

    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.config(local_IP, gateway, subnet);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected: " + WiFi.localIP().toString());

  server.on("/", handleRoot);
  server.on("/set", handleSet);
  server.on("/setpattern", handleSetPattern);
  server.begin();

  pinMode(micPin, INPUT);

  FastLED.addLeds<WS2812B, MATRIX_PIN, GRB>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);

  // Create FFT task pinned to core 0
  xTaskCreatePinnedToCore(sampleAndProcessFFT, "FFT", 8192, NULL, 1, NULL, 0);
}

void loop() {
  server.handleClient();
}