Skip to content

PoC - MSGEQ7 based AudioReactive#5673

Draft
netmindz wants to merge 5 commits into
wled:mainfrom
netmindz:msgeq7
Draft

PoC - MSGEQ7 based AudioReactive#5673
netmindz wants to merge 5 commits into
wled:mainfrom
netmindz:msgeq7

Conversation

@netmindz

@netmindz netmindz commented Jun 7, 2026

Copy link
Copy Markdown
Member

This pull request introduces a new MSGEQ7 usermod for WLED, providing a software-based emulation of the classic MSGEQ7 seven-band graphic equalizer IC, with optional support for the physical chip. It also adds documentation and a Python tool for validating the band response using a sine sweep test. The usermod is designed to be a drop-in replacement for the existing audioreactive usermod, with identical data output, but lower RAM usage and no external FFT library dependency.

Key additions and improvements:

New MSGEQ7 usermod and documentation

  • Added msgeq7 usermod, which emulates the MSGEQ7 IC in software using IIR bandpass filters and supports optional hardware chip input. This allows all existing WLED audio-reactive effects to work without modification. (usermods/msgeq7/readme.md, usermods/msgeq7/library.json) [1] [2]
  • Provided detailed documentation covering backend selection (software/hardware), hardware wiring guides for both backends, configuration settings, comparison with the original audioreactive usermod, and validation instructions. (usermods/msgeq7/readme.md)

Validation tooling

  • Added a Python script sweep_analyze.py to analyze serial logs from a sine sweep test, plot band amplitudes over time, and verify correct filter chain operation. This helps users validate the accuracy of the band response. (usermods/msgeq7/tools/sweep_analyze.py)

netmindz added 2 commits June 7, 2026 11:06
…chip support

Implements a drop-in replacement for the audioreactive usermod that uses
seven biquad bandpass IIR filters at the classic MSGEQ7 center frequencies
(63/160/400/1k/2.5k/6.25k/16k Hz) instead of FFT. Produces the identical
um_data_t 8-slot structure so all existing audio-reactive effects work
without modification (registers as USERMOD_ID_AUDIOREACTIVE).

Software backend (default):
- I2S/ADC mic capture via audio_source.h (copied from audioreactive,
  supports INMP441, ES7243, SPH0645, ES8388, PDM, ADC)
- esp-dsp dsps_biquad_f32_ae32/aes3 SIMD bandpass filters at 44100 Hz
- Asymmetric peak-hold envelope (15 ms attack / 80 ms decay)
- Log compression matching real MSGEQ7 chip output characteristic
- 7-to-16 channel log-frequency interpolation (weights precomputed at setup)
- FFT_MajorPeak via parabolic interpolation between top-2 bands
- Beat/samplePeak detection from sub-bass rate-of-rise
- FreeRTOS task on Core 0, no external library dependency

Hardware backend (optional):
- Physical MSGEQ7 chip via strobe/reset/ADC1 GPIO pins
- Standard pulse-and-read protocol, ~50 Hz update rate

Also includes readme.md (wiring, settings, effect caveats) and
tools/sweep_analyze.py for serial-log band response validation.
- s_swTaskHandle was not volatile, allowing the compiler to optimize away
  the polling loop in _stopProcessing(). Now declared volatile.
- The task never nulled s_swTaskHandle before vTaskDelete(), so
  _stopProcessing() always timed out at 500 ms even though the task
  exited within ~1 ms. Now nulled in-task before self-deletion.
- xTaskCreatePinnedToCore() requires a non-volatile TaskHandle_t*; use a
  local temporary and store it to the volatile after the call.
- s_volumeRaw / _volumeRaw typed int16_t but registered as UMT_UINT16;
  changed to uint16_t throughout for consistency with the slot type.
- Remove readme paragraph referencing MSGEQ7_DEBUG_SWEEP, a define that
  was never implemented.
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 625e4e40-d45c-48e8-92f3-45f16e4ec295

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@netmindz netmindz requested a review from softhack007 June 7, 2026 10:15
@netmindz netmindz added the AI Partly generated by an AI. Make sure that the contributor fully understands the code! label Jun 7, 2026
@netmindz

netmindz commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Had a quick go at seeing what AI would come up with for software based MSGEQ7 emulation. Put as it's own usermod with the idea that it would be easiest to see in isolation of the rest of the AR code, but not sure that was the right choice. A PR with just the additions might have been easier to see @softhack007

… resolution

