Adding Real-Time Audio DSP and EQ to ESP32 Projects: Biquad Filters, Crossovers, and the ESP-DSP Library
Once you have I2S audio input and output working on an ESP32 — wiring covered elsewhere on this site with the MAX98357A DAC and INMP441 microphone — the next question is usually "now how do I actually process this signal?" Passing raw PCM samples straight from ADC to DAC gets you a passthrough, but real projects need equalization, crossover filtering for multi-driver speaker builds, bass boost, or voice-band filtering for intercoms and radios. This guide covers the DSP fundamentals and practical implementation using Espressif's ESP-DSP library.
Why This Needs Real DSP, Not Just Gain
A volume knob scales every frequency equally. An equalizer, crossover, or voice filter needs to treat different frequencies differently, which means filtering in either the frequency domain (FFT-based) or, far more commonly for real-time low-latency audio on a microcontroller, the time domain using digital filters applied sample-by-sample. The workhorse for this is the biquad filter — a second-order IIR filter that, with the right coefficients, implements a low-pass, high-pass, band-pass, peaking EQ, or shelf filter using just five multiplies and four additions per sample.
The ESP-DSP Library
Espressif publishes ESP-DSP as an ESP-IDF component (also usable from Arduino via the arduino-esp32 core's IDF component support) with optimized biquad, FFT, FIR, and matrix operations that take advantage of the ESP32's instruction set — the S3 in particular benefits from its vector instructions for FIR and FFT work, while the base ESP32's Xtensa LX6 cores have a hardware floating-point unit that makes float-based biquad math practical without dropping to fixed-point.
- dsps_biquad_f32: Applies a single biquad stage to a block of float samples — this is the core building block for every filter type below.
- dsps_biquad_gen_lpf_f32 / hpf / bpf / peakingEQ / lowShelf / highShelf: Coefficient generator functions that turn a cutoff frequency, sample rate, and Q (or gain, for peaking/shelf types) into the five biquad coefficients, so you don't need to derive the Audio EQ Cookbook math by hand.
- dsps_fft2r_fc32: A radix-2 FFT, useful for spectrum analyzers and VU meters (like the ESP32 FFT/LED-bar project covered elsewhere on this site) rather than real-time filtering, since FFT-based filtering adds latency that biquad chains avoid.
Building an EQ or Crossover from Biquad Stages
A single biquad gives you one filter band. Real EQs and crossovers chain several biquads in series, each sample passing through stage after stage before reaching the output buffer.
ApplicationFilter ChainTypical Parameters 3-band graphic EQLow shelf → peaking (mid) → high shelfLow shelf ~150Hz, mid peak ~1kHz with adjustable Q, high shelf ~6kHz 2-way speaker crossoverLow-pass (woofer path) + high-pass (tweeter path), run as two parallel chains on the same inputLinkwitz-Riley alignment, crossover point typically 2-3.5kHz depending on driver specs Voice-band filter for intercom/radioHigh-pass ~300Hz → low-pass ~3.4kHzMatches traditional telephone voice bandwidth, cuts rumble and hiss outside the speech range Bass boostLow shelf with positive gainShelf frequency ~100-120Hz, gain +6 to +9dB depending on driver and enclosureTiming Budget and Core Assignment
This is the part that trips people up coming from a PC audio background: the ESP32 has a hard real-time budget. At a 44.1kHz sample rate with a 128-sample block, you have roughly 2.9 milliseconds to fully process each block before the I2S peripheral needs the next one, or you'll get underruns and audible glitches. On dual-core parts (original ESP32, S3), the standard pattern is to pin the DSP processing task to Core 1 with FreeRTOS task affinity, leaving Core 0 free for WiFi/BLE stack housekeeping, which otherwise causes intermittent audio glitches when radio activity spikes. Single-core parts (C3) can still do modest biquad chains but have much less headroom — keep the filter chain short (3-4 stages) and the block size larger to reduce per-block overhead.
Practical Implementation Pattern
The typical structure looks like this: an I2S read task pulls a block of samples into a buffer, hands it off via a FreeRTOS queue to the DSP task, which runs the sample block through each active biquad stage in the chain using dsps_biquad_f32, then pushes the processed block to a second queue feeding the I2S write task. Keeping read, process, and write as separate tasks connected by queues (rather than one monolithic function) makes it much easier to profile where time is actually going when you're tuning for latency, and lets you swap filter chains at runtime — useful if you want a user-adjustable EQ rather than a fixed one baked into firmware.
Common Pitfalls
- Clipping after gain stages: A peaking EQ boost or bass shelf can push samples outside the -1.0 to 1.0 float range (or outside 16/24-bit integer range if you're working in fixed point). Add a soft limiter or simply reduce overall gain headroom before the final stage to avoid harsh digital clipping.
- Coefficient recalculation causing clicks: If you let a user adjust EQ sliders in real time, recalculating biquad coefficients on the fly and swapping them mid-stream can cause audible clicks or pops. Crossfading between old and new coefficients over a few samples, or only updating at a zero-crossing, smooths this out.
- Ignoring the DC blocker: Cheap I2S microphones and some ADC front ends have a small DC offset. A simple high-pass biquad around 20-40Hz as the first stage in any chain removes this before it accumulates through downstream gain stages.
- Fixed-point overflow if you drop to Q15/Q31 fixed-point math for extra speed on cores without a strong FPU — the ESP-DSP fixed-point biquad functions exist for this, but intermediate accumulator overflow is a real risk with aggressive gain settings and needs headroom built into the coefficient scaling.
Once a working biquad chain is in place, extending it is mostly a matter of adding stages and exposing their parameters — a proper parametric EQ, a multi-band crossover for a 3-way speaker build, or a voice enhancer for a walkie-talkie project are all the same underlying technique with different coefficient choices.