-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
[WIP] Add new audio-reactive palettes for smoothed FFT data #5467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
779a82f
5390cfc
746f8c1
9adbba6
bfb2b88
55e8868
09faf4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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 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 | ||
|
|
@@ -1683,6 +1685,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<NUM_GEQ_CHANNELS; i+=2) fftResult[i] = 16; // make a tiny pattern | ||
| inputLevel = 128; // reset level slider to default | ||
| autoResetPeak(); | ||
|
|
@@ -2232,6 +2235,64 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { | |
| value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2); | ||
| } | ||
| break; | ||
| 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: | ||
| // 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]; // 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai what's the meaning of "500" here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Here's what it means visually and mathematically: The centroid drives the palette hue through: float logC = log2f(constrain(centroid, 60.0f, 8000.0f));
uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f);Plugging in 500 Hz:
As a sanity check, the geometric mean of the clamped range [60 Hz, 8000 Hz] on a log scale is If you'd like the silence colour to be exactly at the visual centre of the palette's hue range (hue = 100), the corresponding frequency would be 🧠 Learnings used
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could the code also use logf() instead of log2f() ? This would avoid pulling in another libm function that needs flash space on 8266.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Current code: float logC = log2f(constrain(centroid, 60.0f, 8000.0f));
uint8_t baseHue = (uint8_t)mapf(logC, 5.9f, 13.0f, 0.0f, 200.0f);The constants float logC = logf(constrain(centroid, 60.0f, 8000.0f));
uint8_t baseHue = (uint8_t)mapf(logC, 4.094f, 8.987f, 0.0f, 200.0f);The output is bit-for-bit equivalent (same hue for any given centroid), and 🧠 Learnings used |
||
| // 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)); | ||
| 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 | ||
| hsv = CHSV(baseHue + hueSpread, saturation, (uint8_t)constrain(x, 30, 255)); | ||
softhack007 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| value = hsv; | ||
| break; | ||
| } | ||
| 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 | ||
| 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; | ||
| break; | ||
| } | ||
| } | ||
| return value; | ||
| } | ||
|
|
@@ -2240,6 +2301,15 @@ void AudioReactive::fillAudioPalettes() { | |
| if (!palettes) return; | ||
| size_t lastCustPalette = customPalettes.size(); | ||
| if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes; | ||
|
|
||
| // 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]); | ||
| } | ||
|
|
||
| for (int pal=0; pal<palettes; pal++) { | ||
| uint8_t tcp[16]; // Needs to be 4 times however many colors are being used. | ||
| // 3 colors = 12, 4 colors = 16, etc. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.