diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 757ad35482..013cb96398 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -71,7 +71,7 @@ #define PLOT_PRINTF(x...) #endif -#define MAX_PALETTES 3 +#define MAX_PALETTES 5 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) @@ -98,6 +98,9 @@ static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same t 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 +static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages used only by audio palettes 3 & 4 (EMA, alpha=0.05 → ~400ms time constant at 20ms cycle) +static constexpr float PALETTE_SMOOTHING = 0.05f; // EMA smoothing factor for paletteBandAvg: 0.05 gives ~400ms time constant; increase for faster response, decrease for slower + // 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 @@ -1683,6 +1686,7 @@ class AudioReactive : public Usermod { memset(fftCalc, 0, sizeof(fftCalc)); memset(fftAvg, 0, sizeof(fftAvg)); memset(fftResult, 0, sizeof(fftResult)); + memset(paletteBandAvg, 0, sizeof(paletteBandAvg)); for(int i=(init?0:1); i 1.0f) ? (wSum / tEnergy) : 500.0f; + // Map centroid to hue on a log scale (human pitch perception is logarithmic). + // log2(60 Hz) ≈ 5.9, log2(8000 Hz) ≈ 13.0 → hue range 0..200 (red → blue-purple) + float logC = log2f(constrain(centroid, 60.0f, 8000.0f)); // softhack007 ToDO: use logf() instead of log2f() + uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f); // mapf() cannot produce negative results due to previous constrain() --> safe to directly cast to uint8_t + int8_t hueSpread = map(x, 0, 255, -30, 30); // spread palette positions ±30 hue units // softhack007 ToDO: use CHSV32 with 16bit HUE + uint8_t saturation = (uint8_t)constrain((int)(tEnergy / 6.0f) + 180, 180, 255); // louder = more saturated // softhack007 WTF dude? + hsv = CHSV(baseHue + hueSpread, saturation, (uint8_t)constrain(x, 30, 255)); + value = hsv; + break; + } + // AI: end + // AI: below section was generated by an AI + case 4: { + // "Spectral Balance" palette (palette index 4) + // Divides the spectrum into three broad bands and uses their energy ratio to derive hue: + // bass dominant (channels 0-3, ~43-301 Hz) → warm hue ≈ 20 (red/orange) + // mid dominant (channels 4-9, ~301-1895 Hz) → green hue ≈ 110 (green/cyan) + // high dominant (channels 10-15, ~1895-9259 Hz)→ cool hue ≈ 190 (blue/violet) + // x (0-255) spreads palette positions ±25 hue units around that weighted hue, + // giving a smooth colour band rather than a single flat colour. + 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 = bassEnergy + midEnergy + highEnergy; + if (total < 1.0f) total = 1.0f; // 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 = roundf(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); + uint8_t hue = min(255, max(0, weightedHue)); // clamp to [0...255] + // Saturation: dominated spectrum (one band clearly wins) → high sat; balanced → lower sat + float maxRatio = max(bassRatio, max(midRatio, highRatio)); + uint8_t sat = (uint8_t)constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255); // softhack007 OMG, WTF? + int8_t hueOffset = map(x, 0, 255, -25, 25); // spread palette positions ±25 hue units // softhack007 ToDO: use CHSV32 with 16bit HUE + // brightness: minimum 30, boosted by overall loudness and palette position + uint8_t val = (uint8_t)constrain((int)(total / 8.0f) + (int)map(x, 0, 255, 30, 255), 30, 255); + hsv = CHSV(hue + hueOffset, sat, val); + value = hsv; + break; + } + // AI: end default: if (x == 1) { value = CRGB(fftResult[10]/2, fftResult[4]/2, fftResult[0]/2); @@ -2240,6 +2308,17 @@ void AudioReactive::fillAudioPalettes() { if (!palettes) return; size_t lastCustPalette = customPalettes.size(); if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes; + + // AI: below section was generated by an AI + // Update slowly-smoothed band averages used by palettes 3 & 4. + // Alpha=PALETTE_SMOOTHING gives ~400ms time constant at a 20ms update cycle, + // so palette colours reflect the overall tonal character of the music rather than + // reacting to individual beats (which would appear "twitchy"). + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + paletteBandAvg[i] += PALETTE_SMOOTHING * ((float)fftResult[i] - paletteBandAvg[i]); // BUG: this IIR filter assumes 20ms activation rate (which is totally wrong) + } + // AI: end + for (int pal=0; pal