DQ-DSP: A $20 DSP Brain for My DIY 2-Way Active Speakers 🔊
May 2, 2026 · the one where i replace a $200 miniDSP with a $5 ESP32
> tl;dr: built a USB Audio → DSP → dual-I2S box on an esp32.
> drives a 2-way active speaker. $20 BOM. open-source. 😎
DQ = my daughter's name. Yes, I named a DSP after her. Don't @ me. 🍼
The build 📸
A 2-way bookshelf I cut from MDF with a friend's router, drivers from Parts Express, the DSP wired up on a breadboard while I figured out the firmware. The receipts, before the words:





The itch — and the things I tried first 🤔🥲
Active speakers were the destination from day one. Passive crossovers — those chunks of inductors and caps inside the cab — have tradeoffs you can't fix from outside the box: phase shift, insertion loss, no per-driver EQ, no time alignment. They're also expensiveeee in Vietnam — decent air-core inductors and audiophile-grade film caps are painful to source here, and the ones that do show up usually cost more than the actual drivers. So: active bi-amp, with the crossover done in DSP. The DSP was just the question of how.
First stop: a Wondom 2-in / 4-out board with the Analog Devices ADAU1701 SigmaDSP chip. You program it with SigmaStudio — ADI's Windows-only graphical tool whose UI hasn't had a serious refresh since roughly the iPhone 4 era. It worked. The sound was… fine. But it never quite landed where I was hearing it in my head, and tweaking anything meant booting Windows + clicking through that ancient block-diagram editor.

Plan B was the miniDSP 2x4 HD — the de-facto gold standard for hobby active crossovers. Plot twist: the regular miniDSP 2x4 runs the exact same ADAU1701 I already had on the Wondom, just in a nicer enclosure with a USB UAC bridge bolted to the front. The HD version moves up to a SHARC DSP — i.e. fancier = more money I didn't have :)) Either way, getting one into Vietnam meant ~USD 200 for the box plus ~USD 60+ in international shipping, or roping a friend abroad into smuggling one back in their carry-on. Neither happened.
Plan C: roll my own. The ESP32-S3 on my desk was a leftover from a different abandoned project. Two PCM5102A breakouts cost less than my morning coffee. The rest of this post is what came out of that.
The parts I bought 🎤💸
For about $15 in chips and a couple of $30 drivers, the whole rig fits on one corner of my desk:
The drivers — cho ai quan tâm:
- Tweeter ·
Dayton Audio NHP25Ti-4— 1" titanium dome, 4Ω. ~$28. Smooth top end, plays nicely from ~2 kHz up. - Mid/Bass ·
Dayton Audio TCP115-4— 5" paper-cone polypropylene-coated, 4Ω. ~$38. Tight bottom, easy to crossover ~2 kHz.
Dayton drivers are cheap and well-measured — Parts Express publishes proper FRD/ZMA files so you can simulate the crossover before cutting wood.
And on the DSP side — the actual $20:
- ESP32-S3-DevKitC-1 N16R8 — 16 MB flash + 8 MB Octal PSRAM. ~$8 on AliExpress. Two USB-C ports, one for audio, one for control.
- 2× GY-PCM5102A breakout — purple PCB, I²S DAC with 3.5 mm jack. ~$2 each. Surprisingly clean output for the price.
- Dupont jumper wires — 8 minimum. You probably already have a bag.
- 2× USB-C data cables — one for UAC audio, one for Web Serial control. Both must be data-capable.
- 1× 3.5mm pigtail per DAC — line-level out into your power amps.


Total: ~$15–20 in chips. ESP-IDF v5.2 is the only software dependency.
The web UI 🖥
Open Chrome, click Connect Serial, pick the DSP's USB port — and every parameter running on the chip becomes a live knob in your browser tab. Drag a PEQ band, the change lands on the DAC output in a few milliseconds. Pull the crossover frequency from 2 kHz down to 1.8 kHz, hear the woofer take more of the band. Drop a REW measurement file in and the room-correction filters get auto-fitted to the target curve you picked (flat, Harman, custom tilt). Group the L and R outputs so tweaking one mirrors the other. There's a 2 × 4 routing matrix if you want to do something weirder than stereo. The CPU-load and buffer-fill telemetry charts are right there too, so you can see if your filter chain is about to overrun.

