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
Copy link
Copy Markdown
Member

@netmindz netmindz commented Jun 7, 2026

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

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
Copy link
Copy Markdown
Member Author

netmindz commented Jun 7, 2026

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
Copy link
Copy Markdown
Collaborator

DedeHai commented Jun 7, 2026

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
Copy link
Copy Markdown
Member Author

netmindz commented Jun 7, 2026

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
Copy link
Copy Markdown
Member

softhack007 commented Jun 7, 2026

🤔 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
Copy link
Copy Markdown
Collaborator

DedeHai commented Jun 7, 2026

  • 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
Copy link
Copy Markdown
Member

@softhack007 softhack007 Jun 7, 2026

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 one of the most stupid mistakes we made 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).
Copy link
Copy Markdown
Member

@softhack007 softhack007 Jun 7, 2026

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%.

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;
Copy link
Copy Markdown
Member

@softhack007 softhack007 Jun 7, 2026

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:
Copy link
Copy Markdown
Member

@softhack007 softhack007 Jun 7, 2026

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
Copy link
Copy Markdown
Collaborator

DedeHai commented Jun 7, 2026

@softhack007 16-band demo:

AR-tool-demo

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