PoC - MSGEQ7 based AudioReactive#5673
Conversation
…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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
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.
| float decayC = p->decayCoeff; | ||
| float env = envelope[b]; | ||
| for (int i = 0; i < MSGEQ7_BLOCK_SIZE; i++) { | ||
| float absVal = fabsf(filteredBuf[i]); |
There was a problem hiding this comment.
you may get better results by sing RMS instead of simple abs(), I saw less "jitter" in my tests when doing so
|
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. 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.
|
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.
|
🤔 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:
|
| float decayC = timeConstToCoeff(_decayMs * 0.001f, (float)MSGEQ7_SAMPLE_RATE); | ||
| float linearGain = _gainPercent / 128.0f; | ||
|
|
||
| _swTaskParams = new SWTaskParams{ |
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
If you want to do this properly, don't rely on uint8_t... in hindsight using 8bit for squelch/gain was one of the most stupid mistakes we made in AR.
| uint32_t _lastLoopMs = 0; | ||
|
|
||
| // PROGMEM key for config JSON | ||
| static const char _name[]; |
There was a problem hiding this comment.
This is effectively the same as nullptr.
| | 3 | 1 000 Hz | | ||
| | 4 | 2 500 Hz | | ||
| | 5 | 6 250 Hz | | ||
| | 6 | 16 000 Hz | |
There was a problem hiding this comment.
This centered frequency is well above nyquist (when sampling at 22khz). I'd say reduce it to something around 10kHz.
| | 5 | 6 250 Hz | | ||
| | 6 | 16 000 Hz | | ||
|
|
||
| The filter outputs are peak-hold envelope-detected, log-compressed to match the |
There was a problem hiding this comment.
suggestion: perform log compression as a separate post-processing step, and work on uncompressed audio samples/filter outputs for all intermediate stages.
| 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). |
There was a problem hiding this comment.
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%.
| | 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 | |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
AR audiosource driver were never tested with "delete". Expect crashes here.
| _swTaskParams = nullptr; | ||
| } | ||
| if (_audioSource) { | ||
| _audioSource->deinitialize(); |
There was a problem hiding this comment.
The deInitialize method was never used in AR, expect strange behaviour.
| } | ||
|
|
||
| void _stopProcessing() { | ||
| // Stop the SW task regardless of the current _useHardwareChip value: |
There was a problem hiding this comment.
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.
|
Lessons learned today: don't let an AI write code that manages FreeRTOS tasks 😜 |
|
@softhack007 16-band demo:
|

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
msgeq7usermod, 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]usermods/msgeq7/readme.md)Validation tooling
sweep_analyze.pyto 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)