← How-Tos
electronics 55 min ago ◯ 5 min read

Adding Real-Time Audio DSP and EQ to ESP32 Projects: Biquad Filters, Crossovers, and the ESP-DSP Library

esp32audiodspeqbiquadesp-dspi2scrossoversignal processing

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.

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 enclosure

Timing 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

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.