When the preset finally sounds the way it should, hit Save to Flash — the box remembers and keeps playing from any USB host forever, no laptop needed. No installer, no driver hunt, no firmware recompile. Everything happens in a browser tab.
Live at dq-dsp.tamduongs.com. Chrome / Edge / Brave / Opera desktop only — Web Serial is still browser-locked.
just go play with it 👇
Every knob, every slider, every cryptic acronym has a hover tooltip explaining what it does. The UI is built to teach itself — open it, hover anything you don't recognise, and you'll know more about active DSP crossovers in 10 minutes than I knew in 3 weekends.
Stack, for the curious: React 19 + TypeScript + Tailwind 4 + Zustand for state, Web Serial as the only transport. SLIP-framed CRC-8 packets on UART0 over the DevKit's USB-Serial-JTAG port. Source on GitHub, deployed on Vercel. Plus 6 weekends of my life, an unreasonable amount of coffee, and CLAUDEEEE. ☕🤖
Under the hood (& a love letter to the ESP32-S3) 💚
ESP-IDF v5.2, pure C, FreeRTOS underneath. Core 0 handles USB and the control plane; Core 1 runs the entire DSP pipeline. Code splits cleanly into two parts: `main/` is the ESP-IDF component (USB, I2S, NVS, serial transport), and `shared/dsp/` is a pure-C DSP core — no Espressif headers in there at all, so it compiles and unit-tests on a laptop without an MCU in sight.
Why the ESP32-S3 is, frankly, an absurd deal at $5:
- Dual-core 240 MHz Xtensa LX7 — one core shovels samples through the biquad chain at ~3% load; the other answers USB and the web-UI faster than any human can drag a slider.
- 8 MB Octal PSRAM — headroom for the USB ring buffer, ASRC scratch space, and parameter shadow-buffers without ever sweating internal SRAM.
- 16 MB flash — firmware, preset slots, and room to grow. Nothing fancy, just nice.
- Two USB peripherals on one chip — native USB-OTG (handles the UAC audio stream) and USB-Serial-JTAG (handles the Web Serial control channel) running simultaneously on two physical USB-C ports of the DevKit. No external USB-to-anything chip required.
- Two independent I²S TX — I²S0 and I²S1, each with their own DMA, both lockable to a single APLL so the two PCM5102A DACs stay sample-accurate forever.
- TinyUSB upstream — UAC 1.0 + CDC over the same chip is roughly a hundred lines of glue. The UAC stream descriptor, the volume/mute controls, the CDC parameter channel — all already there as components.
- ESP-IDF that actually works — `idf.py build flash monitor` on a fresh install builds and boots. No 6-hour vendor-IDE odyssey. Documentation that's correct. A debugger that lives on the same USB-C as the flasher.
The signal chain
Stereo USB audio in → input EQ → 2×4 routing → per-output crossover → output EQ → delay + gain → 2× I²S → 2× PCM5102A → 4 line-outs. All of it lives on Core 1.
Honestly, the chip itself is the most romantic part. Two USB peripherals, two I²S TX, 8 MB PSRAM, 240 MHz dual-core, hardware crypto, hardware floating-point, and the whole thing costs less than two iced coffees. Espressif quietly made the entire "cheap, capable, hackable embedded audio" category make sense, and ESP-IDF is the only mainstream MCU toolchain I've used where the first compile of a fresh project just… works. 💚
Want to flash this on your own ESP32-S3? Two paths:
⚡ Prebuilt binary (v1.0.0) ↗
one .bin · `esptool write_flash 0x0` · ~30 seconds
🛠 Build from source ↗
ESP-IDF v5.2 · `idf.py build flash monitor`
DSP specifications 📋
Everything that runs in real time on Core 1, fed from a USB Audio Class 1.0 stream:
| Block | Specification |
|---|---|
| USB input | UAC 1.0 · 16-bit / 48 kHz · stereocapped by the USB-driver class spec — UAC 1.0 maxes at 16-bit/48 kHz without USB 2.0 high-speed |
| DAC output | Dual I²S · 16-bit / 48 kHz · 4 line-outs (2 × PCM5102A in this build)output is plain I²S — swap the PCM5102A for any I²S DAC you like (ES9038, AK4493, PCM1808, whatever). Pipeline tracks the USB input bit-depth |
| ASRC | PI controller · ±1400 ppm · per-DAC drift correctionKp / Ki are tunable live in the System tab of the web UI |
| Input PEQ | 10 bands · 2 channels · biquad cascade · ±18 dB |
| Room EQ | 10 bands · 2 channels · per-input correction · REW import |
| Routing matrix | 2 × 4 · per-cell linear gain · L/R link groups |
| Output PEQ | 10 bands · 4 channels · biquad cascade · ±18 dB |
| Crossover | HP + LP per output · Linkwitz–Riley / Butterworth · 6 / 12 / 18 / 24 dB/oct |
| Per-channel trim | Gain · phase invert · mute · delay 0–10 ms |
| CPU load | ≈ 3 % of one Core 1 @ 48 kHz · pure C, no FPU acrobatics |
| Storage | 1 NVS preset · save-to-flash · persists across power cycles |
| Control plane | Web Serial · SLIP + CRC-8 frames · UART0 over USB-Serial-JTAG |
| Connectivity | 2 × USB-C · USB-OTG (audio) + USB-Serial-JTAG (control) |
All numbers are from the running v1.0.0 firmware. Source of truth: shared/dsp/dsp_config.h and the protocol header in shared/dsp/serial_protocol.h.
Hooking it up 🔌
Six signal wires + shared 3V3 + GND between the ESP32 and the two DACs, then two USB-C cables out — one for the audio stream, one for the control channel. That's the whole physical install.
What I learned 🧠
- TinyUSB UAC + CDC together is fiddly — you have to share endpoint memory between UAC and the control channel. The example projects don't cover this case.
- Dual-I2S clock alignment matters — if the two I2S TX peripherals drift, the L/R image shifts over time. ESP-IDF lets you sync them with a single APLL — use it.
- ASRC PI tuning is a vibe check — the PCM5102A wants its own clock, USB host wants its clock, neither is the other's clock. PI controller in software keeps the buffers from over/underflowing. ±1400 ppm is comfortable.
- Naming things is hard — DQ-DSP is the third name. The first two were objectively worse.
Achievements unlocked 🏆
FANCY UI
live web app · zero installer · runs in your browser tab
OPEN SOURCE
both repos public · GPLv3 · forks welcome
SOUNDS DECENT FOR $20
two amps + DSP + DAC for the price of a takeout meal
TOO MUCH COFFEE — WORTH IT
6 weekends · n cups of cà phê sữa đá · 0 regrets
GPLv3. Forks must stay open. Stay chill. ✌️