From 779a82faafe44327d7c2ebee7768fb811fd0ee46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:31:41 +0000 Subject: [PATCH 1/5] Initial plan From 5390cfcf437121c3717c57b5cd75788afd2ff23d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:01:58 +0000 Subject: [PATCH 2/5] Add two new smoothed audio-reactive palettes (Track Character + Spectral Balance) Agent-Logs-Url: https://github.com/wled/WLED/sessions/a11e5210-96ae-4713-abdd-ba42b0e33bc5 Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- usermods/audioreactive/audio_reactive.cpp | 56 ++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 757ad35482..594e992876 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) @@ -226,6 +226,7 @@ static uint64_t sampleTime = 0; // FFT Task variables (filtering and post-processing) 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) +static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages for audio palettes (EMA, ~400ms time constant) #ifdef SR_DEBUG static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. #endif @@ -1683,6 +1684,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; + float logC = log2f(constrain(centroid, 60.0f, 8000.0f)); + // log2(60)≈5.9, log2(8000)≈13.0 → map to hue 0..200 + uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f); + // Spread palette positions around centroid hue + int8_t hueSpread = map(x, 0, 255, -30, 30); + uint8_t saturation = (uint8_t)constrain((int)(tEnergy / 6.0f) + 180, 180, 255); + hsv = CHSV(baseHue + hueSpread, saturation, (uint8_t)constrain(x, 30, 255)); + value = hsv; + break; + } + case 4: { + // "Spectral Balance" palette - bass vs mid vs high energy balance + float bassEnergy = 0, midEnergy = 0, highEnergy = 0; + for (int i = 0; i < 4; i++) bassEnergy += paletteBandAvg[i]; + for (int i = 4; i < 10; i++) midEnergy += paletteBandAvg[i]; + for (int i = 10; i < 16; i++) highEnergy += paletteBandAvg[i]; + float total = bassEnergy + midEnergy + highEnergy; + if (total < 1.0f) total = 1.0f; + float bassRatio = bassEnergy / total; + float midRatio = midEnergy / total; + float highRatio = highEnergy / total; + // Weighted hue: bass→warm(20), mid→green(110), high→cool(190) + uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); + // More concentrated spectrum = more saturated + float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); + uint8_t sat = (uint8_t)constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255); + // Spread across palette position + int8_t hueOffset = map(x, 0, 255, -25, 25); + 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; + } } return value; } @@ -2240,6 +2287,13 @@ void AudioReactive::fillAudioPalettes() { if (!palettes) return; size_t lastCustPalette = customPalettes.size(); if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes; + + // Update smoothed band averages for palettes 3 and 4 (EMA, ~400ms time constant at 20ms cycle) + static constexpr float PALETTE_SMOOTHING = 0.05f; + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + paletteBandAvg[i] += PALETTE_SMOOTHING * ((float)fftResult[i] - paletteBandAvg[i]); + } + for (int pal=0; pal Date: Thu, 9 Apr 2026 17:12:35 +0000 Subject: [PATCH 3/5] Improve documentation of new audio palettes per reviewer feedback Agent-Logs-Url: https://github.com/wled/WLED/sessions/2dff17f8-2992-4060-8a37-5c72dd1fbf51 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- usermods/audioreactive/audio_reactive.cpp | 64 ++++++++++++++--------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 594e992876..73a07ecea0 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -226,7 +226,8 @@ static uint64_t sampleTime = 0; // FFT Task variables (filtering and post-processing) 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) -static float paletteBandAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Slowly smoothed band averages for audio palettes (EMA, ~400ms time constant) +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 #ifdef SR_DEBUG static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. #endif @@ -2235,45 +2236,58 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { } break; case 3: { - // "Track Character" palette - smoothed spectral centroid drives hue - static const float bandFreq[NUM_GEQ_CHANNELS] = { + // "Track Character" palette (palette index 3) + // Uses the spectral centroid of paletteBandAvg[] to derive a single hue that + // reflects the tonal balance of the music over the past ~400ms: + // low centroid (bass-heavy drop) → warm red/orange (hue ≈ 0) + // mid centroid (vocals/melody) → green/cyan (hue ≈ 80-120) + // high centroid (cymbals/bright synth) → blue/purple (hue ≈ 200) + // x (0-255) spreads palette positions ±30 hue units around that base hue. + static const float bandFreq[NUM_GEQ_CHANNELS] = { // approximate centre frequency (Hz) of each GEQ channel 65, 107, 172, 258, 365, 495, 689, 969, 1270, 1658, 2153, 2713, 3359, 4091, 5792, 8182 }; float wSum = 0, tEnergy = 0; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - wSum += paletteBandAvg[i] * bandFreq[i]; - tEnergy += paletteBandAvg[i]; + wSum += paletteBandAvg[i] * bandFreq[i]; // frequency-weighted energy + tEnergy += paletteBandAvg[i]; // total energy } + // centroid = energy-weighted average frequency; default to 500 Hz when signal is silent float centroid = (tEnergy > 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)); - // log2(60)≈5.9, log2(8000)≈13.0 → map to hue 0..200 uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f); - // Spread palette positions around centroid hue - int8_t hueSpread = map(x, 0, 255, -30, 30); - uint8_t saturation = (uint8_t)constrain((int)(tEnergy / 6.0f) + 180, 180, 255); + int8_t hueSpread = map(x, 0, 255, -30, 30); // spread palette positions ±30 hue units + uint8_t saturation = (uint8_t)constrain((int)(tEnergy / 6.0f) + 180, 180, 255); // louder = more saturated hsv = CHSV(baseHue + hueSpread, saturation, (uint8_t)constrain(x, 30, 255)); value = hsv; break; } case 4: { - // "Spectral Balance" palette - bass vs mid vs high energy balance + // "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]; - for (int i = 4; i < 10; i++) midEnergy += paletteBandAvg[i]; - for (int i = 10; i < 16; i++) highEnergy += paletteBandAvg[i]; + 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; - float bassRatio = bassEnergy / total; + 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: bass→warm(20), mid→green(110), high→cool(190) - uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); - // More concentrated spectrum = more saturated - float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); - uint8_t sat = (uint8_t)constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255); - // Spread across palette position - int8_t hueOffset = map(x, 0, 255, -25, 25); + // Weighted hue: pure bass→20, pure mid→110, pure high→190 + uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); + // Saturation: dominated spectrum (one band clearly wins) → high sat; balanced → lower sat + float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); + uint8_t sat = (uint8_t)constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255); + int8_t hueOffset = map(x, 0, 255, -25, 25); // spread palette positions ±25 hue units + // 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; @@ -2288,8 +2302,10 @@ void AudioReactive::fillAudioPalettes() { size_t lastCustPalette = customPalettes.size(); if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes; - // Update smoothed band averages for palettes 3 and 4 (EMA, ~400ms time constant at 20ms cycle) - static constexpr float PALETTE_SMOOTHING = 0.05f; + // 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]); } From bfb2b885597435bc56d9453f84a17111f8ede0f5 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:51:31 +0200 Subject: [PATCH 4/5] fix some bugs, mark some leftover errors * move paletteBandAvg out of esp32-only section (fixes 8266 build error) * style: move default case _below_ other cases * add missing "generated by AI" labels * mark some leftover bugs --- usermods/audioreactive/audio_reactive.cpp | 45 +++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 73a07ecea0..388faf94a6 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -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 @@ -226,8 +229,6 @@ static uint64_t sampleTime = 0; // FFT Task variables (filtering and post-processing) 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) -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 #ifdef SR_DEBUG static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. #endif @@ -2226,19 +2227,11 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255)); // pick hue value = hsv; // convert to R,G,B break; - default: - if (x == 1) { - value = CRGB(fftResult[10]/2, fftResult[4]/2, fftResult[0]/2); - } else if(x == 255) { - value = CRGB(fftResult[10]/2, fftResult[0]/2, fftResult[4]/2); - } else { - value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2); - } - break; + // AI: below section was generated by an AI case 3: { // "Track Character" palette (palette index 3) // Uses the spectral centroid of paletteBandAvg[] to derive a single hue that - // reflects the tonal balance of the music over the past ~400ms: + // reflects the tonal balance of the music over the past ~400ms: // softhack007 this statement is wrong // low centroid (bass-heavy drop) → warm red/orange (hue ≈ 0) // mid centroid (vocals/melody) → green/cyan (hue ≈ 80-120) // high centroid (cymbals/bright synth) → blue/purple (hue ≈ 200) @@ -2256,14 +2249,16 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { float centroid = (tEnergy > 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)); + 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); - int8_t hueSpread = map(x, 0, 255, -30, 30); // spread palette positions ±30 hue units - uint8_t saturation = (uint8_t)constrain((int)(tEnergy / 6.0f) + 180, 180, 255); // louder = more saturated + 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: @@ -2284,15 +2279,25 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { // Weighted hue: pure bass→20, pure mid→110, pure high→190 uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); // Saturation: dominated spectrum (one band clearly wins) → high sat; balanced → lower sat - float maxRatio = fmaxf(bassRatio, fmaxf(midRatio, highRatio)); - uint8_t sat = (uint8_t)constrain((int)(maxRatio * 255.0f * 1.5f), 180, 255); - int8_t hueOffset = map(x, 0, 255, -25, 25); // spread palette positions ±25 hue units + 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); + } else if(x == 255) { + value = CRGB(fftResult[10]/2, fftResult[0]/2, fftResult[4]/2); + } else { + value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2); + } + break; } return value; } @@ -2302,13 +2307,15 @@ void AudioReactive::fillAudioPalettes() { 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]); + 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 Date: Fri, 10 Apr 2026 01:52:07 +0200 Subject: [PATCH 5/5] fix a few more AI-generated bugs WTF, dude --- usermods/audioreactive/audio_reactive.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 388faf94a6..013cb96398 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -1727,6 +1727,7 @@ class AudioReactive : public Usermod { // reset sound data volumeRaw = 0; volumeSmth = 0; for(int i=(init?0:1); i 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)); @@ -2277,7 +2278,8 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { float midRatio = midEnergy / total; float highRatio = highEnergy / total; // Weighted hue: pure bass→20, pure mid→110, pure high→190 - uint8_t hue = (uint8_t)(bassRatio * 20.0f + midRatio * 110.0f + highRatio * 190.0f); + 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?