Skip to content

[WIP] Add new audio-reactive palettes for smoothed FFT data#5467

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/add-smooth-colour-palettes
Draft

[WIP] Add new audio-reactive palettes for smoothed FFT data#5467
Copilot wants to merge 2 commits intomainfrom
copilot/add-smooth-colour-palettes

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 1, 2026

  • Update MAX_PALETTES from 3 to 5
  • Add paletteBandAvg[NUM_GEQ_CHANNELS] static array alongside existing FFT arrays
  • Add EMA update for paletteBandAvg in fillAudioPalettes() before the palette loop
  • Add case 3 ("Track Character") — spectral centroid from smoothed bands drives hue (warm=bass, cool=treble)
  • Add case 4 ("Spectral Balance") — bass/mid/high energy ratios drive hue with smooth transitions
  • Reset paletteBandAvg in onUpdateBegin() alongside other FFT buffer resets
  • Run npm test to validate
  • Run pio run -e esp32dev hardware build
Original prompt

Problem

The AudioReactive usermod currently contains three colour palettes (defined in getCRGBForBand() at line 2211 of usermods/audioreactive/audio_reactive.cpp) that are updated every frame using raw fftResult[] values. While this makes them very responsive, it also makes them "twitchy" — they react to instantaneous FFT values rather than reflecting the overall character or balance of the music.

Goal

Add new audio-reactive palette(s) that use smoothed/averaged FFT data to reflect the overall tonal balance of the music. The key user experience should be:

  • Warm red/orange hues when bass frequencies dominate (e.g., a heavy drop)
  • Green/cyan hues during vocal/melodic passages (mid-frequency dominant)
  • Blue/purple hues when high-frequency content dominates (cymbals, hi-hats, bright synths)
  • Smooth, flowing transitions between colours rather than frame-by-frame twitching

Implementation Details

All changes should be in usermods/audioreactive/audio_reactive.cpp.

1. Add a smoothed band average buffer for palette use

Add a static float paletteBandAvg[NUM_GEQ_CHANNELS] array (alongside the existing fftCalc, fftAvg, fftResult arrays near line 227). This buffer should be updated with an exponential moving average (EMA) in fillAudioPalettes() before the palette colours are computed:

static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f};
static constexpr float PALETTE_SMOOTHING = 0.05f; // ~400ms time constant at 20ms cycle

// In fillAudioPalettes(), before the palette loop:
for (int i = 0; i < NUM_GEQ_CHANNELS; i++) {
  paletteBandAvg[i] += PALETTE_SMOOTHING * ((float)fftResult[i] - paletteBandAvg[i]);
}

2. Add a "Spectral Centroid / Track Character" palette

Add a new case 3: to the switch (pal) in getCRGBForBand(). This palette computes the spectral centroid (centre-of-mass of the frequency spectrum) from the smoothed band averages and maps it to hue:

case 3: {
  // "Track Character" palette - smoothed spectral centroid drives hue
  static const float bandFreq[NUM_GEQ_CHANNELS] = {
    65, 107, 172, 258, 365, 495, 689, 969,
    1270, 1658, 2153, 2713, 3359, 4091, 5792, 8182
  };
  float wSum = 0, tEnergy = 0;
  for (int i = 0; i < NUM_GEQ_CHANNELS; i++) {
    wSum += paletteBandAvg[i] * bandFreq[i];
    tEnergy += paletteBandAvg[i];
  }
  float centroid = (tEnergy > 1.0f) ? (wSum / tEnergy) : 500.0f;
  float logC = log2f(constrain(centroid, 60.0f, 8000.0f));
  // log2(60)≈5.9, log2(8000)≈13.0 → map to hue 0..200
  uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f);

  // Spread palette positions around centroid hue
  int8_t hueSpread = map(x, 0, 255, -30, 30);
  uint8_t saturation = constrain((int)(tEnergy / 6.0f) + 180, 180, 255);
  hsv = CHSV(baseHue + hueSpread, saturation, constrain(x, 30, 255));
  value = hsv;
  break;
}

3. Add a "Spectral Balance" palette

Add a new case 4: to the switch (pal) in getCRGBForBand(). This palette computes energy ratios across bass/mid/high regions and uses the weighted balance to derive hue:

case 4: {
  // "Spectral Balance" palette - bass vs mid vs high energy balance
  float bassEnergy = 0, midEnergy = 0, highEnergy = 0;
  for (int i = 0;  i < 4;  i++) bassEnergy += paletteBandAvg[i];
  for (int i = 4;  i < 10; i++) midEnergy  += paletteBandAvg[i];
  for (int i = 10; i < 16; i++) highEnergy += paletteBandAvg[i];

  float total = bassEnergy + midEnergy + highEnergy;
  if (total < 1.0f) total = 1.0f;

  float bassRatio = bassEnergy / total;
  float midRatio  = midEnergy  / total;
  float highRatio = highEnergy / total;

  // Weighted hue: bass→warm(20), mid→green(110), high→cool(190)
  uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f);

  // More concentrated spectrum = more saturated
  float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio));
  uint8_t sat = constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255);

  // Spread across palette position
  int8_t hueOffset = map(x, 0, 255, -25, 25);
  uint8_t val = constrain((int)(total / 8.0f) + (int)map(x, 0, 255, 30, 255), 30, 255);
  hsv = CHSV(hue + hueOffset, sat, val);
  value = hsv;
  break;
}

4. Update MAX_PALETTES

Change line 74 from:

#define MAX_PALETTES 3

to:

#define MAX_PALETTES 5

5. Reset smoothing buffer on audio reset

In onUpdateBegin() (around line 1683), add a reset of paletteBandAvg alongside the existing memset(fftCalc, ...) calls:

memset(paletteBandAvg, 0, sizeof(paletteBandAvg));

Key constraints

  • All changes are in the single file usermods/audioreactive/audio_reactive.cpp
  • Memory impact is minimal: one additional float[16] array = 64 bytes
  • CPU impact is negligible: a few multiply-adds per frame
  • The existing 3 palettes (cases 0, 1, 2) must remain unchanged for backward compatibility
  • The paletteBandAvg array must be declared static at file scope alongside the existing FFT...

This pull request was created from Copilot chat.

…ral Balance)

Agent-Logs-Url: https://github.com/wled/WLED/sessions/a11e5210-96ae-4713-abdd-ba42b0e33bc5

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
Copilot AI requested a review from netmindz April 1, 2026 08:31
Copilot stopped work on behalf of netmindz due to an error April 1, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants