From 3e494cc55155c9c6cfc0079bd95ce15f884f3f60 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:07:37 +0200 Subject: [PATCH] removed broken frequency squelch, added frequency scaling options - removed broken FFTResult "squelch" feature. It was completely broken, and caused flashes in GEQ. - added Frequency scaling options: linear and logarithmic - fixed a few numerical accidents in FX.cpp (bouncing_balls, ripplepeak, freqmap, gravfreq, waterfall) --- usermods/audioreactive/audio_reactive.h | 150 +++++++++++++++++------- wled00/FX.cpp | 14 ++- 2 files changed, 117 insertions(+), 47 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index c7c7e5f8..02b20def 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -64,6 +64,8 @@ static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 static bool limiterOn = true; // bool: enable / disable dynamics limiter static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec +// user settable options for FFTResult scaling +static uint8_t FFTScalingMode = 2; // 0 none; 1 optimized logarithmic; 2 optimized linear // // AGC presets @@ -88,8 +90,14 @@ static AudioSource *audioSource = nullptr; static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) +static int16_t rawSampleAgc = 0; // not smoothed AGC sample +static float sampleAvg = 0.0f; // Smoothed Average sampleRaw +static float sampleAgc = 0.0f; // Smoothed AGC sample + //////////////////// // Begin FFT Code // //////////////////// @@ -113,6 +121,9 @@ static float vReal[samplesFFT] = {0.0f}; static float vImag[samplesFFT] = {0.0f}; static float fftBin[samplesFFT_2] = {0.0f}; +#define FFT_DOWNSCALE 0.65f // downscaling factor for FFT results - "Flat-Top" window +#define LOG_256 5.54517744 + #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT static float windowWeighingFactors[samplesFFT] = {0.0f}; #endif @@ -131,9 +142,6 @@ static unsigned long fftTime = 0; static unsigned long sampleTime = 0; #endif -// Table of linearNoise results to be multiplied by soundSquelch in order to reduce squelch across fftResult bins. -static uint8_t linearNoise[16] = { 34, 28, 26, 25, 20, 12, 9, 6, 4, 4, 3, 2, 2, 2, 2, 2 }; - // Table of multiplication factors so that we can even out the frequency response. static float fftResultPink[16] = { 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 }; @@ -146,6 +154,11 @@ static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); static TaskHandle_t FFT_Task = nullptr; +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + static float fftAddAvg(int from, int to) { float result = 0.0f; for (int i = from; i <= to; i++) { @@ -213,7 +226,8 @@ void FFTcode(void * parameter) #ifdef UM_AUDIOREACTIVE_USE_NEW_FFT FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data + FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy + //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection FFT.compute( FFTDirection::Forward ); // Compute FFT FFT.complexToMagnitude(); // Compute magnitudes #else @@ -253,44 +267,84 @@ void FFTcode(void * parameter) * Multiplier = (End frequency/ Start frequency) ^ 1/16 * Multiplier = 1.320367784 */ + if (sampleAvg > 1) { // noise gate open // Range - fftCalc[ 0] = fftAddAvg(3,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,255); // 3880 - 5120 + fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 + fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 + fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 + fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 + fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 + fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 + fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 + fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 + fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 + fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 + fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 + fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 + fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 + fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 + fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 + fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate + + } else { // noise gate closed + for (int i=0; i < 16; i++) { + fftCalc[i] *= 0.82f; // decay to zero + if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; + } + } for (int i=0; i < 16; i++) { - // Noise supression of fftCalc bins using soundSquelch adjustment for different input types. - fftCalc[i] = (fftCalc[i] < ((float)soundSquelch * (float)linearNoise[i] / 4.0f)) ? 0 : fftCalc[i]; - // Adjustment for frequency curves. - fftCalc[i] *= fftResultPink[i]; - // Manual linear adjustment of gain using sampleGain adjustment for different input types. - fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //with inputLevel adjustment - + + if (sampleAvg > 1) { // noise gate open + // Adjustment for frequency curves. + fftCalc[i] *= fftResultPink[i]; + if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function + // Manual linear adjustment of gain using sampleGain adjustment for different input types. + fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //with inputLevel adjustment + if(fftCalc[i] < 0) fftCalc[i] = 0; + } + // smooth results - rise fast, fall slower if(fftCalc[i] > fftAvg[i]) // rise fast fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] else // fall slow - fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // will need approx 5 cycles (150ms) for converging against fftCalc[i] - //fftAvg[i] = fftCalc[i]*0.05f + 0.95f*fftAvg[i]; // will need approx 10 cycles (250ms) for converging against fftCalc[i] + fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // will need approx 5 cycles (150ms) for converging against fftCalc[i] + + // constrain internal vars - just to be sure + fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); + fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); + + float currentResult; + if(limiterOn == true) + currentResult = fftAvg[i]; + else + currentResult = fftCalc[i]; + + if (FFTScalingMode > 0) { + if (FFTScalingMode == 1) { + // Logarithmic scaling + currentResult *= 0.42; // 42 is the answer ;-) + currentResult -= 8.0; // this skips the lowest row, giving some room for peaks + 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] + + } else { + // Linear scaling + currentResult *= 0.30f; // needs a bit more damping, get stay below 255 + currentResult -= 4.0; // giving a bit more room for peaks + if (currentResult < 1.0f) currentResult = 0.0f; + currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies + } + } else { + currentResult -= 4; // just a bit more room for peaks + } // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. - if(limiterOn == true) - fftResult[i] = constrain((int)fftAvg[i], 0, 254); - else - fftResult[i] = constrain((int)fftCalc[i], 0, 254); + fftResult[i] = constrain((int)currentResult, 0, 255); } #ifdef WLED_DEBUG @@ -396,12 +450,7 @@ class AudioReactive : public Usermod { bool udpSamplePeak = 0; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel; smoothed over 16 samples) double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controler. - float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. - float sampleAvg = 0.0f; // Smoothed Average sampleRaw - float sampleAgc = 0.0f; // Our AGC sample - int16_t rawSampleAgc = 0; // Our AGC sample - raw uint32_t timeOfPeak = 0; unsigned long lastTime = 0; // last time of running UDP Microphone Sync float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller @@ -941,7 +990,7 @@ 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.isUpdating() && (millis() - lastUMRun < 1)) return; // be nice, but not too nice // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please odd other orrides here if needed @@ -1115,6 +1164,11 @@ class AudioReactive : public Usermod { sampleRaw = 0; rawSampleAgc = 0; my_magnitude = 0; FFT_Magnitude = 0; FFT_MajorPeak = 0; multAgc = 1; + // reset FFT data + memset(fftCalc, 0, sizeof(fftCalc)); + memset(fftAvg, 0, sizeof(fftAvg)); + memset(fftResult, 0, sizeof(fftResult)); + for(int i=(init?0:1); i<16; i+=2) fftResult[i] = 16; // make a tiny pattern if (init && FFT_Task) { vTaskSuspend(FFT_Task); // update is about to begin, disable task to prevent crash @@ -1372,6 +1426,9 @@ class AudioReactive : public Usermod { dynLim[F("Rise")] = attackTime; dynLim[F("Fall")] = decayTime; + JsonObject freqScale = top.createNestedObject("Frequency"); + freqScale[F("Scale")] = FFTScalingMode; + JsonObject sync = top.createNestedObject("sync"); sync[F("port")] = audioSyncPort; sync[F("mode")] = audioSyncEnabled; @@ -1418,6 +1475,8 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top["dynamics"][F("Rise")], attackTime); configComplete &= getJsonValue(top["dynamics"][F("Fall")], decayTime); + configComplete &= getJsonValue(top["Frequency"][F("Scale")], FFTScalingMode); + configComplete &= getJsonValue(top["sync"][F("port")], audioSyncPort); configComplete &= getJsonValue(top["sync"][F("mode")], audioSyncEnabled); @@ -1443,11 +1502,14 @@ class AudioReactive : public Usermod { oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:Limiter');")); oappend(SET_F("addOption(dd,'Off',0);")); oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:Limiter',0,' Limiter On ');")); // 0 is field type, 1 is actual field - //oappend(SET_F("addInfo('AudioReactive:dynamics:Rise',0,'min. ');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:Rise',1,' ms
(volume reactive FX only)');")); - //oappend(SET_F("addInfo('AudioReactive:dynamics:Fall',0,'min. ');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:Fall',1,' ms
(volume reactive FX only)');")); + oappend(SET_F("addInfo('AudioReactive:dynamics:Limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:dynamics:Rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo('AudioReactive:dynamics:Fall',1,'ms (♪ effects only)');")); + + oappend(SET_F("dd=addDropdown('AudioReactive','Frequency:Scale');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); + oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); oappend(SET_F("addOption(dd,'Off',0);")); diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b4104937..b5a9ab79 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2842,12 +2842,13 @@ uint16_t mode_bouncing_balls(void) { for (size_t i = 0; i < numBalls; i++) { float timeSinceLastBounce = (time - balls[i].lastBounceTime)/((255-SEGMENT.speed)*8/256 +1); - balls[i].height = 0.5 * gravity * pow(timeSinceLastBounce/1000 , 2.0) + balls[i].impactVelocity * timeSinceLastBounce/1000; + float timeSec = timeSinceLastBounce/1000.0f; + balls[i].height = 0.5 * gravity * (timeSec * timeSec) + balls[i].impactVelocity * timeSec; // avoid use pow(x, 2) - its extremely slow ! if (balls[i].height < 0) { //start bounce balls[i].height = 0; //damping for better effect using multiple balls - float dampening = 0.90 - float(i)/pow(numBalls,2); + float dampening = 0.90 - float(i)/(float(numBalls) * float(numBalls)); // avoid use pow(x, 2) - its extremely slow ! balls[i].impactVelocity = dampening * balls[i].impactVelocity; balls[i].lastBounceTime = time; @@ -2863,7 +2864,7 @@ uint16_t mode_bouncing_balls(void) { color = SEGCOLOR(i % NUM_COLORS); } - uint16_t pos = round(balls[i].height * (SEGLEN - 1)); + uint16_t pos = roundf(balls[i].height * (SEGLEN - 1)); SEGMENT.setPixelColor(pos, color); } @@ -6006,7 +6007,9 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli case 255: // Initialize ripple variables. ripples[i].pos = random16(SEGLEN); #ifdef ESP32 + if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); + else ripples[i].color = 0; #else ripples[i].color = random8(); #endif @@ -6716,6 +6719,7 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. } float FFT_MajorPeak = *(float*) um_data->u_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 4.0f; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) SEGMENT.fade_out(SEGMENT.speed); @@ -6806,6 +6810,7 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. } float FFT_MajorPeak = *(float*) um_data->u_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. SEGMENT.fade_out(fadeRate); @@ -6908,6 +6913,7 @@ uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. } float FFT_MajorPeak = *(float*) um_data->u_data[4]; float volumeSmth = *(float*) um_data->u_data[0]; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) SEGMENT.fade_out(240); @@ -7022,6 +7028,8 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin uint8_t *binNum = (uint8_t*)um_data->u_data[7]; float my_magnitude = *(float*) um_data->u_data[5] / 8.0f; + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) + if (SEGENV.call == 0) { SEGMENT.setUpLeds(); SEGMENT.fill(BLACK);