diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index c7fd194ad2..05576890e0 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -21,13 +21,18 @@ * .... */ -#define FFT_PREFER_EXACT_PEAKS // use different FFT wndowing -> results in "sharper" peaks and less "leaking" into other frequencies +#define FFT_PREFER_EXACT_PEAKS // use different FFT windowing -> results in "sharper" peaks and less "leaking" into other frequencies //#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) +// 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 +#else #define FFTTASK_PRIORITY 1 // standard: looptask prio -//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp -//#define FFTTASK_PRIORITY 4 // above asyc_tcp +//#define FFTTASK_PRIORITY 2 // above looptask, below async_tcp +#endif #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -86,8 +91,27 @@ #define PLOT_FLUSH() #endif +// sanity checks +#ifdef ARDUINO_ARCH_ESP32 + // we need more space in for oappend() stack buffer -> SETTINGS_STACK_BUF_SIZE and CONFIG_ASYNC_TCP_TASK_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 + #endif + #if (CONFIG_ASYNC_TCP_TASK_STACK_SIZE - SETTINGS_STACK_BUF_SIZE) < 4352 // at least 4096+256 words of free task stack is needed by async_tcp alone + #error remaining async_tcp stack will be too low - please increase CONFIG_ASYNC_TCP_TASK_STACK_SIZE + #endif +#endif + +// audiosync constants +#define AUDIOSYNC_NONE 0x00 // UDP sound sync off +#define AUDIOSYNC_SEND 0x01 // UDP sound sync - send mode +#define AUDIOSYNC_REC 0x02 // UDP sound sync - receiver mode +#define AUDIOSYNC_REC_PLUS 0x06 // UDP sound sync - receiver + local mode (uses local input if no receiving udp sound) +#define AUDIOSYNC_IDLE_MS 2500 // timeout for "receiver idle" (milliseconds) + static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +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 bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! @@ -109,9 +133,13 @@ static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects +volatile bool haveNewFFTResult = false; // flag to directly inform UDP sound sender when new FFT results are available (to reduce latency). Flag is reset at next UDP send + +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0}; // Our calculated freq. channel result table to be used by effects +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) // TODO: probably best not used by receive nodes static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 @@ -123,7 +151,7 @@ static uint16_t decayTime = 300; // int: decay time in milliseconds // peak detection #ifdef ARDUINO_ARCH_ESP32 -static void detectSamplePeak(void); // peak detection function (needs scaled FFT reasults in vReal[]) - no used for 8266 receive-only mode +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode #endif 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) @@ -150,7 +178,7 @@ static uint8_t inputLevel = 128; // UI slider value #endif // user settable options for FFTResult scaling -static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized sqare root +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root #ifndef SR_FREQ_PROF static uint8_t pinkIndex = 0; // 0: default; 1: line-in; 2: IMNP441 #else @@ -220,7 +248,7 @@ static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // static TaskHandle_t FFT_Task = nullptr; // Table of multiplication factors so that we can even out the frequency response. -#define MAX_PINK 10 // 0 = standard, 1= line-in (pink moise only), 2..4 = IMNP441, 5..6 = ICS-43434, ,7=SPM1423, 8..9 = userdef, 10= flat (no pink noise adjustment) +#define MAX_PINK 10 // 0 = standard, 1= line-in (pink noise only), 2..4 = IMNP441, 5..6 = ICS-43434, ,7=SPM1423, 8..9 = userdef, 10= flat (no pink noise adjustment) static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = { { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }, // 0 default from SR WLED // { 1.30f, 1.32f, 1.40f, 1.46f, 1.52f, 1.57f, 1.68f, 1.80f, 1.89f, 2.00f, 2.11f, 2.21f, 2.30f, 2.39f, 3.09f, 4.34f }, // - Line-In Generic -> pink noise adjustment only @@ -244,7 +272,7 @@ static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = { /* how to make your own profile: * =============================== * preparation: make sure your microphone has direct line-of-sigh with the speaker, 1-2meter distance is best - * Prepare your HiFi equipment: disable all "Sound enhancements" - like Loudness, Equalizer, Bass Boost. Bass/Trebble controls set to middle. + * Prepare your HiFi equipment: disable all "Sound enhancements" - like Loudness, Equalizer, Bass Boost. Bass/Treble controls set to middle. * Your HiFi equipment should receive its audio input from Line-In, SPDIF, HDMI, or another "undistorted" connection (like CDROM). * Try not to use Bluetooth or MP3 when playing the "pink noise" audio. BT-audio and MP3 both perform "acoustic adjustments" that we don't want now. @@ -258,7 +286,7 @@ static const float fftResultPink[MAX_PINK+1][NUM_GEQ_CHANNELS] = { * Your own profile: * - Target for each LED bar is 50% to 75% of the max height --> 8(high) x 16(wide) panel means target = 5. 32 x 16 means target = 22. - * - From left to right - count the LEDs in each of the 16 frequency colums (that's why you need the photo). This is the barheight for each channel. + * - From left to right - count the LEDs in each of the 16 frequency columns (that's why you need the photo). This is the barheight for each channel. * - math time! Find the multiplier that will bring each bar to to target. * * in case of square root scale: multiplier = (target * target) / (barheight * barheight) * * in case of linear scale: multiplier = target / barheight @@ -280,8 +308,6 @@ static float sampleTime = 0; // avg (blocked) time for reading I2S sample // FFT Task variables (filtering and post-processing) static float lastFftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // backup of last FFT channels (before postprocessing) -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) #if !defined(CONFIG_IDF_TARGET_ESP32C3) // audio source parameters and constant @@ -360,7 +386,7 @@ static float mapf(float x, float in_min, float in_max, float out_min, float out_ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } -// compute average of several FFT resut bins +// compute average of several FFT result bins // linear average static float fftAddAvgLin(int from, int to) { float result = 0.0f; @@ -430,7 +456,7 @@ void FFTcode(void * parameter) for(uint_fast16_t binInd = 0; binInd < samplesFFT; binInd++) { float binFreq = binInd * binWidth + binWidth/2.0f; if (binFreq > (SAMPLE_RATE * 0.42f)) - binFreq = (SAMPLE_RATE * 0.42f) - 0.25 * (binFreq - (SAMPLE_RATE * 0.42f)); // supress noise and aliasing + binFreq = (SAMPLE_RATE * 0.42f) - 0.25 * (binFreq - (SAMPLE_RATE * 0.42f)); // suppress noise and aliasing pinkFactors[binInd] = sqrtf(binFreq) / pinkcenter; } pinkFactors[0] *= 0.5; // suppress 0-42hz bin @@ -442,16 +468,16 @@ void FFTcode(void * parameter) // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + if (disableSoundProcessing || (audioSyncEnabled == AUDIOSYNC_REC)) { isFirstRun = false; vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers continue; } #if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // timing uint64_t start = esp_timer_get_time(); bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid - static uint64_t lastCycleStart = 0; static uint64_t lastLastTime = 0; if ((lastCycleStart > 0) && (lastCycleStart < start)) { // filter out overflows @@ -466,6 +492,14 @@ void FFTcode(void * parameter) if (audioSource) audioSource->getSamples(vReal, samplesFFT); #if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // debug info in case that stack usage changes + static unsigned int minStackFree = UINT32_MAX; + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + if (minStackFree > stackFree) { + minStackFree = stackFree; + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minStackFree); //WLEDMM + } + // timing if (start < esp_timer_get_time()) { // filter out overflows uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10.0; // smooth @@ -474,7 +508,7 @@ void FFTcode(void * parameter) #endif xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay - isFirstRun = !isFirstRun; // toggle throtte + isFirstRun = !isFirstRun; // toggle throttle #ifdef MIC_LOGGER float datMin = 0.0f; @@ -491,6 +525,11 @@ void FFTcode(void * parameter) } #endif +#if defined(WLEDMM_FASTPATH) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && defined(ARDUINO_ARCH_ESP32) + // experimental - be nice to LED update task (trying to avoid flickering) - dual core only + if (strip.isServicing()) delay(2); +#endif + // band pass filter - can reduce noise floor by a factor of 50 // downside: frequencies below 100Hz will be ignored if ((useInputFilter > 0) && (useInputFilter < 99)) { @@ -611,7 +650,7 @@ void FFTcode(void * parameter) * * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. - * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then detetermine the bins. + * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. * End frequency = Start frequency * multiplier ^ 16 * Multiplier = (End frequency/ Start frequency) ^ 1/16 * Multiplier = 1.320367784 @@ -665,7 +704,7 @@ void FFTcode(void * parameter) fftCalc[13] = fftAddAvg(86,103); // 18 3704 - 4479 high mid fftCalc[14] = fftAddAvg(104,164) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping } - else if (freqDist == 1) { //WLEDMM: Rightshft: note ewowi: frequencies in comments are not correct + else if (freqDist == 1) { //WLEDMM: Rightshift: note ewowi: frequencies in comments are not correct if (useInputFilter==1) { // skip frequencies below 100hz fftCalc[ 0] = 0.8f * fftAddAvg(1,1); @@ -709,12 +748,13 @@ void FFTcode(void * parameter) memcpy(fftCalc, lastFftCalc, sizeof(fftCalc)); // restore last "good" channels } - // post-processing of frequency channels (pink noise adjustment, AGC, smooting, scaling) + // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) if (pinkIndex > MAX_PINK) pinkIndex = MAX_PINK; //postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); postProcessFFTResults((fabsf(volumeSmth)>0.25f)? true : false , NUM_GEQ_CHANNELS); // this function modifies fftCalc, fftAvg and fftResult #if defined(WLED_DEBUG) || defined(SR_DEBUG)|| defined(SR_STATS) + // timing static uint64_t lastLastFFT = 0; if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding @@ -727,6 +767,9 @@ void FFTcode(void * parameter) autoResetPeak(); detectSamplePeak(); + // we have new results - notify UDP sound send + haveNewFFTResult = true; + #if !defined(I2S_GRAB_ADC1_COMPLETELY) if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC #endif @@ -766,7 +809,7 @@ static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // p // FIR lowpass, to remove high frequency noise float highFilteredSample; if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes - else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // spcial handling for last sample in array + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array last_vals[1] = last_vals[0]; last_vals[0] = sampleBuffer[i]; sampleBuffer[i] = highFilteredSample; @@ -895,7 +938,7 @@ static void autoResetPeak(void) { uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. samplePeak = false; - if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + if (audioSyncEnabled == AUDIOSYNC_NONE) udpSamplePeak = false; // this is normally reset by transmitAudioData } } @@ -957,7 +1000,7 @@ class AudioReactive : public Usermod { float sampleRaw; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting float sampleSmth; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting uint8_t samplePeak; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved1; // 01 Bytes - for future extensions - not used yet + uint8_t frameCounter; // 01 Bytes - track duplicate/out of order packets uint8_t fftResult[16]; // 16 Bytes float FFT_Magnitude; // 04 Bytes float FFT_MajorPeak; // 04 Bytes @@ -989,7 +1032,11 @@ class AudioReactive : public Usermod { // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) unsigned long lastTime = 0; // last time of running UDP Microphone Sync +#if defined(WLEDMM_FASTPATH) + const uint16_t delayMs = 5; // I don't want to sample too often and overload WLED +#else const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED +#endif uint16_t audioSyncPort= 11988;// default port for UDP sound sync bool updateIsRunning = false; // true during OTA. @@ -1001,7 +1048,7 @@ class AudioReactive : public Usermod { // variables used by getSample() and agcAvg() int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controler. + double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller float expAdjF = 0.0f; // Used for exponential filter. float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. @@ -1039,7 +1086,7 @@ class AudioReactive : public Usermod { //////////////////// void logAudio() { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & AUDIOSYNC_REC) == 0))) return; // no audio available #ifdef MIC_LOGGER // Debugging functions for audio input and sound processing. Comment out the values you want to see PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth + 256.0f); PLOT_PRINT("\t"); // +256 to move above other lines @@ -1132,13 +1179,13 @@ class AudioReactive : public Usermod { * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal * 3. the amplification depends on signal level: * a) normal zone - very slow adjustment - * b) emergency zome (<10% or >90%) - very fast adjustment + * b) emergency zone (<10% or >90%) - very fast adjustment */ void agcAvg(unsigned long the_time) { const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function - float lastMultAgc = multAgc; // last muliplier used + float lastMultAgc = multAgc; // last multiplier used float multAgcTemp = multAgc; // new multiplier float tmpAgc = sampleReal * multAgc; // what-if amplified signal @@ -1178,13 +1225,13 @@ class AudioReactive : public Usermod { if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) - control_integrated += control_error * 0.002 * 0.25; // 2ms = intgration time; 0.25 for damping + control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping else - control_integrated *= 0.9; // spin down that beasty integrator + control_integrated *= 0.9; // spin down that integrator beast // apply PI Control tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain - if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower emergy zone + if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower emergency zone multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } else { // "normal zone" @@ -1192,7 +1239,7 @@ class AudioReactive : public Usermod { multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } - // limit amplification again - PI controler sometimes "overshoots" + // limit amplification again - PI controller sometimes "overshoots" //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; @@ -1222,7 +1269,7 @@ class AudioReactive : public Usermod { void getSample() { float sampleAdj; // Gain adjusted sample value - float tmpSample; // An interim sample variable used for calculatioins. + float tmpSample; // An interim sample variable used for calculations. #ifdef WLEDMM_FASTPATH constexpr float weighting = 0.35f; // slightly reduced filter strength, to reduce audio latency constexpr float weighting2 = 0.25f; @@ -1398,7 +1445,7 @@ class AudioReactive : public Usermod { if (limiterOn == false) return; long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up float deltaSample = volumeSmth - last_volumeSmth; if (attackTime > 0) { // user has defined attack time > 0 @@ -1416,6 +1463,39 @@ class AudioReactive : public Usermod { last_time = millis(); } + // MM experimental: limiter to smooth GEQ samples (only for UDP sound receiver mode) + // target value (if gotNewSample) : fftCalc + // last filtered value: fftAvg + void limitGEQDynamics(bool gotNewSample) { + constexpr float bigChange = 202; // just a representative number - a large, expected sample value + constexpr float smooth = 0.8f; // a bit of filtering + static unsigned long last_time = 0; + + if (limiterOn == false) return; + + if (gotNewSample) { // take new FFT samples as target values + for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] = fftResult[i]; + fftResult[i] = fftAvg[i]; + } + } + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> silly lil hick-up + float maxAttack = (attackTime <= 0) ? 255.0f : (bigChange * float(delta_time) / float(attackTime)); + float maxDecay = (decayTime <= 0) ? -255.0f : (-bigChange * float(delta_time) / float(decayTime)); + + for(unsigned i=0; i < NUM_GEQ_CHANNELS; i++) { + float deltaSample = fftCalc[i] - fftAvg[i]; + if (deltaSample > maxAttack) deltaSample = maxAttack; + if (deltaSample < maxDecay) deltaSample = maxDecay; + deltaSample = deltaSample * smooth; + fftAvg[i] = fmaxf(0.0f, fminf(255.0f, fftAvg[i] + deltaSample)); + fftResult[i] = fftAvg[i]; + } + last_time = millis(); + } + ////////////////////// // UDP Sound Sync // ////////////////////// @@ -1426,7 +1506,7 @@ class AudioReactive : public Usermod { // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection static unsigned long last_connection_attempt = 0; - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if ((audioSyncPort <= 0) || (audioSyncEnabled == AUDIOSYNC_NONE)) return; // Sound Sync not enabled if (!(apActive || WLED_CONNECTED || interfacesInited)) { if (udpSyncConnected) { udpSyncConnected = false; @@ -1434,11 +1514,11 @@ class AudioReactive : public Usermod { receivedFormat = 0; DEBUGSR_PRINTLN(F("AR connectUDPSoundSync(): connection lost, UDP closed.")); } - return; // neither AP nor other connections availeable + return; // neither AP nor other connections available } if (udpSyncConnected) return; // already connected if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; // don't reconect during OTA + if (updateIsRunning) return; // don't reconnect during OTA // if we arrive here, we need a UDP connection but don't have one last_connection_attempt = millis(); @@ -1448,16 +1528,19 @@ class AudioReactive : public Usermod { void transmitAudioData() { if (!udpSyncConnected) return; + static uint8_t frameCounter = 0; //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); audioSyncPacket transmitData; + memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized + strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); // transmit samples that were not modified by limitSampleDynamics() transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - transmitData.reserved1 = 0; + transmitData.frameCounter = frameCounter; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); @@ -1470,7 +1553,8 @@ class AudioReactive : public Usermod { fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); fftUdp.endPacket(); } - return; + + frameCounter++; } // transmitAudioData() #endif static bool isValidUdpSyncVersion(const char *header) { @@ -1480,8 +1564,29 @@ class AudioReactive : public Usermod { return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } - void decodeAudioData(int packetSize, uint8_t *fftBuff) { + bool decodeAudioData(int packetSize, uint8_t *fftBuff) { + if((0 == packetSize) || (nullptr == fftBuff)) return false; // sanity check audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + + // validate sequence, discard out-of-sequence packets + static uint8_t lastFrameCounter = 0; + // add info for UI + if ((receivedPacket->frameCounter > 0) && (lastFrameCounter > 0)) receivedFormat = 3; // v2+ + else receivedFormat = 2; // v2 + // check sequence + bool sequenceOK = false; + if(receivedPacket->frameCounter > lastFrameCounter) sequenceOK = true; // sequence OK + if((lastFrameCounter < 12) && (receivedPacket->frameCounter > 248)) sequenceOK = false; // prevent sequence "roll-back" due to late packets (1->254) + if((lastFrameCounter > 248) && (receivedPacket->frameCounter < 12)) sequenceOK = true; // handle roll-over (255 -> 0) + if(audioSyncSequence == false) sequenceOK = true; // sequence checking disabled by user + if((sequenceOK == false) && (receivedPacket->frameCounter != 0)) { // always accept "0" - its the legacy value + DEBUGSR_PRINTF("Skipping audio frame out of order or duplicated - %u vs %u\n", lastFrameCounter, receivedPacket->frameCounter); + return false; // reject out-of sequence frame + } + else { + lastFrameCounter = receivedPacket->frameCounter; + } + // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); @@ -1506,6 +1611,7 @@ class AudioReactive : public Usermod { my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + return true; } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { @@ -1568,16 +1674,15 @@ class AudioReactive : public Usermod { // VERIFY THAT THIS IS A COMPATIBLE PACKET if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftUdpBuffer))) { - decodeAudioData(packetSize, fftUdpBuffer); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; receivedFormat = 2; + haveFreshData = decodeAudioData(packetSize, fftUdpBuffer); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); } else { if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftUdpBuffer))) { decodeAudioData_v1(packetSize, fftUdpBuffer); + receivedFormat = 1; //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); haveFreshData = true; - receivedFormat = 1; } else receivedFormat = 0; // unknown format } } @@ -1632,7 +1737,7 @@ class AudioReactive : public Usermod { um_data->u_type[10] = UMT_FLOAT; #else // ESP8266 - // See https://github.com/MoonModules/WLED/pull/60#issuecomment-1666972133 for explaination of these alternative sources of data + // See https://github.com/MoonModules/WLED/pull/60#issuecomment-1666972133 for explanation of these alternative sources of data um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[6] = UMT_BYTE; @@ -1678,7 +1783,13 @@ class AudioReactive : public Usermod { //useInputFilter = 0; // in case you need to disable low-cut software filtering audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); delay(100); - if (audioSource) audioSource->initialize(sdaPin, sclPin, i2swsPin, i2ssdPin, i2sckPin, mclkPin); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; case 3: DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); @@ -1710,11 +1821,38 @@ class AudioReactive : public Usermod { break; #endif case 6: - DEBUGSR_PRINTLN(F("AR: ES8388 Source")); + #ifdef use_es8388_mic + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Mic)")); + #else + DEBUGSR_PRINTLN(F("AR: ES8388 Source (Line-In)")); + #endif audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour delay(100); - if (audioSource) audioSource->initialize(sdaPin, sclPin, i2swsPin, i2ssdPin, i2sckPin, mclkPin); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 7: + #ifdef use_wm8978_mic + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Mic)")); + #else + DEBUGSR_PRINTLN(F("AR: WM8978 Source (Line-In)")); + #endif + audioSource = new WM8978Source(SAMPLE_RATE, BLOCK_SIZE, 1.0f); + //useInputFilter = 0; // to disable low-cut software filtering and restore previous behaviour + delay(100); + // WLEDMM align global pins + if ((sdaPin >= 0) && (i2c_sda < 0)) i2c_sda = sdaPin; // copy usermod prefs into globals (if globals not defined) + if ((sclPin >= 0) && (i2c_scl < 0)) i2c_scl = sclPin; + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; + + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) @@ -1733,7 +1871,7 @@ class AudioReactive : public Usermod { if (!audioSource) enabled = false; // audio failed to initialise #endif - if (enabled) onUpdateBegin(false); // create FFT task, and initailize network + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 if (FFT_Task == nullptr) enabled = false; // FFT task creation failed @@ -1758,6 +1896,10 @@ class AudioReactive : public Usermod { DEBUGSR_PRINT(F("AR: init done, enabled = ")); DEBUGSR_PRINTLN(enabled ? F("true.") : F("false.")); USER_FLUSH(); + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1774,7 +1916,7 @@ class AudioReactive : public Usermod { DEBUGSR_PRINTLN(F("AR connected(): old UDP connection closed.")); } - if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { + if ((audioSyncPort > 0) && (audioSyncEnabled > AUDIOSYNC_NONE)) { #ifdef ARDUINO_ARCH_ESP32 udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); #else @@ -1788,6 +1930,10 @@ class AudioReactive : public Usermod { DEBUGSR_PRINTLN(udpSyncConnected ? F("AR connected(): UDP: connected to WIFI.") : F("AR connected(): UDP is disconnected (Wifi).")); } } + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } @@ -1811,7 +1957,18 @@ class AudioReactive : public Usermod { return; } // We cannot wait indefinitely before processing audio data - if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + if (strip.isServicing() && (millis() - lastUMRun < 2)) return; // WLEDMM isServicing() is the critical part (be nice, but not too nice) + + // 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)) + useNetworkAudio = true; + else + useNetworkAudio = false; + if (audioSyncEnabled == AUDIOSYNC_REC) + 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 @@ -1822,31 +1979,45 @@ class AudioReactive : public Usermod { ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { #ifdef WLED_DEBUG - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" + if ((disableSoundProcessing == false) && (audioSyncEnabled < AUDIOSYNC_REC)) { // we just switched to "disabled" DEBUG_PRINTLN("[AR userLoop] realtime mode active - audio processing suspended."); DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } #endif disableSoundProcessing = true; + useNetworkAudio = false; } else { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + if ((disableSoundProcessing == true) && (audioSyncEnabled < AUDIOSYNC_REC) && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN("[AR userLoop] realtime mode ended - audio processing resumed."); DEBUG_PRINTF( " RealtimeMode = %d; RealtimeOverride = %d\n", int(realtimeMode), int(realtimeOverride)); } #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + if ((disableSoundProcessing == true) && (audioSyncEnabled < AUDIOSYNC_REC)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping disableSoundProcessing = false; } - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode + if (audioSyncEnabled == AUDIOSYNC_REC) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode + if (audioSyncEnabled & AUDIOSYNC_SEND) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode #ifdef ARDUINO_ARCH_ESP32 - if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source - + if (!audioSource->isInitialized()) { // no audio source + disableSoundProcessing = true; + if (audioSyncEnabled > AUDIOSYNC_SEND) useNetworkAudio = true; + } + if ((audioSyncEnabled == AUDIOSYNC_REC_PLUS) && useNetworkAudio) disableSoundProcessing = true; // UDP sound receiving - disable local audio + + #ifdef SR_DEBUG + // debug info in case that task stack usage changes + static unsigned int minLoopStackFree = UINT32_MAX; + unsigned int stackFree = uxTaskGetStackHighWaterMark(NULL); + if (minLoopStackFree > stackFree) { + minLoopStackFree = stackFree; + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), minLoopStackFree); //WLEDMM + } + #endif // Only run the sampling code IF we're not in Receive mode or realtime mode - if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { + if ((audioSyncEnabled != AUDIOSYNC_REC) && !disableSoundProcessing && !useNetworkAudio) { if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) unsigned long t_now = millis(); // remember current time @@ -1855,9 +2026,9 @@ class AudioReactive : public Usermod { #if defined(SR_DEBUG) // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS - //if ((userloopDelay > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - //DEBUG_PRINTF("[AR userLoop] hickup detected -> was inactive for last %d millis!\n", userloopDelay); + // softhack007 disabled temporarily - avoid serial console spam with MANY LEDs and low FPS + //if ((userloopDelay > /*23*/ 65) && !disableSoundProcessing && (audioSyncEnabled == AUDIOSYNC_NONE)) { + //DEBUG_PRINTF("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n", userloopDelay); //} #endif @@ -1903,27 +2074,33 @@ class AudioReactive : public Usermod { connectUDPSoundSync(); // ensure we have a connection - if needed // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + if ((audioSyncEnabled & AUDIOSYNC_REC) && udpSyncConnected) { // Only run the audio listener code if we're in Receive mode static float syncVolumeSmth = 0; bool have_new_sample = false; if (millis() - lastTime > delayMs) { have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); + if (have_new_sample) { + last_UDPTime = millis(); + useNetworkAudio = true; // UDP input arrived - use it + } lastTime = millis(); } else { #ifdef ARDUINO_ARCH_ESP32 fftUdp.flush(); // WLEDMM: Flush this if we haven't read it. Does not work on 8266. #endif } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + if (useNetworkAudio) { + if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample + else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter + limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + limitGEQDynamics(have_new_sample); // WLEDMM experimental: smooth FFT (GEQ) samples + } } else { receivedFormat = 0; } - if ( (audioSyncEnabled & 0x02) // receive mode + if ( (audioSyncEnabled & AUDIOSYNC_REC) // receive mode && udpSyncConnected // connected && (receivedFormat > 0) // we actually received something in the past && ((millis() - last_UDPTime) > 25000)) { // close connection after 25sec idle @@ -1969,7 +2146,12 @@ class AudioReactive : public Usermod { #ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + #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 + if ((audioSyncEnabled & AUDIOSYNC_SEND) && (millis() - lastTime > 20)) { // standard: send data each 20ms + #endif + haveNewFFTResult = false; // reset notification // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); @@ -2030,7 +2212,7 @@ class AudioReactive : public Usermod { xTaskCreateUniversal( FFTcode, // Function to implement the task "FFT", // Name of the task - 5000, // Stack size in words + 3592, // Stack size in words // 3592 leaves 800-1024 bytes of task stack free NULL, // Task input parameter FFTTASK_PRIORITY, // Priority of the task &FFT_Task // Task handle @@ -2040,6 +2222,10 @@ class AudioReactive : public Usermod { micDataReal = 0.0f; // just to be sure if (enabled) disableSoundProcessing = false; updateIsRunning = init; + + #if defined(ARDUINO_ARCH_ESP32) && defined(SR_DEBUG) + DEBUGSR_PRINTF("|| %-9s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM + #endif } #else // reduced function for 8266 @@ -2115,7 +2301,15 @@ class AudioReactive : public Usermod { infoArr.add(uiDomString); if (enabled) { + bool audioSyncIDLE = false; // true if sound sync is not receiving + #ifdef ARDUINO_ARCH_ESP32 + // audio sync status + if ((audioSyncEnabled & AUDIOSYNC_REC) && (!udpSyncConnected || (millis() - last_UDPTime > AUDIOSYNC_IDLE_MS))) // connected and nothing received in 2.5sec + audioSyncIDLE = true; + if ((audioSource == nullptr) || (!audioSource->isInitialized())) // local audio not configured + audioSyncIDLE = false; + // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running if (soundAgc > 0) { @@ -2142,11 +2336,11 @@ class AudioReactive : public Usermod { // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG // current Audio input infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { + if ((audioSyncEnabled == AUDIOSYNC_REC) || (!audioSyncIDLE && (audioSyncEnabled == AUDIOSYNC_REC_PLUS))){ // UDP sound sync - receive mode infoArr.add(F("UDP sound sync")); if (udpSyncConnected) { - if (millis() - last_UDPTime < 2500) + if (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS) infoArr.add(F(" - receiving")); else infoArr.add(F(" - idle")); @@ -2161,7 +2355,7 @@ class AudioReactive : public Usermod { } else { // Analog or I2S digital input if (audioSource && (audioSource->isInitialized())) { - // audio source sucessfully configured + // audio source successfully configured if (audioSource->getType() == AudioSource::Type_I2SAdc) { infoArr.add(F("ADC analog")); } else { @@ -2194,13 +2388,13 @@ class AudioReactive : public Usermod { } // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + if ((soundAgc == 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { infoArr = user.createNestedArray(F("Manual Gain")); float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets infoArr.add(roundf(myGain*100.0f) / 100.0f); infoArr.add("x"); } - if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + if ((soundAgc > 0) && (disableSoundProcessing == false) && !(audioSyncEnabled == AUDIOSYNC_REC)) { infoArr = user.createNestedArray(F("AGC Gain")); infoArr.add(roundf(multAgc*100.0f) / 100.0f); infoArr.add("x"); @@ -2209,18 +2403,24 @@ class AudioReactive : public Usermod { // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { + if (audioSyncEnabled & AUDIOSYNC_SEND) { infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { + if ((udpSyncConnected) && (millis() - lastTime < AUDIOSYNC_IDLE_MS)) infoArr.add(F(" v2+")); + } else if (audioSyncEnabled == AUDIOSYNC_REC) { infoArr.add(F("receive mode")); + } else if (audioSyncEnabled == AUDIOSYNC_REC_PLUS) { + infoArr.add(F("receive+local mode")); } } else infoArr.add("off"); if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < AUDIOSYNC_IDLE_MS)) { if (receivedFormat == 1) infoArr.add(F(" v1")); if (receivedFormat == 2) infoArr.add(F(" v2")); + if (receivedFormat == 3) { + if (audioSyncSequence) infoArr.add(F(" v2+")); // Sequence checking enabled + else infoArr.add(F(" v2")); + } } #if defined(WLED_DEBUG) || defined(SR_DEBUG) || defined(SR_STATS) @@ -2336,6 +2536,11 @@ class AudioReactive : public Usermod { JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); dmic[F("type")] = dmType; + // WLEDMM: align with globals I2C pins + if ((dmType == 2) || (dmType == 6)) { // only for ES7243 and ES8388 + if (i2c_sda >= 0) sdaPin = -1; // -1 = use global + if (i2c_scl >= 0) sclPin = -1; // -1 = use global + } JsonArray pinArray = dmic.createNestedArray("pin"); pinArray.add(i2ssdPin); pinArray.add(i2swsPin); @@ -2367,6 +2572,7 @@ class AudioReactive : public Usermod { JsonObject sync = top.createNestedObject("sync"); sync[F("port")] = audioSyncPort; sync[F("mode")] = audioSyncEnabled; + sync[F("check_sequence")] = audioSyncSequence; } @@ -2435,6 +2641,7 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); + configComplete &= getJsonValue(top["sync"][F("check_sequence")], audioSyncSequence); return configComplete; } @@ -2497,7 +2704,11 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addOption(dd,'ES8388 ☾',6);")); #endif - + #if SR_DMTYPE==7 + oappend(SET_F("addOption(dd,'WM8978 ☾ (⎌)',7);")); + #else + oappend(SET_F("addOption(dd,'WM8978 ☾',7);")); + #endif #ifdef SR_SQUELCH oappend(SET_F("addInfo('AudioReactive:config:squelch',1,'⎌ ")); oappendi(SR_SQUELCH); oappend("');"); // 0 is field type, 1 is actual field #endif @@ -2601,12 +2812,20 @@ class AudioReactive : public Usermod { oappend(SET_F("addInfo('AudioReactive:frequency:profile',1,'☾');")); #endif oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Off',0);")); // AUDIOSYNC_NONE #ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Send',1);")); + oappend(SET_F("addOption(dd,'Send',1);")); // AUDIOSYNC_SEND #endif - oappend(SET_F("addOption(dd,'Receive',2);")); - oappend(SET_F("addInfo('AudioReactive:sync:mode',1,'
Sync audio data with other WLEDs');")); + oappend(SET_F("addOption(dd,'Receive',2);")); // AUDIOSYNC_REC +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addOption(dd,'Receive or Local',6);")); // AUDIOSYNC_REC_PLUS +#endif + // check_sequence: Receiver skips out-of-sequence packets when enabled + oappend(SET_F("dd=addDropdown('AudioReactive','sync:check_sequence');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + + oappend(SET_F("addInfo('AudioReactive:sync:check_sequence',1,'when receiving
Sync audio data with other WLEDs');")); // must append this to the last field of 'sync' oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field #ifdef ARDUINO_ARCH_ESP32 @@ -2649,7 +2868,7 @@ class AudioReactive : public Usermod { oappend(SET_F("xOpt('AudioReactive:digitalmic:pin[]',5,' ⎌',")); oappendi(ES7243_SCLPIN); oappend(");"); #endif oappend(SET_F("dRO('AudioReactive:digitalmic:pin[]',5);")); // disable read only pins -#endif +#endif } diff --git a/wled00/wled.h b/wled00/wled.h index 217f7e977a..f29839fad6 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,33 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.14.0-b2 + @version 0.14.1-b1x @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2308250 +#define VERSION 2401030 + +// WLEDMM - you can check for this define in usermods, to only enabled WLEDMM specific code in the "right" fork. Its not defined in AC WLED. +#define _MoonModules_WLED_ + +//WLEDMM + Moustachauve/Wled-Native +// You can define custom product info from build flags. +// This is useful to allow API consumer to identify what type of WLED version +// they are interacting with. Be aware that changing this might cause some third +// party API consumers to consider this as a non-WLED device since the values +// returned by the API and by MQTT will no longer be default. However, most +// third party only uses mDNS to validate, so this is generally fine to change. +// For example, Home Assistant will still work fine even with this value changed. +// Use like this: +// -D WLED_BRAND="\"Custom Brand\"" +// -D WLED_PRODUCT_NAME="\"Custom Product\"" +#ifndef WLED_BRAND + #define WLED_BRAND "WLED" +#endif +#ifndef WLED_PRODUCT_NAME + #define WLED_PRODUCT_NAME "MoonModules" +#endif //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -50,7 +71,7 @@ #define WLED_ENABLE_FS_EDITOR // enable /edit page for editing FS content. Will also be disabled with OTA lock // to toggle usb serial debug (un)comment the following line -#define WLED_DEBUG +//#define WLED_DEBUG // filesystem specific debugging //#define WLED_DEBUG_FS @@ -65,7 +86,7 @@ //This is generally a terrible idea, but improves boot success on boards with a 3.3v regulator + cap setup that can't provide 400mA peaks //#define WLED_DISABLE_BROWNOUT_DET -// WLED-MM MANDATORY flags +// WLEDMM MANDATORY flags #define WLEDMM_PROTECT_SERVICE // prevents crashes when effects are drawing while asyncWebServer tries to modify segments at the same time // Library inclusions. @@ -156,7 +177,7 @@ // ESP32-WROVER features SPI RAM (aka PSRAM) which can be allocated using ps_malloc() // we can create custom PSRAMDynamicJsonDocument to use such feature (replacing DynamicJsonDocument) // The following is a construct to enable code to compile without it. -// There is a code thet will still not use PSRAM though: +// There is a code that will still not use PSRAM though: // AsyncJsonResponse is a derived class that implements DynamicJsonDocument (AsyncJson-v6.h) #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) // WLEDMM struct PSRAM_Allocator { @@ -173,6 +194,7 @@ struct PSRAM_Allocator { } }; using PSRAMDynamicJsonDocument = BasicJsonDocument; +//#define DynamicJsonDocument PSRAMDynamicJsonDocument // WLEDMM experiment #else #define PSRAMDynamicJsonDocument DynamicJsonDocument #endif @@ -320,6 +342,7 @@ WLED_GLOBAL bool noWifiSleep _INIT(true); // disabling #else WLED_GLOBAL bool noWifiSleep _INIT(false); #endif +WLED_GLOBAL bool force802_3g _INIT(false); #ifdef WLED_USE_ETHERNET #ifdef WLED_ETH_DEFAULT // default ethernet board type if specified @@ -425,7 +448,7 @@ WLED_GLOBAL uint16_t e131ProxyUniverse _INIT(0); // output this WLED_GLOBAL int dmxEnablePin _INIT(0); #endif -WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consequtive universes) +WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consecutive universes) WLED_GLOBAL uint16_t e131Port _INIT(5568); // DMX in port. E1.31 default is 5568, Art-Net is 6454 WLED_GLOBAL byte e131Priority _INIT(0); // E1.31 port priority (if != 0 priority handling is active) WLED_GLOBAL E131Priority highPriority _INIT(3); // E1.31 highest priority tracking, init = timeout in seconds @@ -528,7 +551,7 @@ WLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use i // wifi WLED_GLOBAL bool apActive _INIT(false); WLED_GLOBAL bool forceReconnect _INIT(false); -WLED_GLOBAL uint32_t lastReconnectAttempt _INIT(0); +WLED_GLOBAL unsigned long lastReconnectAttempt _INIT(0); WLED_GLOBAL bool interfacesInited _INIT(false); WLED_GLOBAL bool wasConnected _INIT(false); @@ -537,7 +560,7 @@ WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random co // transitions WLED_GLOBAL bool transitionActive _INIT(false); -WLED_GLOBAL uint16_t transitionDelayDefault _INIT(transitionDelay); // default transition time (storec in cfg.json) +WLED_GLOBAL uint16_t transitionDelayDefault _INIT(transitionDelay); // default transition time (stored in cfg.json) WLED_GLOBAL uint16_t transitionDelayTemp _INIT(transitionDelay); // actual transition duration (overrides transitionDelay in certain cases) WLED_GLOBAL unsigned long transitionStartTime; WLED_GLOBAL float tperLast _INIT(0.0f); // crossfade transition progress, 0.0f - 1.0f @@ -557,7 +580,7 @@ WLED_GLOBAL byte colNlT[] _INIT_N(({ 0, 0, 0, 0 })); // current nightligh WLED_GLOBAL unsigned long lastOnTime _INIT(0); WLED_GLOBAL bool offMode _INIT(!turnOnAtBoot); WLED_GLOBAL byte bri _INIT(briS); // global brightness (set) -WLED_GLOBAL byte briOld _INIT(0); // global brightnes while in transition loop (previous iteration) +WLED_GLOBAL byte briOld _INIT(0); // global brightness while in transition loop (previous iteration) WLED_GLOBAL byte briT _INIT(0); // global brightness during transition WLED_GLOBAL byte briLast _INIT(128); // brightness before turned off. Used for toggle function WLED_GLOBAL byte whiteLast _INIT(128); // white channel before turned off. Used for toggle function @@ -652,10 +675,11 @@ WLED_GLOBAL String escapedMac; WLED_GLOBAL DNSServer dnsServer; // network time +#define NTP_NEVER 999000000L WLED_GLOBAL bool ntpConnected _INIT(false); WLED_GLOBAL time_t localTime _INIT(0); -WLED_GLOBAL unsigned long ntpLastSyncTime _INIT(999000000L); -WLED_GLOBAL unsigned long ntpPacketSentTime _INIT(999000000L); +WLED_GLOBAL unsigned long ntpLastSyncTime _INIT(NTP_NEVER); +WLED_GLOBAL unsigned long ntpPacketSentTime _INIT(NTP_NEVER); WLED_GLOBAL IPAddress ntpServerIP; WLED_GLOBAL uint16_t ntpLocalPort _INIT(2390); WLED_GLOBAL uint16_t rolloverMillis _INIT(0); @@ -714,7 +738,7 @@ WLED_GLOBAL bool e131NewData _INIT(false); WLED_GLOBAL BusManager busses _INIT(BusManager()); WLED_GLOBAL WS2812FX strip _INIT(WS2812FX()); WLED_GLOBAL BusConfig* busConfigs[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES] _INIT({nullptr}); //temporary, to remember values from network callback until after -// WLEDMM a few "poor man's" mutal exclusion (mutex) flags, because there are not mutex objects on 8266. +// WLEDMM a few "poor man's" mutual exclusion (mutex) flags, because there are not mutex objects on 8266. WLED_GLOBAL volatile bool doInitBusses _INIT(false); // WLEDMM "volatile" added - needed as we want to sync parallel tasks WLED_GLOBAL volatile bool loadLedmap _INIT(false); // WLEDMM use as bool and use loadedLedmap for Nr WLED_GLOBAL volatile uint8_t loadedLedmap _INIT(0); // WLEDMM default 0 @@ -765,7 +789,17 @@ WLED_GLOBAL int8_t spi_sclk _INIT(HW_PIN_CLOCKSPI); #endif // global ArduinoJson buffer +#if defined(ALL_JSON_TO_PSRAM) && defined(WLED_USE_PSRAM_JSON) +// WLEDMM experimental : always use dynamic JSON + #ifndef WLED_DEFINE_GLOBAL_VARS + WLED_GLOBAL PSRAMDynamicJsonDocument doc; + #else + WLED_GLOBAL PSRAMDynamicJsonDocument doc(JSON_BUFFER_SIZE); + #warning experimental - trying to always use dynamic JSON + #endif +#else WLED_GLOBAL StaticJsonDocument doc; +#endif // WLEDMM end WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); // enable additional debug output