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);