- Software backend now scales envelopes against a fixed full-scale reference
  (int16 32768) instead of per-frame band peak. Preserves absolute amplitude
  so quiet music stays quiet, matching audioreactive's fftResult[] semantics
  and what every WLED audio-reactive effect expects.
- Move squelch from pre-compression (compared against int16-scale envelope —
  effectively dead code at the default value) to post-compression on the
  user-facing 0..255 scale. Update default 8 -> 10 to match audioreactive.
- Default filter Q changed 1.4 -> 1.0 to match the real MSGEQ7 chip's
  ~1.32-octave band spacing (Q = sqrt(2^N)/(2^N-1) with N=1.32).
- Hardware backend: scale 12-bit ADC with the full 0..4095 range before
  truncating to 0..255, instead of dropping the bottom 4 bits with >>4.
Comment thread usermods/msgeq7/msgeq7.cpp Outdated
float decayC = p->decayCoeff;
float env = envelope[b];
for (int i = 0; i < MSGEQ7_BLOCK_SIZE; i++) {
float absVal = fabsf(filteredBuf[i]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you may get better results by sing RMS instead of simple abs(), I saw less "jitter" in my tests when doing so

@DedeHai

DedeHai commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

by coincidence I just today dug up the code for MSGEQ7 we talked about on discord last year and gave it another spin. I think this more complete implementation takes better advantage of it - its main advantage being low latency. I see you increased the sampling rate and reduced the block size.
What I found in my tests this morning as that the output of the filters I chose (did not check you did took the same approach) is that the lower frequency bands tend to show less amplitude, some correction factors may be needed.

I also have test code up and running on my visualiser tool - using a 16-band filter. I am not sure what to make of it though, it is sometimes much cleaner in separating the lower frequencies than the FFT but on the other hand is more selective as well for higher frequencies.

I am also running my code still on 22kHz, the highest band is a bit "crippled" but it saves computation time. Need some real world test to see if for music display anything above 10kHz is really that relevant: as long as it picks up on hi-hat sounds I think it should be ok.

Move all signal processing and hardware protocol code out of the usermod
class into a standalone header (msgeq7_engine.h), leaving msgeq7.cpp as
thin WLED-specific glue only.

msgeq7_engine.h now owns:
  - All MSGEQ7 constants
  - Shared volatile output state (s_bandEnvelope, s_volumeSmth, etc.)
  - 7→16 channel GEQ interpolation table (buildInterpolationTable)
  - Biquad bandpass filter bank (initBiquadCoeffs, timeConstToCoeff)
  - FreeRTOS software processing task (softwareProcessingTask)
  - Physical chip GPIO protocol (msgeq7_hw_gpio_init, msgeq7_hw_read)

msgeq7.cpp retains:
  - AudioSource construction (I2S/ADC driver setup, audio_source.h types)
  - PinManager pin reservation/release
  - um_data_t registration and WLED effect API
  - JSON config persistence and web UI helpers

This makes the engine code self-contained and easier to transplant into
the audioreactive usermod if the PoC proves successful.
@netmindz

netmindz commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

I've just refactored the code to try and separate the usermod "harness" from the actual MSGEQ7 code

Bug 1 (critical): SW task was never stopped on SW→HW mode switch.
readFromConfig() updates _useHardwareChip before calling _stopProcessing(),
so the old 'if (!_useHardwareChip && s_swTaskHandle)' guard always evaluated
false on a mode switch, leaving the task running and causing use-after-free
when params and AudioSource were then deleted. Fix: check s_swTaskHandle
alone, independent of current mode.

Bug 2 (critical): If the 500ms stop timeout expired, _swTaskParams and
_audioSource were unconditionally deleted while the still-running task held
pointers to both. Fix: on timeout, null both pointers without freeing
(accepting a small leak in this pathological scenario) so the running task
never dereferences freed memory.

Bug 3: Partial allocatePin failure leaked already-allocated pins.
_deinitHardwareChip() was called before _hwChipReady was set, making it a
no-op. Fix: explicitly call deallocatePin for each pin on failure path;
PinManager::deallocatePin is a safe no-op for pins not owned by us.

Bug 4: On malloc failure inside softwareProcessingTask, the task called
vTaskDelete without first clearing s_swTaskHandle, leaving _stopProcessing()
to busy-poll a dead handle for 500ms. Fix: set s_swTaskHandle = nullptr
before vTaskDelete in the malloc-failure path.

Also add a prominent single-TU contract comment to msgeq7_engine.h (the
static globals break silently if the header is included in >1 TU), and
add a Source layout section to readme.md.
@softhack007

softhack007 commented Jun 7, 2026

Copy link
Copy Markdown
Member

🤔 actually the AI code is extremely over-compilcated, and it copies lots of things from AR, but also omit a lot of other stuff that was introduced for improving robustness.

I like the idea of IIR filters instead of a full FFT - for 7 channels the IIR filterbank should -in theorie- perform better than a full FFT.

Maybe I can pick parts of the code, and integrate it into the existing AR framework as a new audio engine.

A few thoughts in general:

  • (question) how do you extract the overall maxSample from the 7 channels provided by the chip?
  • For compatibility with existing effects, 7 channels of the chip should get mapped to 16 GEQ channels.
  • I think the MSGEQ7 has some kind of "sample peak hold" feature, that auto-releases after reading out values. To simulate this, we'd need to add something like um_data->flush so effects can signal the audio core to clean the "peak holder".
  • ideally the difference to AR should be kept small, for example
    • new FFTcode function (background task) that replaces the normal fft
    • modified postProcessFFTResults for making the frequency bands look awesome
    • (maybe) a new detectSamplePeak(), if peak detection must be done differently.

float decayC = timeConstToCoeff(_decayMs * 0.001f, (float)MSGEQ7_SAMPLE_RATE);
float linearGain = _gainPercent / 128.0f;

_swTaskParams = new SWTaskParams{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this ???

@DedeHai

DedeHai commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator
  • For compatibility with existing effects, 7 channels of the chip should get mapped to 16 GEQ channels.

I have a 16 channel version up and running (will email you shortly after testing latest code) - I did not measure performance, it is probably slower than FFT BUT it can run on much fewer samples, say 64 or 128, cutting down on latency. From what I saw in my tests it outperforms FFT in terms of "clarity" but that may be due to test-parameters. Also it needs tuning of bin-scaling etc. Another adavantage is: it requires no pre-filtering and "band-width" i.e. Q factor could be a UI parameter.

while the 16-band version has little in common with the MSGEQ7 it may be a viable alternative to FFT. I let you be the judge of that.

// Compute envelope time-constant coefficients from ms settings.
// Envelope is updated per sample, so use the sample rate here.
float attackC = timeConstToCoeff(_attackMs * 0.001f, (float)MSGEQ7_SAMPLE_RATE);
float decayC = timeConstToCoeff(_decayMs * 0.001f, (float)MSGEQ7_SAMPLE_RATE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attack/decay filters in AR are not depending on sample rate ... not sure what the AI want to do here, but it look really wrong....

int8_t _pinStrobe = -1;
int8_t _pinReset = -1;
int8_t _pinOut = -1;
uint8_t _gainPercent = 128; // 128 = unity gain

@softhack007 softhack007 Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to do this properly, don't rely on uint8_t... in hindsight using 8bit for squelch/gain was a mistake in AR.

uint32_t _lastLoopMs = 0;

// PROGMEM key for config JSON
static const char _name[];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is effectively the same as nullptr.

Comment thread usermods/msgeq7/readme.md
| 3 | 1 000 Hz |
| 4 | 2 500 Hz |
| 5 | 6 250 Hz |
| 6 | 16 000 Hz |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This centered frequency is well above nyquist (when sampling at 22khz). I'd say reduce it to something around 10kHz.

Comment thread usermods/msgeq7/readme.md
| 5 | 6 250 Hz |
| 6 | 16 000 Hz |

The filter outputs are peak-hold envelope-detected, log-compressed to match the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: perform log compression as a separate post-processing step, and work on uncompressed audio samples/filter outputs for all intermediate stages.

Comment thread usermods/msgeq7/readme.md
real chip's output characteristic, and interpolated to the 16-channel
`fftResult[]` array expected by WLED effects.

**Sample rate: 44 100 Hz** (required for the 16 kHz band).

@softhack007 softhack007 Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As only a few people can hear these high frequencies - maybe go back to 22khz, and slightly reduce the highest band to 10kHz. This will reduce cpu load by 50%.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially it was using 22, but when I used opus to grill the implementation for compliance to the MSGEQ7 this was one of the things it then changed, but yeah I agree we don't really want/need that data

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at 22kHz the highest filter will be "cut off", I don't think that is much of an issue though
image

Comment thread usermods/msgeq7/readme.md
| dmType | Microphone type (software backend only) | 1 (Generic I2S) |
| pinSD / pinWS / pinSCK / pinMCLK | I2S pins | unset |
| pinStrobe / pinReset / pinOut | Hardware chip pins | unset |
| gain | Input amplification (128 = unity) | 128 |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means you cannot have more than 2x gain. AR has "unity" around 40, so max "manual gain" is 6x

}
if (_audioSource) {
_audioSource->deinitialize();
delete _audioSource;

@softhack007 softhack007 Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AR audiosource driver were never tested with "delete". Expect crashes here.

_swTaskParams = nullptr;
}
if (_audioSource) {
_audioSource->deinitialize();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deInitialize method was never used in AR, expect strange behaviour.

}

void _stopProcessing() {
// Stop the SW task regardless of the current _useHardwareChip value:

@softhack007 softhack007 Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh oh, this function is so bad I don't really know where to start.

Maybe one thing first: you cannot simply "zero" a task handle - remove it by vTaskDekete() instead. If the task gets deleted while reading from I2S, the I2S driver dies leaving the I2S hardware in a dirty state, and chances are good that it will not re-start without power cycling. Setting audioSource=nullptr while the audio processing task is active has a good chance of causing a nullptr crash.

@softhack007

Copy link
Copy Markdown
Member

Lessons learned today: don't let an AI write code that manages FreeRTOS tasks 😜

@DedeHai

DedeHai commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

@softhack007 16-band demo:

AR-tool-demo

@netmindz

netmindz commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

My personal take on band count is that other than for GEQ, higher band counts as pointless. There aren't 16 different properties you can apply to effects, so you typically end up using say maybe 4 at most and to ensure they work for a variety of different music, you need to then combine the right set of frequencies together as otherwise you miss that aspect of the track.

Every time some has questioned why we have "only" 16 bands, I've asked what they would use more for other than for a GEQ and there has been silence

Once we ditch UMData I would like to actually centralise the simplification of the data, rather than do inside the effect

@netmindz

netmindz commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@DedeHai - if you have an existing implementation of something that behaves exactly like a MSGEQ7, including how it behaves with regards to not missing peaks because you didn't happen to be reading at that exact instance, I'm happy to discard this attempt in favour of yours

@DedeHai

DedeHai commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

I do not know how "exctly" the MSGEQ7 behaves but the code I used to generate the above demo should never miss a peak - I updated it to look at each sample of the filtered output and determine the amplitude by tracking the max value.
If further testing of the 16band filter version shows no disadvantages over FFT we could completely dump FFT in favour of this - it should be similar CPU load with the advantage of having lower latency and not requiring pre-filtering of the signal. Oh and no windowing tables, saving a few kB of RAM.

// --- Run each biquad bandpass filter; update peak-hold envelope ---
for (int b = 0; b < NUM_BANDS; b++) {
// Select the best SIMD variant available at compile time.
#if dsps_biquad_f32_aes3_enabled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this wrapper is already present in esp-dsp ?


static volatile TaskHandle_t s_swTaskHandle = nullptr;

static void IRAM_ATTR softwareProcessingTask(void *pvParams) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iram_attr is not necessary here

delay(1); // feeds IDLE(0) watchdog — do not remove; see audioreactive

if (!p->source || !p->source->isInitialized()) {
vTaskDelay(pdMS_TO_TICKS(1));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

//
// Curve: out = 255 * log10(1 + LOG_SCALE * (env/32768)) / log10(1 + LOG_SCALE)
static constexpr float kFullScale = 32768.0f;
const float kLogDivisor = log10f(1.0f + MSGEQ7_LOG_SCALE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be pre-calculated, instead of re-calculating it in every iteration.


free(inputBuf);
free(filteredBuf);
s_swTaskHandle = nullptr; // signal _stopProcessing() that we have fully exited

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line won't work reliably.

@softhack007

softhack007 commented Jun 8, 2026

Copy link
Copy Markdown
Member

I do not know how "exctly" the MSGEQ7 behaves but the code I used to generate the above demo should never miss a peak - I updated it to look at each sample of the filtered output and determine the amplitude by tracking the max value.
If further testing of the 16band filter version shows no disadvantages over FFT we could completely dump FFT in favour of this - it should be similar CPU load with the advantage of having lower latency and not requiring pre-filtering of the signal. Oh and no windowing tables, saving a few kB of RAM.

@DedeHai we can give it a try - in principle just replace fftCode() with the 16 filter bank, right?

  • RAM saving is a good argument.
  • About runtimes: in theorie, FFT is O(n * log(n)) (n = samples per batch = 512), the 16 filter bank is O(n * 16) ("16" = number of filter channels). So the break-even (where FFT becomes faster than a filterbank) should be around 6-10 channels.
  • did you check if the lower frequency filters have a noticeable delay, compared to the high frequency ones (group delay)?
  • we need to be careful not to "slice down" the size of sample batches too much - I2S has a minimal batch size of 64 samples, and we need to find a good frequency for the filter task -> above 2ms (FreeRTOS sheduler limits), and below <10ms (for best "instant reactions").

@DedeHai

DedeHai commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

@DedeHai we can give it a try - in principle just replace fftCode() with the 16 filter bank, right?

give the test tool a try, it basically has them side-by-side for testing and parameter tuning. Its much easier to do it there and then port all parameters over to AR UM - it should be like a 1 hour task to make a draft with the FFT commented out and the bandpass copied in - it does then require the new code from this PR to find major peak etc. as that is still taken from FFT in my version.

  • About runtimes: in theorie, FFT is O(n * log(n)) (n = samples per batch = 512), the 16 filter bank is O(n * 16) ("16" = number of filter channels). So the break-even (where FFT becomes faster than a filterbank) should be around 6-10 channels.

yes, but it may still be faster as that does not take into account the windowing and that an FFT "operation" probably takes more clock cycles. Anyway, the processing time will be similar, maybe a bit faster, maybe a bit slower but in the same order of 2-3ms, also if we use frames of 128samples, it will be faster anyway but run twice as often.

  • did you check if the lower frequency filters have a noticeable delay, compared to the high frequency ones (group delay)?

I did not but its a good point. I had a feeling the base was slightly late and on fast base beats there was a visible phase shift between sub-base and base but was not sure this was due to other processing. I can ask AI to make a python script that shows all the graphs using scipy and matplotlib, including gain, phase and group delay.

  • we need to be careful not to "slice down" the size of sample batches too much - I2S has a minimal batch size of 64 samples, and we need to find a good frequency for the filter task -> above 2ms (FreeRTOS sheduler limits), and below <10ms (for best "instant reactions").

Just a thought: can the I2S invoke a interrupt / trigger the task to run (or is that already how it is)? I think 128samples would be good, that would make the task run every ~6ms or roughly 150Hz, fast enough to have new AR data even at high frame rates. 192samples still would be good enough or we keep it at the current 256samples.

One other question will be if these filters can run at a reasonable accuracy without using floats (for C3, S2)

@softhack007

softhack007 commented Jun 8, 2026

Copy link
Copy Markdown
Member

One other question will be if these filters can run at a reasonable accuracy without using floats (for C3, S2)

AFAIK - "yes, but". The esp-dsp library only has "f32" biquad filters, so they are running on float even on S2 and C3.
I found a proposal for a fixed-point implementation in the german "make" magazine. They say their fixed-point code ("direct form I") is stable, however you cannot simply transfer the "float" version ("direct form II") to fixed-point due to numerical freak-out. If you want, i can send you a scan by e-mail :-)

@DedeHai

DedeHai commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pointers, I will ask claude about the integer implementation, it is quite knowledgable when it comes to such algorithms but it helps if you point it to potential issues, the "using form I" is probably a good one.

I checked computation speed of current code - the raw FFT (without any pre and post filtering etc.) takes 0.95ms, running the 16band filter takes just barely more than that at 1.05ms, given that the FFT also needs additional processing I'd say the band pass is on par if not faster. Reason is probably that the bandpass runs on 256 samples while the FFT takes 512 samples, (old + new).

regarding group delay, this is a minor issue but not a show stopper, at the current settings, which match FFT quite nicely i.e. Order=2, Q= 3.5 this is the group delay for the band (note that the lowest band is 75Hz, highest ist 10kHz, log distribution):
image

I don't think the 20ms will be noticeable, there is additional latency coming on top of that - worst case its probably more like 50ms given that the data needs to propagate out to the LEDs.

@DedeHai

DedeHai commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@softhack007 I cobbled together an interger version as per your recommendation I instructed claude to use "direct form I" which has more internal parameters and uses a bit more RAM. I am not well educated in digital filtering and its caveats like stability etc. but the result is not too bad. It is less accurate and a bit more "noisy" than the float version but for the purposes of running it fast on a C3 and S2 it may be good enough.
I am sure the details can be improved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants