diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..e60d367e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,82 @@ +name: Build + +on: + push: + pull_request: + +jobs: + 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 + ref: 16_x + 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 + 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 + 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/${AR_REPO}.git#${AR_SHA} + https://github.com/softhack007/arduinoFFT.git#56c867c + EOF + + - name: Build + run: | + cd WLED-MM + pio run -e esp32_4MB_V4_S 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 672cbdf0..6e4e400a 100644 --- a/audio_reactive.h +++ b/audio_reactive.h @@ -10,13 +10,33 @@ */ - #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 + #if !defined(SOC_I2S_NUM) + #define SOC_I2S_NUM 2 + #endif +#endif + #include -#include +#if defined(CONFIG_IDF_TARGET_ESP32) && defined(SOC_I2S_SUPPORTS_ADC) && (ESP_IDF_VERSION_MAJOR < 5) + #include +#endif #include #endif @@ -33,8 +53,42 @@ * .... */ +// 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 + +#ifdef _MoonModules_WLED_ +#pragma message "building audioreactive V2.0 for WLED-MM" +#endif -#if defined(WLEDMM_FASTPATH) && defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32) +// 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(SOC_CPU_HAS_FPU) && (SOC_CPU_CORES_NUM > 1) #define FFT_USE_SLIDING_WINDOW // perform FFT with sliding window = 50% overlap #endif @@ -43,7 +97,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 @@ -53,14 +107,14 @@ #endif #endif -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#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(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) +#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) @@ -119,8 +173,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 @@ -141,11 +195,18 @@ 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 !! -#define MAX_PALETTES 3 // number of audioreactive palettes provided by this usermod +#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 @@ -176,6 +237,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 @@ -187,7 +258,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. @@ -203,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) @@ -258,9 +328,9 @@ 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) -#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) +//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(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. @@ -362,7 +432,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 @@ -413,7 +483,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 @@ -423,7 +493,6 @@ constexpr float binWidth = SAMPLE_RATE / (float)samplesFFT; // frequency range o #include // Helper functions - // compute average of several FFT result bins // linear average static float fftAddAvgLin(int from, int to) { @@ -448,7 +517,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; @@ -460,13 +529,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 @@ -478,6 +547,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) { @@ -494,6 +584,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 // @@ -517,13 +623,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 @@ -533,7 +644,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 @@ -636,7 +748,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); @@ -1031,7 +1143,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 @@ -1049,7 +1161,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: @@ -1122,9 +1234,12 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 // HUB75 workaround - audio receive only -#ifdef WLED_ENABLE_HUB75MATRIX -#undef SR_DMTYPE -#define SR_DMTYPE 254 // "network 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; @@ -1208,6 +1323,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 processed // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) @@ -1243,6 +1359,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 @@ -1251,7 +1368,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[]; @@ -1261,6 +1378,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); @@ -1885,7 +2004,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(); @@ -1897,7 +2020,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 @@ -1926,7 +2053,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; } @@ -1946,6 +2073,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 @@ -1989,28 +2117,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); @@ -2043,9 +2171,9 @@ 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: 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); @@ -2105,6 +2233,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); @@ -2126,7 +2257,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: @@ -2252,7 +2383,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; @@ -2260,14 +2391,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."); @@ -2396,7 +2522,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) { @@ -2457,6 +2587,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 @@ -2464,7 +2600,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 @@ -2506,9 +2645,11 @@ 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); iisInitialized())) { + 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) { @@ -2699,7 +2844,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")); } } @@ -2876,7 +3022,7 @@ class AudioReactive : public Usermod { 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) + #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 @@ -2908,13 +3054,14 @@ 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 #endif + JsonObject dynLim = top.createNestedObject("dynamics"); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; @@ -2925,6 +3072,7 @@ class AudioReactive : public Usermod { sync[F("mode")] = audioSyncEnabled; sync[F("skip_old_data")] = audioSyncPurge; sync[F("check_sequence")] = audioSyncSequence; + sync[F("silence_stop")] = audioSyncSilenceStop; sync[F("broadcast")] = audioSyncBroadcast; } @@ -2962,22 +3110,22 @@ class AudioReactive : public Usermod { 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) + #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); @@ -3012,6 +3160,7 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); configComplete &= getJsonValue(top["sync"][F("skip_old_data")], audioSyncPurge); configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); + configComplete &= getJsonValue(top["sync"][F("silence_stop")], audioSyncSilenceStop); configComplete &= getJsonValue(top["sync"][F("broadcast")], audioSyncBroadcast); #ifdef ARDUINO_ARCH_ESP32 @@ -3043,7 +3192,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 @@ -3056,7 +3205,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 @@ -3083,16 +3232,16 @@ 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 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 @@ -3131,8 +3280,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,'☾');")); @@ -3257,6 +3406,14 @@ class AudioReactive : public Usermod { oappend(SET_F("addOption(dd,'Off',0);")); oappend(SET_F("addOption(dd,'On',1);")); oappend(SET_F("addInfo(ux+':sync:check_sequence',1,'when 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' @@ -3281,7 +3438,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 @@ -3327,15 +3484,23 @@ 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) { - if (palettes) return; +#ifndef _MoonModules_WLED_ + if (palettes || processingPalettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); - static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2}; + static const char *const palNames[MAX_PALETTES] PROGMEM = {_palName0, _palName1, _palName2, _palName3, _palName4}; for (int i=0; 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); // 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]); + } + + // 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 + // 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 + }; + 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 + // 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). + // 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)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 + // 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)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 + // 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)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; @@ -3408,13 +3723,15 @@ void AudioReactive::fillAudioPalettes() { ump.palette.loadDynamicGradientPalette(tcp); } + processingPalettes = false; // unlock usermodPalettes +#endif } // strings to reduce flash memory usage (used more than twice) 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"; @@ -3424,3 +3741,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"; diff --git a/audio_source.h b/audio_source.h index bac500e8..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 @@ -73,12 +80,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 @@ -100,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) @@ -297,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 @@ -332,11 +342,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 @@ -400,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); @@ -464,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 @@ -541,7 +552,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; @@ -673,7 +684,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; @@ -850,7 +861,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; @@ -955,7 +966,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; @@ -1059,7 +1070,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; @@ -1082,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 @@ -1152,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 @@ -1291,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 } };