From fa4b5fee4e36f9842a024275063daf19f03b072e Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 20 Jun 2026 10:59:02 +0100 Subject: [PATCH 01/23] Sync final copy of local AR from MM --- audio_reactive.h | 126 ++++++++++++++++++++++++++++++++++------------- audio_source.h | 17 ++++--- 2 files changed, 101 insertions(+), 42 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 040c3ca6..87336077 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -143,8 +143,9 @@ static bool audioSyncSequence = true; // if true, the receiv static uint8_t audioSyncPurge = 1; // 0: process each packet (don't purge); 1: auto-purge old packets; 2: only process latest received packet (always purge) static bool audioSyncBroadcast = false; // if true, use subnet broadcast for sending instead of multicast; receivers on multicast sockets will still receive broadcast packets static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync +static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start) -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 @@ -257,8 +258,8 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // static AudioSource *audioSource = nullptr; static uint8_t useInputFilter = 0; // enables low-cut filtering. Applies before FFT. -//WLEDMM add experimental settings -static uint8_t micLevelMethod = 0; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) +//WLEDMM experimental settings +static uint8_t micLevelMethod = 1; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) static constexpr uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs. #else @@ -459,13 +460,13 @@ static bool alocateFFTBuffers(void) { USER_PRINT(F("\nFree heap ")); USER_PRINTLN(ESP.getFreeHeap()); #endif - if (vReal) free(vReal); // should not happen - if (vImag) free(vImag); // should not happen - if ((vReal = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die - if ((vImag = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; + if (vReal) d_free(vReal); vReal = nullptr; // should not happen + if (vImag) d_free(vImag); vImag = nullptr; // should not happen + if ((vReal = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die + if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; #ifdef FFT_MAJORPEAK_HUMAN_EAR - if (pinkFactors) free(pinkFactors); - if ((pinkFactors = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; + if (pinkFactors) p_free(pinkFactors); + if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) return false; #endif #ifdef SR_DEBUG @@ -477,6 +478,27 @@ static bool alocateFFTBuffers(void) { return(true); // success } +// de-allocate FFT sample buffers from heap +static void destroyFFTBuffers(bool panicOOM) { + #ifdef FFT_MAJORPEAK_HUMAN_EAR + if (pinkFactors) p_free(pinkFactors); pinkFactors = nullptr; +#endif + if (vImag) d_free(vImag); vImag = nullptr; + if (vReal) d_free(vReal); vReal = nullptr; + + if (panicOOM && !isOOM) { // notify user + isOOM = true; + errorFlag = ERR_LOW_MEM; + USER_PRINTLN("AR startup failed - out of memory!"); + } + #ifdef SR_DEBUG + USER_PRINTLN("\ndestroyFFTBuffers() completed successfully."); + USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); + USER_FLUSH(); + #endif +} + + // High-Pass "DC blocker" filter // see https://www.dsprelated.com/freebooks/filters/DC_Blocker.html static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) { @@ -493,6 +515,22 @@ static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) { } } +// +// FFT runner - "return" from a task function causes crash. This wrapper keeps the task alive +// +void runFFTcode(void * parameter) __attribute__((noreturn,used)); +void runFFTcode(void * parameter) { + bool firstFail = true; // prevents flood of warnings + do { + if (!disableSoundProcessing) { + FFTcode(parameter); + if (firstFail) {USER_PRINTLN(F("warning: unexpected exit of FFT main task."));} + firstFail = false; + } else firstFail = true; // re-enable warning message + vTaskDelay(1000); // if we arrive here, FFcode has returned due to OOM. Wait a bit, then try again. + } while (true); +} + // // FFT main task // @@ -516,13 +554,18 @@ void FFTcode(void * parameter) static float* oldSamples = nullptr; // previous 50% of samples static bool haveOldSamples = false; // for sliding window FFT bool usingOldSamples = false; - if (!oldSamples) oldSamples = (float*) calloc(samplesFFT_2, sizeof(float)); // allocate on first run - if (!oldSamples) { disableSoundProcessing = true; return; } // no memory -> die + if (!oldSamples) oldSamples = (float*) d_calloc(samplesFFT_2, sizeof(float)); // allocate on first run + if (!oldSamples) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(true); return; } // no memory -> die #endif bool success = true; if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run - if (success == false) { disableSoundProcessing = true; return; } // no memory -> die + if (success == false) { + // no memory -> clean up heap, then suspend + disableSoundProcessing = true; + destroyFFTBuffers(true); + return; + } // create FFT object - we have to do if after allocating buffers #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 @@ -532,7 +575,8 @@ void FFTcode(void * parameter) // recommended version optimized by @softhack007 (API version 1.9) #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) static float* windowWeighingFactors = nullptr; - if (!windowWeighingFactors) windowWeighingFactors = (float*) calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap + if (!windowWeighingFactors) windowWeighingFactors = (float*) d_calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap + if (!windowWeighingFactors) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(true); return; } // alloc failed #else static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM #endif @@ -1874,7 +1918,11 @@ class AudioReactive : public Usermod { } catch (...) { packetSize = 0; #ifdef ARDUINO_ARCH_ESP32 + #if ESP_IDF_VERSION_MAJOR < 5 fftUdp.flush(); + #else + fftUdp.clear(); + #endif #endif DEBUG_PRINTLN(F("receiveAudioData: parsePacket out of memory exception caught!")); USER_FLUSH(); @@ -1886,7 +1934,11 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) { + #if ESP_IDF_VERSION_MAJOR < 5 fftUdp.flush(); + #else + fftUdp.clear(); + #endif continue; // Skip invalid packets -> next iteration } #endif @@ -1915,7 +1967,7 @@ class AudioReactive : public Usermod { } while ((packetSize > 0) && ((packetsReceived < maxSamples) || (maxSamples == AR_UDP_FLUSH_ALL))); // repeat until we have read enough packets, or no more packets available #if defined(WLED_DEBUG) || defined(SR_DEBUG) - if ((packetsReceived > 1) && haveFreshData) {DEBUGSR_PRINTF("AR UDP: dropped %d packets [%ums]\t%d maxDrop.\n", packetsReceived-1, millis() - last_UDPTime, maxSamples-1);} // for debugging + if ((packetsReceived > 1) && haveFreshData) {DEBUGSR_PRINTF("AR UDP: dropped %d packets [%lums]\t%d maxDrop.\n", packetsReceived-1, millis() - last_UDPTime, maxSamples-1);} // for debugging #endif return haveFreshData; } @@ -1935,6 +1987,7 @@ class AudioReactive : public Usermod { void setup() override { disableSoundProcessing = true; // just to be sure + isOOM = false; if (!initDone) { // usermod exchangeable data // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers @@ -2034,7 +2087,7 @@ class AudioReactive : public Usermod { break; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) case 5: - DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); useInputFilter = 1; // PDM bandpass filter - this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) delay(100); @@ -2248,14 +2301,9 @@ class AudioReactive : public Usermod { useNetworkAudio = true; // don't fall back to local audio in standard "receive mode" } - // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) - if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed - &&( (realtimeMode == REALTIME_MODE_GENERIC) - ||(realtimeMode == REALTIME_MODE_E131) - ||(realtimeMode == REALTIME_MODE_UDP) - ||(realtimeMode == REALTIME_MODE_ADALIGHT) - ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed - { + // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET, DDP, DMX) + // exception: sound input is still needed when useMainSegmentOnly - other segments are still running with local input. + if ((realtimeMode != REALTIME_MODE_INACTIVE) && (realtimeOverride == REALTIME_OVERRIDE_NONE) && !useMainSegmentOnly) { #ifdef WLED_DEBUG if ((disableSoundProcessing == false) && (audioSyncEnabled < AUDIOSYNC_REC)) { // we just switched to "disabled" DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); @@ -2384,7 +2432,11 @@ class AudioReactive : public Usermod { lastTime = millis(); } else { #ifdef ARDUINO_ARCH_ESP32 + #if ESP_IDF_VERSION_MAJOR < 5 fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. + #else + fftUdp.clear(); + #endif #endif } if (useNetworkAudio) { @@ -2511,11 +2563,12 @@ class AudioReactive : public Usermod { vTaskResume(FFT_Task); connected(); // resume UDP } else { + isOOM = false; if (audioSource) // WLEDMM only create FFT task if we have a valid audio source // xTaskCreatePinnedToCore( // xTaskCreate( // no need to "pin" this task to core #0 xTaskCreateUniversal( - FFTcode, // Function to implement the task + runFFTcode, // Function to implement the task "FFT", // Name of the task 3592, // Stack size in words // 3592 leaves 800-1024 bytes of task stack free NULL, // Task input parameter @@ -2664,15 +2717,17 @@ class AudioReactive : public Usermod { #else // ESP32 only } else { // Analog or I2S digital input - if (audioSource && (audioSource->isInitialized())) { + if (audioSource && (audioSource->isInitialized()) && !isOOM) { // audio source successfully configured if (audioSource->getType() == AudioSource::Type_I2SAdc) { infoArr.add(F("ADC analog")); } else { - if (dmType != 51) - infoArr.add(F("I2S digital")); + if (dmType != 51) { + if (dmType == 5) infoArr.add(F("PDM digital")); + else infoArr.add(F("I2S digital")); + } else - infoArr.add(F("legacy I2S PDM")); + infoArr.add(F("legacy PDM")); } // input level or "silence" if (maxSample5sec > 1.0) { @@ -2685,7 +2740,8 @@ class AudioReactive : public Usermod { } else { // error during audio source setup infoArr.add(F("not initialized")); - if (dmType < 254) infoArr.add(F(" - check pin settings")); + if (isOOM) infoArr.add(F(" - out of memory")); + else if (dmType < 254) infoArr.add(F(" - check pin settings")); } } @@ -3048,14 +3104,14 @@ class AudioReactive : public Usermod { #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #if SR_DMTYPE==5 - oappend(SET_F("addOption(dd,'Generic I2S PDM (⎌)',5);")); + oappend(SET_F("addOption(dd,'Generic PDM (⎌)',5);")); #else - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + oappend(SET_F("addOption(dd,'Generic PDM',5);")); #endif #if SR_DMTYPE==51 - oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾ (⎌)',51);")); + oappend(SET_F("addOption(dd,'.Legacy PDM ☾ (⎌)',51);")); #else - oappend(SET_F("addOption(dd,'.Legacy I2S PDM ☾',51);")); + oappend(SET_F("addOption(dd,'.Legacy PDM ☾',51);")); #endif #endif #if SR_DMTYPE==6 @@ -3094,8 +3150,8 @@ class AudioReactive : public Usermod { //WLEDMM: experimental settings oappend(SET_F("xx='experiments';")); // shortcut oappend(SET_F("dd=addDropdown(ux,xx+':micLev');")); - oappend(SET_F("addOption(dd,'Floating (⎌)',0);")); - oappend(SET_F("addOption(dd,'Freeze',1);")); + oappend(SET_F("addOption(dd,'Floating',0);")); + oappend(SET_F("addOption(dd,'Freeze (⎌)',1);")); oappend(SET_F("addOption(dd,'Fast Freeze',2);")); oappend(SET_F("addInfo(ux+':'+xx+':micLev',1,'☾');")); diff --git a/audio_source.h b/audio_source.h index bac500e8..7ecafc00 100644 --- a/audio_source.h +++ b/audio_source.h @@ -73,12 +73,15 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c // data type requested from the I2S driver - currently we always use 32bit //#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible -#if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) - // this is bitter, but necessary to survive - #define I2S_USE_16BIT_SAMPLES -#endif +//#if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) +// // this is bitter, but necessary to survive // disabled - does not work!! +// #define I2S_USE_16BIT_SAMPLES +//#endif #ifdef I2S_USE_16BIT_SAMPLES +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)) // see https://github.com/MoonModules/WLED-MM/issues/333#issuecomment-3910779668 +#error "16bit sampling currently does not work with newer framework versions. Please build without I2S_USE_16BIT_SAMPLES" +#endif #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT #define I2S_datatype int16_t #define I2S_unsigned_datatype uint16_t @@ -332,11 +335,11 @@ class I2SSource : public AudioSource { // S3: not supported; S2: supported; C3: not supported _config.use_apll = false; // APLL not supported on this MCU #endif - #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(CONFIG_IDF_TARGET_ESP32) if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 #endif - #if defined(WLED_ENABLE_HUB75MATRIX) - _config.use_apll = false; // APLL needed for HUB75 DMA driver ? + #if defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL is needed for Ethernet, and possibly for HUB75, too #endif #endif From 42dde71370f90806fa3c5ec5870f9f92ae75ffc6 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 20 Jun 2026 13:02:27 +0100 Subject: [PATCH 02/23] Add CI workflow to build WLED-MM against this repo Checks out WLED-MM and overrides AR_lib_deps via platformio_override.ini to point at the commit under test, then builds the esp32_4MB_V4_S environment. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build-wled-mm.yml | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/build-wled-mm.yml diff --git a/.github/workflows/build-wled-mm.yml b/.github/workflows/build-wled-mm.yml new file mode 100644 index 00000000..97d252d1 --- /dev/null +++ b/.github/workflows/build-wled-mm.yml @@ -0,0 +1,40 @@ +name: Build WLED-MM + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout this repo + uses: actions/checkout@v4 + + - name: Checkout WLED-MM + uses: actions/checkout@v4 + with: + repository: MoonModules/WLED-MM + path: WLED-MM + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: pip install platformio + + - name: Override AR_lib_deps to use this commit + run: | + cat > WLED-MM/platformio_override.ini << EOF + [common_mm] + AR_lib_deps = + https://github.com/${{ github.repository }}.git#${{ github.sha }} + https://github.com/softhack007/arduinoFFT.git#56c867c + EOF + + - name: Build + run: | + cd WLED-MM + pio run -e esp32_4MB_V4_S From 9467e568bd8fe01ec25a45487dfa359126692691 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 20 Jun 2026 18:37:10 +0100 Subject: [PATCH 03/23] remove WLED_ENABLE_HUB75MATRIX hack --- audio_reactive.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 040c3ca6..1c4f8f5a 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -1120,11 +1120,6 @@ class AudioReactive : public Usermod { private: #ifdef ARDUINO_ARCH_ESP32 -// HUB75 workaround - audio receive only -#ifdef WLED_ENABLE_HUB75MATRIX -#undef SR_DMTYPE -#define SR_DMTYPE 254 // "network receive only" -#endif #ifndef AUDIOPIN int8_t audioPin = -1; #else From f66a9c5d112f1c230ec46ad47a8d0694a161efc7 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 12 Sep 2026 11:43:41 +0100 Subject: [PATCH 04/23] 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 9eb9b67423b4744d69d47fc08719edb623efe3fd Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 12 Sep 2026 12:04:18 +0100 Subject: [PATCH 05/23] fix: build fork PRs by referencing head repo/sha instead of upstream --- .github/workflows/build-wled-mm.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-wled-mm.yml b/.github/workflows/build-wled-mm.yml index 97d252d1..16196e2f 100644 --- a/.github/workflows/build-wled-mm.yml +++ b/.github/workflows/build-wled-mm.yml @@ -26,11 +26,14 @@ jobs: run: pip install platformio - name: Override AR_lib_deps to use this commit + env: + AR_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + AR_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: | cat > WLED-MM/platformio_override.ini << EOF [common_mm] AR_lib_deps = - https://github.com/${{ github.repository }}.git#${{ github.sha }} + https://github.com/${AR_REPO}.git#${AR_SHA} https://github.com/softhack007/arduinoFFT.git#56c867c EOF From 05657ce6bc159c1fb442c80cb09f859179a31baa Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 13 Sep 2026 15:11:33 +0100 Subject: [PATCH 06/23] Add CI workflow to build mainline WLED against this repo (out-of-tree usermod) --- .github/workflows/build-wled.yml | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/build-wled.yml diff --git a/.github/workflows/build-wled.yml b/.github/workflows/build-wled.yml new file mode 100644 index 00000000..fe83a685 --- /dev/null +++ b/.github/workflows/build-wled.yml @@ -0,0 +1,42 @@ +name: Build WLED + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout this repo + uses: actions/checkout@v4 + + - name: Checkout WLED + uses: actions/checkout@v4 + with: + repository: wled/WLED + path: WLED + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: pip install platformio + + - name: Override custom_usermods to use this commit (out-of-tree) + env: + AR_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + AR_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + cat > WLED/platformio_override.ini << EOF + [env:esp32dev] + custom_usermods = + audioreactive = https://github.com/${AR_REPO}.git#${AR_SHA} + EOF + + - name: Build + run: | + cd WLED + pio run -e esp32dev From d40b252dc9970eb673fd34be2d86655c14fab2bc Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 13 Sep 2026 15:13:57 +0100 Subject: [PATCH 07/23] Merge WLED and WLED-MM CI builds into a single workflow with parallel jobs --- .github/workflows/build-wled.yml | 42 ------------------- .../{build-wled-mm.yml => build.yml} | 42 ++++++++++++++++++- 2 files changed, 40 insertions(+), 44 deletions(-) delete mode 100644 .github/workflows/build-wled.yml rename .github/workflows/{build-wled-mm.yml => build.yml} (50%) diff --git a/.github/workflows/build-wled.yml b/.github/workflows/build-wled.yml deleted file mode 100644 index fe83a685..00000000 --- a/.github/workflows/build-wled.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build WLED - -on: - push: - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout this repo - uses: actions/checkout@v4 - - - name: Checkout WLED - uses: actions/checkout@v4 - with: - repository: wled/WLED - path: WLED - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install PlatformIO - run: pip install platformio - - - name: Override custom_usermods to use this commit (out-of-tree) - env: - AR_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - AR_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - cat > WLED/platformio_override.ini << EOF - [env:esp32dev] - custom_usermods = - audioreactive = https://github.com/${AR_REPO}.git#${AR_SHA} - EOF - - - name: Build - run: | - cd WLED - pio run -e esp32dev diff --git a/.github/workflows/build-wled-mm.yml b/.github/workflows/build.yml similarity index 50% rename from .github/workflows/build-wled-mm.yml rename to .github/workflows/build.yml index 16196e2f..724bc2b4 100644 --- a/.github/workflows/build-wled-mm.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,49 @@ -name: Build WLED-MM +name: Build on: push: pull_request: jobs: - build: + build-wled: + name: Build WLED + runs-on: ubuntu-latest + steps: + - name: Checkout this repo + uses: actions/checkout@v4 + + - name: Checkout WLED + uses: actions/checkout@v4 + with: + repository: wled/WLED + path: WLED + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install PlatformIO + run: pip install platformio + + - name: Override custom_usermods to use this commit (out-of-tree) + env: + AR_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + AR_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + cat > WLED/platformio_override.ini << EOF + [env:esp32dev] + custom_usermods = + audioreactive = https://github.com/${AR_REPO}.git#${AR_SHA} + EOF + + - name: Build + run: | + cd WLED + pio run -e esp32dev + + build-wled-mm: + name: Build WLED-MM runs-on: ubuntu-latest steps: - name: Checkout this repo From d2b0ee9ffd7cebce7e778b063f020a97952635f5 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 13 Sep 2026 15:17:55 +0100 Subject: [PATCH 08/23] Pin mainline WLED checkout to 16_x branch --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 724bc2b4..e60d367e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ jobs: uses: actions/checkout@v4 with: repository: wled/WLED + ref: 16_x path: WLED - name: Set up Python From 7308746c9daed656ab8546177a4ee3093f4914f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:21:40 +0200 Subject: [PATCH 09/23] Refactor preprocessor directives for ESP32 targets * fixes a stupid bracket error that wrongly disabled sliding-window FFT on the -S3 * add -P4 to high performance targets * add a warning if WLEDMM_FASTPATH is not enabled on boards with FPU --- audio_reactive.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index b02e12c2..b86a3e6e 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -33,8 +33,11 @@ * .... */ +#if !defined(WLEDMM_FASTPATH) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4)) +#warning "please define WLEDMM_FASTPATH for full performance on your board" +#endif -#if defined(WLEDMM_FASTPATH) && defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) +#if defined(WLEDMM_FASTPATH) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4)) #define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap #endif @@ -53,14 +56,14 @@ #endif #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) // this applies "pink noise scaling" to FFT results before computing the major peak for effects. // currently only for ESP32-S3 and classic ESP32, due to increased runtime #define FFT_MAJORPEAK_HUMAN_EAR #endif // high-resolution type for input filters -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) #define SR_HIRES_TYPE double // ESP32 and ESP32-S3 (with FPU) are fast enough to use "double" #else #define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3) From 4842d6babf5673ac587b2d4301f8223bd9a48ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:36:15 +0000 Subject: [PATCH 10/23] Add WLEDMM compatibility defines in audio_reactive.h Added preprocessor directives for enforcing WLEDMM compatibility --- audio_reactive.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/audio_reactive.h b/audio_reactive.h index b86a3e6e..fef6b177 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -33,6 +33,16 @@ * .... */ +// make sure that defines are consistent for WLEDMM +#if defined(WLEDMM) || defined(_MoonModules_WLED_) + #ifndef _MoonModules_WLED_ + #define _MoonModules_WLED_ + #endif + #ifndef WLED_USE_PINMANAGER_V14 + #define WLED_USE_PINMANAGER_V14 + #endif +#endif + #if !defined(WLEDMM_FASTPATH) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4)) #warning "please define WLEDMM_FASTPATH for full performance on your board" #endif From 0e600ff9aa3830a05f10e3928a3d262e5ce8215d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:45:08 +0000 Subject: [PATCH 11/23] Change WLEDMM to _MoonModules_WLED_ in audio_source.h Use the standard flag that identifies WLED-MM --- audio_source.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/audio_source.h b/audio_source.h index 7ecafc00..2d9272a7 100644 --- a/audio_source.h +++ b/audio_source.h @@ -544,7 +544,7 @@ class ES7243 : public I2SSource { ERRORSR_PRINTF("\nAR: invalid ES7243 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } -#ifdef WLEDMM +#ifdef _MoonModules_WLED_ if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; @@ -676,7 +676,7 @@ class ES8388Source : public I2SSource { ERRORSR_PRINTF("\nAR: invalid ES8388 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } -#ifdef WLEDMM +#ifdef _MoonModules_WLED_ if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; @@ -853,7 +853,7 @@ class ES8388Source : public I2SSource { ERRORSR_PRINTF("\nAR: invalid es8311 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } -#ifdef WLEDMM +#ifdef _MoonModules_WLED_ if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; @@ -958,7 +958,7 @@ class WM8978Source : public I2SSource { ERRORSR_PRINTF("\nAR: invalid WM8978 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } -#ifdef WLEDMM +#ifdef _MoonModules_WLED_ if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; @@ -1062,7 +1062,7 @@ class AC101Source : public I2SSource { ERRORSR_PRINTF("\nAR: invalid AC101 global I2C pins: SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; } -#ifdef WLEDMM +#ifdef _MoonModules_WLED_ if (!ar_joinWire(i2c_sda, i2c_scl)) { // WLEDMM specific: start I2C with globally defined pins ERRORSR_PRINTF("\nAR: failed to join I2C bus with SDA=%d, SCL=%d\n", i2c_sda, i2c_scl); return; From 8dd22c7248fe1edb91c6c39be1657fac112d11ca Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:29:02 +0200 Subject: [PATCH 12/23] limit SETTINGS_STACK_BUF_SIZE sanity check to WLED-MM builds upstream WLED does not use stack space for oappend() --- audio_reactive.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index fef6b177..1ebd753c 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -132,8 +132,8 @@ #define PLOT_FLUSH() #endif -// sanity checks -#ifdef ARDUINO_ARCH_ESP32 +// sanity checks - only needed for WLED-MM +#if defined(ARDUINO_ARCH_ESP32) && defined(_MoonModules_WLED_) // we need more space in for oappend() stack buffer -> SETTINGS_STACK_BUF_SIZE and CONFIG_ASYNC_TCP_STACK_SIZE #if SETTINGS_STACK_BUF_SIZE < 3904 // 3904 is required for WLEDMM-0.14.0-b28 #warning please increase SETTINGS_STACK_BUF_SIZE >= 3904 From 8d6a7b141b4055f90be511dd8d356b0df43a1001 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:39:07 +0200 Subject: [PATCH 13/23] bring back some lost code a few additions from WLED-MM that were lost when transitioning to out-of-tree --- audio_reactive.h | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 1ebd753c..4b892d37 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -304,6 +304,7 @@ 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) @@ -375,7 +376,7 @@ static float filterTime = 0; // avg time for filtering I2S samples // FFT Task variables (filtering and post-processing) static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) -#if !defined(CONFIG_IDF_TARGET_ESP32C3) +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32C6) // audio source parameters and constant constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms //constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms @@ -437,6 +438,11 @@ constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range o // 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) { @@ -461,7 +467,7 @@ static float fftAddAvg(int from, int to) { else return fftAddAvgLin(from, to); // use linear average } -#if defined(CONFIG_IDF_TARGET_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) constexpr bool skipSecondFFT = true; #else constexpr bool skipSecondFFT = false; @@ -1087,7 +1093,7 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool if (currentResult > 1.0) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function else currentResult = 0.0; // special handling, because log(1) = 0; log(0) = undefined currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] + currentResult = my_mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] break; case 2: // Linear scaling @@ -1105,7 +1111,7 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels, bool else currentResult = 0.0; // special handling, because sqrt(0) = undefined currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies //currentResult *= 0.80f + (float(i)/5.6f); //experiment - currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] + currentResult = my_mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] break; case 0: @@ -1177,6 +1183,14 @@ class AudioReactive : public Usermod { private: #ifdef ARDUINO_ARCH_ESP32 +// HUB75 workaround - audio receive only +#if defined(WLED_ENABLE_HUB75MATRIX) && (defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)) + #ifdef SR_DMTYPE + #undef SR_DMTYPE + #warning "HUB75 classic ESP32 builds do not work reliably with I2S microphones. Changing default MIC type to network receive only" + #endif + #define SR_DMTYPE 254 // "network receive only" +#endif #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -2945,9 +2959,9 @@ class AudioReactive : public Usermod { poweruser[F("freqDist")] = freqDist; //poweruser[F("freqRMS")] = averageByRMS; poweruser[F("FFT_Window")] = fftWindow; -#ifdef FFT_USE_SLIDING_WINDOW + #ifdef FFT_USE_SLIDING_WINDOW poweruser[F("I2S_FastPath")] = doSlidingFFT; -#endif + #endif JsonObject freqScale = top.createNestedObject("frequency"); freqScale[F("scale")] = FFTScalingMode; freqScale[F("profile")] = pinkIndex; //WLEDMM From b38f1633c6e36fa83ebb1f8aa196f7639896540d Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:56:01 +0200 Subject: [PATCH 14/23] add build time message building audioreactive v2 for WLED / WLED-MM --- audio_reactive.cpp | 3 +++ audio_reactive.h | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/audio_reactive.cpp b/audio_reactive.cpp index 7b2f6b39..61ce72a9 100644 --- a/audio_reactive.cpp +++ b/audio_reactive.cpp @@ -1,3 +1,6 @@ +#pragma message "building audioreactive V2.0 for WLED" + #include "audio_reactive.h" + static AudioReactive ar_module; REGISTER_USERMOD(ar_module); diff --git a/audio_reactive.h b/audio_reactive.h index 4b892d37..23cc7c06 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -43,11 +43,32 @@ #endif #endif -#if !defined(WLEDMM_FASTPATH) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4)) +#ifdef _MoonModules_WLED_ +#pragma message "building audioreactive V2.0 for WLED-MM" +#endif + +// sanity checks for MCU capability flags +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) + #if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) + #error "configuration error - SOC_CPU_HAS_FPU or SOC_CPU_CORES_NUM does not match your MCU" + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(CONFIG_IDF_TARGET_ESP32P4) + #error "configuration error - SOC_I2S_SUPPORTS_PDM_RX does not match your MCU" + #endif +#endif +#if defined(SOC_I2S_SUPPORTS_PDM_RX) && (defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2)) + #warning "PDM input is not supported yet on your MCU" +#endif +#if defined(SOC_I2S_SUPPORTS_ADC) && (!defined(CONFIG_IDF_TARGET_ESP32) || (ESP_IDF_VERSION_MAJOR > 4)) + #warning "analog ADC input is not supported yet on your MCU" +#endif + + +#if !defined(WLEDMM_FASTPATH) && defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) #warning "please define WLEDMM_FASTPATH for full performance on your board" #endif -#if defined(WLEDMM_FASTPATH) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4)) +#if defined(WLEDMM_FASTPATH) && defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) #define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap #endif From 0ae27e02a5431035fb07ba095f672a7786c66df2 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:58:19 +0200 Subject: [PATCH 15/23] replace MCU checks with capability checks * Check for MCU capabilities instead of directly checking chip names * Some additional sanity checks * Basic support for building with ESP-IDF V5.5.x --- audio_reactive.h | 62 ++++++++++++++++++++++++++---------------------- audio_source.h | 34 ++++++++++++++++---------- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index 23cc7c06..5c872307 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -16,7 +16,9 @@ #ifdef ARDUINO_ARCH_ESP32 #include -#include +#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) + #include +#endif #include #endif @@ -77,7 +79,7 @@ //#define SR_STATS #if !defined(FFTTASK_PRIORITY) -#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) +#if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) // FASTPATH: use higher priority, to avoid that webserver (ws, json, etc) delays sample processing //#define FFTTASK_PRIORITY 3 // competing with async_tcp #define FFTTASK_PRIORITY 4 // above async_tcp @@ -87,14 +89,14 @@ #endif #endif -#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) +#if defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) // this applies "pink noise scaling" to FFT results before computing the major peak for effects. // currently only for ESP32-S3 and classic ESP32, due to increased runtime #define FFT_MAJORPEAK_HUMAN_EAR #endif // high-resolution type for input filters -#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32P4) +#if defined(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) #define SR_HIRES_TYPE double // ESP32 and ESP32-S3 (with FPU) are fast enough to use "double" #else #define SR_HIRES_TYPE float // prefer faster type on slower boards (-S2, -C3) @@ -221,7 +223,7 @@ static bool limiterOn = false; // bool: enable / disable dynamic #else static bool limiterOn = true; #endif -static uint8_t micQuality = 0; // affects input filtering; 0 normal, 1 minimal filtering, 2 no filtering +[[maybe_unused]] static uint8_t micQuality = 0; // input filtering; 0 normal, 1 minimal filtering, 2 no filtering - unused on 8266 #ifdef FFT_USE_SLIDING_WINDOW static uint16_t attackTime = 24; // int: attack time in milliseconds. Default 0.024sec static uint16_t decayTime = 250; // int: decay time in milliseconds. New default 250ms. @@ -294,7 +296,7 @@ static uint8_t useInputFilter = 0; // enables low-cut fil //WLEDMM experimental settings static uint8_t micLevelMethod = 1; // 0=old "floating" miclev, 1=new "freeze" mode, 2=fast freeze mode (mode 2 may not work for you) -#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) +#if !defined(SOC_CPU_HAS_FPU) || (SOC_CPU_CORES_NUM < 2) static constexpr uint8_t averageByRMS = false; // false: use mean value, true: use RMS (root mean squared). use simpler method on slower MCUs. #else static constexpr uint8_t averageByRMS = true; // false: use mean value, true: use RMS (root mean squared). use better method on fast MCUs. @@ -448,7 +450,7 @@ constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range o // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#if !defined(SOC_CPU_HAS_FPU) // these options actually cause slow-down on -S2 (-S2 doesn't have floating point hardware) //#define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc), and a few other speedups - WLEDMM not faster on ESP32 //#define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - WLEDMM slower on ESP32 @@ -719,7 +721,7 @@ void FFTcode(void * parameter) } #endif -#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) +#if defined(WLEDMM_FASTPATH) && defined(ARDUINO_ARCH_ESP32) && (SOC_CPU_CORES_NUM > 1) // experimental - be nice to LED update task (trying to avoid flickering) - dual core only #if FFTTASK_PRIORITY > 1 if (strip.isServicing()) delay(1); @@ -1335,7 +1337,7 @@ class AudioReactive : public Usermod { static const char _name[]; static const char _enabled[]; static const char _inputLvl[]; -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(ARDUINO_ARCH_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && !defined(__riscv) static const char _analogmic[]; #endif static const char _digitalmic[]; @@ -2074,28 +2076,28 @@ class AudioReactive : public Usermod { // Reset I2S peripheral for good measure - not needed in esp-idf v4.4.x and later. #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed - #if !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32) && !defined(__riscv) delay(100); periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 #endif #endif delay(100); // Give that poor microphone some time to setup. - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 51; // dummy user support: SCK == -1 --means--> PDM microphone #endif useInputFilter = 2; // default: DC blocker switch (dmType) { - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips + #if !defined(CONFIG_IDF_TARGET_ESP32) || !defined(SOC_I2S_SUPPORTS_ADC) || (ESP_IDF_VERSION_MAJOR > 4) case 0: //ADC analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) || defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2) case 5: //PDM Microphone case 51: //legacy PDM Microphone #endif - #endif case 1: DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); @@ -2128,7 +2130,7 @@ class AudioReactive : public Usermod { delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 5: DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); @@ -2190,6 +2192,9 @@ class AudioReactive : public Usermod { if (i2c_scl >= 0) sclPin = -1; if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; + case 10: // reserved for ES7210 (without ES83xx) + USER_PRINTLN(F("AR: ES7220 Source (Mic) is not supported yet - trying ES8311.")); + // falls through case 9: DEBUGSR_PRINTLN(F("AR: ES8311 Source (Mic)")); audioSource = new ES8311Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); @@ -2211,7 +2216,7 @@ class AudioReactive : public Usermod { audioSyncEnabled = AUDIOSYNC_REC; // force udp sound receive mode break; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // ADC over I2S is only possible on "classic" ESP32 case 0: default: @@ -2948,7 +2953,7 @@ class AudioReactive : public Usermod { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; #ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); amic["pin"] = audioPin; #endif @@ -2987,6 +2992,7 @@ class AudioReactive : public Usermod { freqScale[F("scale")] = FFTScalingMode; freqScale[F("profile")] = pinkIndex; //WLEDMM #endif + JsonObject dynLim = top.createNestedObject("dynamics"); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; @@ -3032,22 +3038,22 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); #ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); #else audioPin = -1; // MCU does not support analog mic #endif configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + #if !defined(CONFIG_IDF_TARGET_ESP32) if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) || defined(__riscv) || defined(CONFIG_IDF_TARGET_ESP32S2) if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM if (dmType == 51) dmType = SR_DMTYPE; // MCU does not support legacy PDM - #endif - #else + #else if (dmType == 5) useInputFilter = 1; // enable filter for PDM - if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM + if (dmType == 51) useInputFilter = 1; // switch on filter for legacy PDM + #endif #endif configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); @@ -3105,7 +3111,7 @@ class AudioReactive : public Usermod { oappend(SET_F("addInfo(ux+':help',0,'');")); #ifdef ARDUINO_ARCH_ESP32 //WLEDMM: add defaults - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // -S3/-S2/-C3 don't support analog audio + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // -S3/-S2/-C3 don't support analog audio #ifdef AUDIOPIN oappend(SET_F("xOpt(ux+':analogmic:pin',1,' ⎌',")); oappendi(AUDIOPIN); oappend(");"); #endif @@ -3118,7 +3124,7 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addOption(dd,'None - network receive only',254);")); #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) #if SR_DMTYPE==0 oappend(SET_F("addOption(dd,'Generic Analog (⎌)',0);")); #else @@ -3145,7 +3151,7 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) #if SR_DMTYPE==5 oappend(SET_F("addOption(dd,'Generic PDM (⎌)',5);")); #else @@ -3343,7 +3349,7 @@ class AudioReactive : public Usermod { oappend(SET_F("addInfo(uxp,3,'master clock','I2S MCLK');")); oappend(SET_F("dRO(uxp,3);")); // disable read only pins - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + #if defined(CONFIG_IDF_TARGET_ESP32) oappend(SET_F("dOpt(uxp,3,2,2);")); //only use -1, 0, 1 or 3 oappend(SET_F("dOpt(uxp,3,4,39);")); //only use -1, 0, 1 or 3 #endif @@ -3392,7 +3398,7 @@ class AudioReactive : public Usermod { const char AudioReactive::_name[] PROGMEM = "AudioReactive"; const char AudioReactive::_enabled[] PROGMEM = "enabled"; const char AudioReactive::_inputLvl[] PROGMEM = "inputLevel"; -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) const char AudioReactive::_analogmic[] PROGMEM = "analogmic"; #endif const char AudioReactive::_digitalmic[] PROGMEM = "digitalmic"; diff --git a/audio_source.h b/audio_source.h index 2d9272a7..8f485b96 100644 --- a/audio_source.h +++ b/audio_source.h @@ -15,10 +15,12 @@ #include #include "wled.h" #include -#include +#if (ESP_IDF_VERSION_MAJOR < 5) #include // needed for SPH0465 timing workaround (classic ESP32) -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#endif +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) +#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) +#include #include #include #endif @@ -28,6 +30,11 @@ #define SRate_t int #endif +#if (ESP_IDF_VERSION_MAJOR < 4) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,4,7)) // backwards compatibility patch + #define ADC_ATTEN_DB_12 ADC_ATTEN_DB_11 +#endif + + constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not change! I2S_NUM_1 possible but this has // strong limitations -> no MCLK routing, no ADC support, no PDM support @@ -38,14 +45,14 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c // see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents // and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes -#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) +#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) || !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) // there are two things in these MCUs that could lead to problems with audio processing: // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) // * single core, so FFT task might slow down other things like LED updates #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) - #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3. + #error "This audio reactive usermod does not support ESP32-C2 or MCUs without I2S." #else - #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3. + #warning "This audio reactive usermod does not support your MCU." #endif #endif @@ -103,7 +110,7 @@ constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; // I2S port to use (do not c * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8)) // should be fixed in IDF 4.4.5, however arduino-esp32 2.0.14 - 2.0.17 did an "I2S rollback" to 4.4.4 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(5, 0, 0)) // should be fixed in IDF 4.4.5, however arduino-esp32 2.0.14 - 2.0.17 did an "I2S rollback" to 4.4.4 // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) @@ -300,7 +307,7 @@ class I2SSource : public AudioSource { #warning this MCU does not support PDM microphones #endif #endif - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + #if defined(SOC_I2S_SUPPORTS_PDM_RX) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S2) // This is an I2S PDM microphone, these microphones only use a clock and // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c @@ -403,6 +410,7 @@ class I2SSource : public AudioSource { } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + // ToDO: seems that this sometimes reverts PDM timing configuration err = i2s_set_clk(AR_I2S_PORT, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. if (err != ESP_OK) { ERRORSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); @@ -467,7 +475,7 @@ class I2SSource : public AudioSource { protected: void _routeMclk(int8_t mclkPin) { -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(__riscv) && !defined(CONFIG_IDF_TARGET_ESP32S3) // MCLK routing by writing registers is not needed any more with IDF > 4.4.0 #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) // this way of MCLK routing only works on "classic" ESP32 @@ -1085,7 +1093,7 @@ class AC101Source : public I2SSource { #endif #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) // ADC over I2S is only available in "classic" ESP32 /* ADC over I2S Microphone @@ -1155,7 +1163,7 @@ class I2SAdcSource : public I2SSource { } // see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino - adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_11); // configure ADC input amplification + adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12); // configure ADC input amplification #if defined(I2S_GRAB_ADC1_COMPLETELY) // according to docs from espressif, the ADC needs to be started explicitly @@ -1294,12 +1302,12 @@ class SPH0654 : public I2SSource { void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN("SPH0654:: initialize();"); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(CONFIG_IDF_TARGET_ESP32) && (ESP_IDF_VERSION_MAJOR < 5) // these registers are only existing in "classic" ESP32 REG_SET_BIT(I2S_TIMING_REG(AR_I2S_PORT), BIT(9)); REG_SET_BIT(I2S_CONF_REG(AR_I2S_PORT), I2S_RX_MSB_SHIFT); #else - #warning FIX ME! Please. + #pragma message "FIX ME! Maybe." #endif } }; From b5d0f70c6ae9e29bf231c39536f5805ced2ea097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:26:33 +0200 Subject: [PATCH 16/23] Enhance compatibility for ESP-IDF v3 Added backwards compatibility definitions for ESP-IDF v3, based on esp32 capabilities. --- audio_reactive.h | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/audio_reactive.h b/audio_reactive.h index 5c872307..5549f93d 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -10,11 +10,26 @@ */ - #include "wled.h" #ifdef ARDUINO_ARCH_ESP32 +// backwards compatibility hacks for esp-idf v3 +#if (ESP_IDF_VERSION_MAJOR < 4) && defined(CONFIG_IDF_TARGET_ESP32) && !defined(SOC_CPU_HAS_FPU) + #if !defined(SOC_CPU_HAS_FPU) + #define SOC_CPU_HAS_FPU 1 + #endif + #if !defined(SOC_CPU_CORES_NUM) + #define SOC_CPU_CORES_NUM 2 + #endif + #if !defined(SOC_I2S_SUPPORTS_ADC) + #define SOC_I2S_SUPPORTS_ADC 1 + #endif + #if !defined(SOC_I2S_SUPPORTS_PDM_RX) + #define SOC_I2S_SUPPORTS_PDM_RX 1 + #endif +#endif + #include #if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) #include From 4840b53169b52aa1b3f342b80585cd1de75627a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:34:18 +0200 Subject: [PATCH 17/23] Define SOC_I2S_NUM if not already defined another hack for backwards campatibility --- audio_reactive.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/audio_reactive.h b/audio_reactive.h index 5549f93d..d2746927 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -28,6 +28,9 @@ #if !defined(SOC_I2S_SUPPORTS_PDM_RX) #define SOC_I2S_SUPPORTS_PDM_RX 1 #endif + #if !defined(SOC_I2S_NUM) + #define SOC_I2S_NUM 2 + #endif #endif #include From b2e414856a313af0147199055c17e80f9375f1fc Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 21 Oct 2026 18:30:29 +0100 Subject: [PATCH 18/23] Add silence-based stop for audio sync transmission (#11) * Add silence-based stop for audio sync transmission * hide "silence stop" on 8266 * bugfix: reset silence flag when not in send mode * reset on task suspend/resume * reset when not in send mode * "receive-or-local" mode: only reactivate when data was received --------- Co-authored-by: Frank <91616163+softhack007@users.noreply.github.com> --- audio_reactive.h | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/audio_reactive.h b/audio_reactive.h index d2746927..e6ae4e52 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -195,8 +195,9 @@ static volatile bool disableSoundProcessing = false; // if true, sound proc static uint8_t audioSyncEnabled = AUDIOSYNC_NONE; // bit field: bit 0 - send, bit 1 - receive, bit 2 - use local if not receiving static bool audioSyncSequence = true; // if true, the receiver will drop out-of-sequence packets static uint8_t audioSyncPurge = 1; // 0: process each packet (don't purge); 1: auto-purge old packets; 2: only process latest received packet (always purge) +static bool audioSyncSilenceStop = true; // if true, the sender will stop transmitting UDP packets after 30 seconds of continuous silence static bool audioSyncBroadcast = false; // if true, use subnet broadcast for sending instead of multicast; receivers on multicast sockets will still receive broadcast packets -static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync +static bool udpSyncConnected = false; // UDP connection status -> true if connected to UDP sync static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start) #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! @@ -1347,6 +1348,7 @@ class AudioReactive : public Usermod { // used to feed "Info" Page unsigned long last_UDPTime = 0; // time of last valid UDP sound sync data packet int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) + unsigned long syncSilenceStart = 0; // millis() when continuous silence began for the sender; 0 = sound is (or was recently) present float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset #define CYCLE_SAMPLEMAX 3500 // time window for measuring @@ -2359,7 +2361,7 @@ class AudioReactive : public Usermod { // sound sync "receive or local" bool useNetworkAudio = false; if (audioSyncEnabled > AUDIOSYNC_SEND) { // we are in "receive" or "receive+local" mode - if (udpSyncConnected && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) + if (udpSyncConnected && (receivedFormat > 0) && ((millis() - last_UDPTime) <= AUDIOSYNC_IDLE_MS)) // receivedFormat == 0 means we haven't received valid data yet useNetworkAudio = true; else useNetworkAudio = false; @@ -2563,6 +2565,12 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode + // Track sender silence: reset timer when sound is present, start it when silence begins + if (audioSyncEnabled & AUDIOSYNC_SEND) { + if (volumeSmth >= 1.0f) syncSilenceStart = 0; // sound detected - clear silence timer + else if (syncSilenceStart == 0) syncSilenceStart = millis(); // just went silent - start timer + } else syncSilenceStart = 0; // not in send mode - no silence + #if defined(WLEDMM_FASTPATH) if ((audioSyncEnabled & AUDIOSYNC_SEND) && (haveNewFFTResult || (millis() - lastTime > 24))) { // fastpath: send data once results are ready, or each 25ms as fallback (max sampling time is 23ms) #else @@ -2570,7 +2578,10 @@ class AudioReactive : public Usermod { #endif haveNewFFTResult = false; // reset notification // Only run the transmit code IF we're in Transmit mode - transmitAudioData(); + // Sender silence gate: skip transmission when silence has persisted for more than 30 seconds + if (!audioSyncSilenceStop || (syncSilenceStart == 0) || (millis() - syncSilenceStart < 30000UL)) { + transmitAudioData(); + } lastTime = millis(); } #endif @@ -2613,6 +2624,7 @@ class AudioReactive : public Usermod { for(int i=(init?0:1); iwhen receiving');")); + +#ifdef ARDUINO_ARCH_ESP32 + // silence_stop: Sender stops transmitting UDP packets after 30s of continuous silence + oappend(SET_F("dd=addDropdown(ux,'sync:silence_stop');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); +#endif + // broadcast: send to subnet broadcast address instead of multicast oappend(SET_F("addInfo(ux+':sync:broadcast',1,'use broadcast instead of multicast ☾
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' From 6b963449cbf3d31cba4f2ca2c12ec33ab60da84a Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 21 Oct 2026 19:36:25 +0100 Subject: [PATCH 19/23] Add audioreactive color palettes (Ratio, Hue, Spectrum) (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. * disable audiopalettes for WLED-MM builds * add simple interlock for palettes - prevents that palettes are removed while fillAudioPalettes() runs - keep dummy getCRGBForBand() function body in WLED-MM --------- Co-authored-by: Frank Möhle <91616163+softhack007@users.noreply.github.com> --- audio_reactive.h | 141 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/audio_reactive.h b/audio_reactive.h index e6ae4e52..4ac4d2ac 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -201,6 +201,7 @@ static bool udpSyncConnected = false; // UDP connection stat static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start) #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 @@ -1313,6 +1314,9 @@ class AudioReactive : public Usermod { bool enabled = false; #endif bool initDone = false; + bool addPalettes = false; + int8_t palettes = 0; + bool processingPalettes = false; // poor man's interlock - to prevent that palettes get removed while they are processes // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) @@ -1363,8 +1367,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 // @@ -2271,6 +2283,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.")); @@ -2585,6 +2598,8 @@ class AudioReactive : public Usermod { lastTime = millis(); } #endif + + fillAudioPalettes(); } #if defined(_MoonModules_WLED_) && defined(WLEDMM_FASTPATH) @@ -2934,6 +2949,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()) { @@ -2943,6 +2963,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. @@ -2982,6 +3009,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_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); @@ -3056,6 +3084,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 @@ -3068,6 +3097,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_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); @@ -3122,6 +3152,14 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["sync"][F("silence_stop")], audioSyncSilenceStop); 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) { @@ -3434,6 +3472,105 @@ class AudioReactive : public Usermod { } }; +void AudioReactive::removeAudioPalettes(void) { +#ifndef _MoonModules_WLED_ + // WLEDMM does not support usermod palettes + if (processingPalettes) { + USER_PRINTLN(F("Audio palettes are processed - cannot remove.")); + return; + } + DEBUG_PRINTLN(F("Removing audio palettes.")); + palettes -= (int8_t)removeUsermodPalettes(_name); + if (palettes < 0) palettes = 0; // safeguard +#endif +} + +void AudioReactive::createAudioPalettes(void) { +#ifndef _MoonModules_WLED_ + if (palettes || processingPalettes) return; + DEBUG_PRINTLN(F("Adding audio palettes.")); + static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2}; + for (int i=0; i Date: Wed, 21 Oct 2026 20:40:36 +0200 Subject: [PATCH 20/23] typo --- audio_reactive.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio_reactive.h b/audio_reactive.h index 4ac4d2ac..9116d3d5 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -1316,7 +1316,7 @@ class AudioReactive : public Usermod { bool initDone = false; bool addPalettes = false; int8_t palettes = 0; - bool processingPalettes = false; // poor man's interlock - to prevent that palettes get removed while they are processes + bool processingPalettes = false; // poor man's interlock - to prevent that palettes get removed while they are processed // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) 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 21/23] 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 22/23] 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 23/23] 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).