From 06326b0cfab1fbb9c5756322d25b3bc412259ffa Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 12 Sep 2026 09:47:36 +0100 Subject: [PATCH 1/5] Add audioreactive color palettes (Ratio, Hue, Spectrum) Port the getCRGBForBand palette generation logic from the upstream WLED usermods/audioreactive fork, adding an opt-in 'add-palettes' setting that registers three audio-reactive color palettes with the core palette system and refreshes them each loop. --- audio_reactive.h | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/audio_reactive.h b/audio_reactive.h index 040c3ca6..672cbdf0 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -145,6 +145,7 @@ static bool audioSyncBroadcast = false; // if true, use subnet static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! +#define MAX_PALETTES 3 // number of audioreactive palettes provided by this usermod // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 @@ -1205,6 +1206,8 @@ class AudioReactive : public Usermod { bool enabled = false; #endif bool initDone = false; + bool addPalettes = false; + int8_t palettes = 0; // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) @@ -1254,8 +1257,16 @@ class AudioReactive : public Usermod { static const char _digitalmic[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; + static const char _addPalettes[]; + static const char _palName0[]; + static const char _palName1[]; + static const char _palName2[]; // private methods + void removeAudioPalettes(void); + void createAudioPalettes(void); + CRGB getCRGBForBand(int x, int pal); + void fillAudioPalettes(void); //////////////////// // Debug support // @@ -2150,6 +2161,7 @@ class AudioReactive : public Usermod { receivedFormat = 0; delay(100); if (enabled) connectUDPSoundSync(); + if (enabled && addPalettes) createAudioPalettes(); initDone = true; DEBUGSR_PRINT(F("AR: init done, enabled = ")); DEBUGSR_PRINTLN(enabled ? F("true.") : F("false.")); @@ -2456,6 +2468,8 @@ class AudioReactive : public Usermod { lastTime = millis(); } #endif + + fillAudioPalettes(); } #if defined(_MoonModules_WLED_) && defined(WLEDMM_FASTPATH) @@ -2800,6 +2814,11 @@ class AudioReactive : public Usermod { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); if (prevEnabled != enabled) onUpdateBegin(!enabled); + if (addPalettes) { + // add/remove custom/audioreactive palettes + if (prevEnabled && !enabled) removeAudioPalettes(); + if (!prevEnabled && enabled) createAudioPalettes(); + } } #ifdef ARDUINO_ARCH_ESP32 if (usermod[FPSTR(_inputLvl)].is()) { @@ -2809,6 +2828,13 @@ class AudioReactive : public Usermod { } } + void onStateChange(uint8_t callMode) { + if (initDone && enabled && addPalettes && palettes==0) { + // if palettes were removed during JSON call re-add them + createAudioPalettes(); + } + } + /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. @@ -2848,6 +2874,7 @@ class AudioReactive : public Usermod { void addToConfig(JsonObject& root) override { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; + top[FPSTR(_addPalettes)] = addPalettes; #ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); @@ -2920,6 +2947,7 @@ class AudioReactive : public Usermod { bool readFromConfig(JsonObject& root) override { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); + bool oldAddPalettes = addPalettes; #ifdef ARDUINO_ARCH_ESP32 // remember previous values @@ -2932,6 +2960,7 @@ class AudioReactive : public Usermod { #endif configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); #ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); @@ -2985,6 +3014,14 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); configComplete &= getJsonValue(top["sync"][F("broadcast")], audioSyncBroadcast); +#ifdef ARDUINO_ARCH_ESP32 + if (initDone) { + // add/remove custom/audioreactive palettes + if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); + if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); + } +#endif + // WLEDMM notify user when a reboot is necessary #ifdef ARDUINO_ARCH_ESP32 if (initDone) { @@ -3289,6 +3326,90 @@ class AudioReactive : public Usermod { } }; +void AudioReactive::removeAudioPalettes(void) { + DEBUG_PRINTLN(F("Removing audio palettes.")); + palettes -= (int8_t)removeUsermodPalettes(_name); + if (palettes < 0) palettes = 0; // safeguard +} + +void AudioReactive::createAudioPalettes(void) { + if (palettes) return; + DEBUG_PRINTLN(F("Adding audio palettes.")); + static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2}; + for (int i=0; i Date: Sat, 12 Sep 2026 11:43:41 +0100 Subject: [PATCH 2/5] Add audioreactive palettes 'Track Character' and 'Spectral Balance' Ports upstream WLED PR #5467: adds two new audio-reactive palettes derived from a slowly-smoothed (~390ms EMA) per-band average, using spectral centroid and bass/mid/high energy ratio to drive hue/saturation. --- audio_reactive.h | 150 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 2 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 672cbdf0..284dbdf9 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -145,7 +145,7 @@ static bool audioSyncBroadcast = false; // if true, use subnet static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! -#define MAX_PALETTES 3 // number of audioreactive palettes provided by this usermod +#define MAX_PALETTES 5 // number of audioreactive palettes provided by this usermod // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 @@ -176,6 +176,16 @@ static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0}; // Our calculated freq. chann static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. (also used by dynamics limiter) static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages used only by audio palettes 3 & 4 (EMA, ~390ms time constant, cadence-independent - see fillAudioPalettes()) +static constexpr float PALETTE_TIME_CONSTANT_MS = 390.0f; // time constant for paletteBandAvg smoothing; derived from original alpha=0.05 @ 20ms cycle (tau = -20ms / ln(1-0.05)) +// Aggregates derived from paletteBandAvg[], precomputed once per fillAudioPalettes() refresh +// for use by getCRGBForBand() palettes 3 & 4 (see there for details). +static uint8_t paletteHue3 = 0; // case 3: spectral-centroid hue +static uint8_t paletteSat3 = 180; // case 3: loudness-derived saturation +static uint8_t paletteHue4 = 0; // case 4: bass/mid/high-weighted hue +static uint8_t paletteSat4 = 180; // case 4: dominance-derived saturation +static float paletteTotal4 = 0.0f;// case 4: total band energy (drives brightness) + static uint16_t zeroCrossingCount = 0; // number of zero crossings in the current batch of 512 samples // TODO: probably best not used by receive nodes @@ -1261,6 +1271,8 @@ class AudioReactive : public Usermod { static const char _palName0[]; static const char _palName1[]; static const char _palName2[]; + static const char _palName3[]; + static const char _palName4[]; // private methods void removeAudioPalettes(void); @@ -2506,6 +2518,7 @@ class AudioReactive : public Usermod { memset(fftCalc, 0, sizeof(fftCalc)); memset(fftAvg, 0, sizeof(fftAvg)); memset(fftResult, 0, sizeof(fftResult)); + memset(paletteBandAvg, 0, sizeof(paletteBandAvg)); for(int i=(init?0:1); i 2000UL) dtMs = 2000UL; // cap a single "catch-up" jump after a long stall (OTA, file I/O) + float alpha = (float)dtMs / (PALETTE_TIME_CONSTANT_MS + (float)dtMs); + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + paletteBandAvg[i] += alpha * ((float)fftResult[i] - paletteBandAvg[i]); + } + + // Precompute the x-independent aggregates for palettes 3 & 4 once per refresh (see + // getCRGBForBand() cases 3 & 4), instead of redoing these 16-channel scans on each of + // the 3 getCRGBForBand() calls (x=1/128/255) made below for every refresh. + { + // Palette 3 ("Track Character"): spectral centroid → hue + static const float bandFreq[NUM_GEQ_CHANNELS] = { // approximate centre frequency (Hz) of each GEQ channel + 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]; // frequency-weighted energy + tEnergy += paletteBandAvg[i]; // total energy + } + // centroid = energy-weighted average frequency; default to 500 Hz when signal is silent + float centroid = (tEnergy > 1.0f) ? (wSum / tEnergy) : 500.0f; + // Map centroid to hue on a log scale (human pitch perception is logarithmic). + // ln(60 Hz) ≈ 4.0943, ln(8000 Hz) ≈ 8.9872 → hue range 0..200 (red → blue-purple). + // Using logf() rather than log2f() avoids pulling in a second libm log function; + // logf() is already linked into this file elsewhere. Bounds are rounded outward so + // mapf()'s result can't go slightly negative. + float logC = logf(constrain(centroid, 60.0f, 8000.0f)); + paletteHue3 = (uint8_t)mapf(logC, 4.0943f, 8.9872f, 0.0f, 200.0f); // mapf() cannot produce negative results due to previous constrain() --> safe to directly cast to uint8_t + // Saturation rises with overall loudness. tEnergy is the SUM of 16 EMA'd bands, so its + // theoretical ceiling is 16*255=4080 - but real music never drives all 16 log-spaced + // bands to full scale simultaneously, so scaling against that ceiling pins saturation + // near its floor for any real signal. Use the AVERAGE band value instead (tEnergy/16), + // which is on the same 0..255 scale that any individual band is treated on elsewhere + // in this file (e.g. the `case 1`/`case 2` palettes above), so saturation actually + // varies across a musically realistic loudness range. + float avgEnergy3 = tEnergy / (float)NUM_GEQ_CHANNELS; + paletteSat3 = (uint8_t)constrain((int)mapf(avgEnergy3, 0.0f, 255.0f, 180.0f, 255.0f), 180, 255); + + // Palette 4 ("Spectral Balance"): bass/mid/high energy ratio → hue. + // Reuses tEnergy (== bassEnergy+midEnergy+highEnergy, since channels 0-15 are already + // summed above) as the total instead of re-summing it, to avoid two full 16-channel + // sums doing the same addition per refresh. + float bassEnergy = 0, midEnergy = 0, highEnergy = 0; + for (int i = 0; i < 4; i++) bassEnergy += paletteBandAvg[i]; // sub-bass + bass + for (int i = 4; i < 10; i++) midEnergy += paletteBandAvg[i]; // midrange + for (int i = 10; i < 16; i++) highEnergy += paletteBandAvg[i]; // high-mid + high + float total = (tEnergy < 1.0f) ? 1.0f : tEnergy; // avoid division by zero when silent + float bassRatio = bassEnergy / total; // fraction of energy in bass band + float midRatio = midEnergy / total; + float highRatio = highEnergy / total; + // Weighted hue: pure bass→20, pure mid→110, pure high→190 + int weightedHue = (int)roundf(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); + paletteHue4 = (uint8_t)constrain(weightedHue, 0, 255); + // Saturation: a clearly dominant band → high sat; a balanced spectrum → lower sat. + // maxRatio is the max of three ratios that sum to 1, so its real range is [1/3, 1], + // not [0, 1]; map that actual range onto [180, 255] instead of an arbitrary factor, + // so saturation actually varies instead of floor/ceiling-clamping almost always. + float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); + paletteSat4 = (uint8_t)constrain((int)mapf(maxRatio, 1.0f/3.0f, 1.0f, 180.0f, 255.0f), 180, 255); + paletteTotal4 = total; + } + // AI: end + // Scan by name pointer identity to find the palettes we added, palIndex = 0/1/2... selects the getCRGBForBand variant, matching how the entries were created. for (auto &ump : usermodPalettes) { if (ump.name != _name) continue; @@ -3424,3 +3568,5 @@ const char AudioReactive::_addPalettes[] PROGMEM = "add-palettes"; const char AudioReactive::_palName0[] PROGMEM = "Ratio"; const char AudioReactive::_palName1[] PROGMEM = "Hue"; const char AudioReactive::_palName2[] PROGMEM = "Spectrum"; +const char AudioReactive::_palName3[] PROGMEM = "Track Character"; +const char AudioReactive::_palName4[] PROGMEM = "Spectral Balance"; From 7fbea1cdf4691350421d98a638580b42a6f8873e Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:10:07 +0200 Subject: [PATCH 3/5] fixing some merge errors --- audio_reactive.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index ee159de8..e71c0578 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -3575,7 +3575,7 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { // scale as any individual band - see paletteSat3's comment for why AVERAGE rather than // the raw SUM in paletteTotal4 is used as the loudness reference) and palette position float avgEnergy4 = paletteTotal4 / (float)NUM_GEQ_CHANNELS; - uint8_t val = (uint8_t)constrain((int)mapf(avgEnergy4, 0.0f, 255.0f, 0.0f, 100.0f) + (int)map(x, 0, 255, 30, 255), 30, 255); + uint8_t val = (uint8_t)constrain((int)my_mapf(avgEnergy4, 0.0f, 255.0f, 0.0f, 100.0f) + (int)map(x, 0, 255, 30, 255), 30, 255); hsv = CHSV(hue, paletteSat4, val); value = hsv; break; @@ -3590,6 +3590,7 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2); } break; + } #endif return value; } @@ -3597,7 +3598,6 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { void AudioReactive::fillAudioPalettes() { #ifndef _MoonModules_WLED_ if (!palettes) return; - processingPalettes = true; // lock palettes to ensure they don't get removed while processing // AI: below section was generated by an AI // Update slowly-smoothed band averages used by palettes 3 & 4, so palette colours @@ -3642,7 +3642,7 @@ void AudioReactive::fillAudioPalettes() { // logf() is already linked into this file elsewhere. Bounds are rounded outward so // mapf()'s result can't go slightly negative. float logC = logf(constrain(centroid, 60.0f, 8000.0f)); - paletteHue3 = (uint8_t)mapf(logC, 4.0943f, 8.9872f, 0.0f, 200.0f); // mapf() cannot produce negative results due to previous constrain() --> safe to directly cast to uint8_t + paletteHue3 = (uint8_t)my_mapf(logC, 4.0943f, 8.9872f, 0.0f, 200.0f); // mapf() cannot produce negative results due to previous constrain() --> safe to directly cast to uint8_t // Saturation rises with overall loudness. tEnergy is the SUM of 16 EMA'd bands, so its // theoretical ceiling is 16*255=4080 - but real music never drives all 16 log-spaced // bands to full scale simultaneously, so scaling against that ceiling pins saturation @@ -3651,7 +3651,7 @@ void AudioReactive::fillAudioPalettes() { // in this file (e.g. the `case 1`/`case 2` palettes above), so saturation actually // varies across a musically realistic loudness range. float avgEnergy3 = tEnergy / (float)NUM_GEQ_CHANNELS; - paletteSat3 = (uint8_t)constrain((int)mapf(avgEnergy3, 0.0f, 255.0f, 180.0f, 255.0f), 180, 255); + paletteSat3 = (uint8_t)constrain((int)my_mapf(avgEnergy3, 0.0f, 255.0f, 180.0f, 255.0f), 180, 255); // Palette 4 ("Spectral Balance"): bass/mid/high energy ratio → hue. // Reuses tEnergy (== bassEnergy+midEnergy+highEnergy, since channels 0-15 are already @@ -3673,11 +3673,12 @@ void AudioReactive::fillAudioPalettes() { // not [0, 1]; map that actual range onto [180, 255] instead of an arbitrary factor, // so saturation actually varies instead of floor/ceiling-clamping almost always. float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); - paletteSat4 = (uint8_t)constrain((int)mapf(maxRatio, 1.0f/3.0f, 1.0f, 180.0f, 255.0f), 180, 255); + paletteSat4 = (uint8_t)constrain((int)my_mapf(maxRatio, 1.0f/3.0f, 1.0f, 180.0f, 255.0f), 180, 255); paletteTotal4 = total; } // AI: end + processingPalettes = true; // lock palettes to ensure they don't get removed while processing // Scan by name pointer identity to find the palettes we added, palIndex = 0/1/2... selects the getCRGBForBand variant, matching how the entries were created. for (auto &ump : usermodPalettes) { if (ump.name != _name) continue; From 5bb3da45dad61bf37e6079c1c17dcd463f0673b8 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:56:08 +0200 Subject: [PATCH 4/5] review feedback integration - move my_mapf() out of the ARDUINO_ARCH_ESP32 section - mark a few open ends as ToDO after merging to main --- audio_reactive.h | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index e71c0578..b230afce 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -203,6 +203,11 @@ static volatile bool isOOM = false; // FFTask: not enough #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! #define MAX_PALETTES 5 // number of audioreactive palettes provided by this usermod +// helper functions +static float my_mapf(float x, float in_min, float in_max, float out_min, float out_max){ // float version of map() + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 #ifndef SR_AGC // Automatic gain control mode @@ -269,7 +274,6 @@ static void detectSamplePeak(void); // peak detection function (needs scaled FF static void autoResetPeak(void); // peak auto-reset function static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) - #ifdef ARDUINO_ARCH_ESP32 // use audio source class (ESP32 specific) @@ -357,7 +361,6 @@ static volatile float micReal_max2 = 0.0f; // MicIn data max afte //////////////////// // some prototypes, to ensure consistent interfaces -static float my_mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float static float fftAddAvg(int from, int to); // average of several FFT result bins void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) @@ -490,12 +493,6 @@ constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range o #include // Helper functions - -// float version of map() -static float my_mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - // compute average of several FFT result bins // linear average static float fftAddAvgLin(int from, int to) { @@ -3615,7 +3612,10 @@ void AudioReactive::fillAudioPalettes() { unsigned long dtMs = lastPaletteTime ? (now - lastPaletteTime) : 20UL; lastPaletteTime = now; if (dtMs > 2000UL) dtMs = 2000UL; // cap a single "catch-up" jump after a long stall (OTA, file I/O) - float alpha = (float)dtMs / (PALETTE_TIME_CONSTANT_MS + (float)dtMs); + + float alpha = (float)dtMs / (PALETTE_TIME_CONSTANT_MS + (float)dtMs); // approximation + // float alpha = 1.0f - expf(-(float)dtMs / PALETTE_TIME_CONSTANT_MS); // (optional) exact value - better to preserve the time constant across loop cadences. + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { paletteBandAvg[i] += alpha * ((float)fftResult[i] - paletteBandAvg[i]); } @@ -3625,7 +3625,11 @@ void AudioReactive::fillAudioPalettes() { // the 3 getCRGBForBand() calls (x=1/128/255) made below for every refresh. { // Palette 3 ("Track Character"): spectral centroid → hue - static const float bandFreq[NUM_GEQ_CHANNELS] = { // approximate centre frequency (Hz) of each GEQ channel + // ToDO: This fixed frequency table does not match all supported FFT mappings. + // With freqDist=1, channel 9 receives fftAddAvg(19,25) (about 950 Hz at 22.05 kHz), but this table weights it as 1658 Hz. + // Track Character therefore overestimates the spectral centroid and shifts the hue. + // Additionally, check if RMS bin averaging can move the central frequencies. + static const float bandFreq[NUM_GEQ_CHANNELS] = { // approximate centre frequency (Hz) of each GEQ channel - need rework to match the frequency mappings of WLED-MM 65, 107, 172, 258, 365, 495, 689, 969, 1270, 1658, 2153, 2713, 3359, 4091, 5792, 8182 }; From 03a62fb1cb5abc73a73fb2e10816b29d0a422f8a Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:21:33 +0200 Subject: [PATCH 5/5] note a few more ToDO's --- audio_reactive.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/audio_reactive.h b/audio_reactive.h index b230afce..6e4e400a 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -3596,6 +3596,9 @@ void AudioReactive::fillAudioPalettes() { #ifndef _MoonModules_WLED_ if (!palettes) return; + // ToDO: Check if this functions needs a rate-limiter - it does not make sense to re-run the full math on every loop() iteration. + // (optionally) only re-run the function when new FFT results are availeable (sync with FFTTask) + // AI: below section was generated by an AI // Update slowly-smoothed band averages used by palettes 3 & 4, so palette colours // reflect the overall tonal character of the music rather than reacting to individual @@ -3639,6 +3642,11 @@ void AudioReactive::fillAudioPalettes() { tEnergy += paletteBandAvg[i]; // total energy } // centroid = energy-weighted average frequency; default to 500 Hz when signal is silent + // ToDo: When a high-frequency signal fades to silence, crossing tEnergy == 1 switches the centroid from about 8 kHz to 500 Hz in a single refresh. + // The base hue jumps from about 199 to 86, while case 3's bright endpoint stays at brightness 255. + // Smoothing the band energies therefore does not prevent an abrupt color change at silence. + // --> Retain the last valid hue below the threshold, or smooth the transition to the fallback hue. + // --> filter/smooth transitions float centroid = (tEnergy > 1.0f) ? (wSum / tEnergy) : 500.0f; // Map centroid to hue on a log scale (human pitch perception is logarithmic). // ln(60 Hz) ≈ 4.0943, ln(8000 Hz) ≈ 8.9872 → hue range 0..200 (red → blue-purple).