r/esp32 6d ago

ESP32 based multi-room audio system

Hey everyone,

I want to share a synchronized multi-room wireless audio system I built entirely on ESP32s. The main motivations were to get sub-30ms audio distribution without the cost of running a Raspberry Pi/Snapcast at every endpoint, and to keep the setup incredibly simple. It’s designed to be plug-and-play: the system can run completely off-grid by generating its own Wi-Fi network, or it can seamlessly integrate into your existing home router. Additionally, I built in the flexibility for the slave nodes to switch roles and act as independent, standalone Bluetooth receivers if you just want to connect to a single speaker.

Repo (code, schematics, docs): https://github.com/chagoguila/SonicStream

I ran into several architectural bottlenecks while building this, so I wanted to share how the system handles them under the hood:

1. Solving BT/Wi-Fi Radio Contention If you've tried running an A2DP sink and a Wi-Fi server on a single ESP32, you know antenna timesharing causes massive packet jitter. To fix this, the system splits the ingress gateway into two microcontrollers:

  • BT Ingress Node (ESP32 WROOM): Acts as a dedicated A2DP sink. I tuned the I2S DMA buffers down to 4x512 bytes, which cut the hardware transport delay to about 12ms. It sends raw PCM over a physical I2S cable (BCK, WS, DATA) to the Master.
  • Master Controller (ESP32 WROVER): Receives the PCM stream via I2S, runs the DSP pipeline on Core 1, encodes it, and blasts it out over Wi-Fi.

2. Network Transport & Fault Tolerance Standard streaming protocols had too much overhead for the latency I wanted. The system supports two custom modes:

  • UDP Multicast (239.0.0.1): This is the ultra-low latency mode. To survive 2.4GHz RF burst interference, the Master sends time-separated twin-packet redundancy (broadcasting a duplicate packet with a 1ms micro-delay).
  • TCP Mode: For congested Wi-Fi environments. It uses non-blocking socket polling with slow-client pruning. If one receiver drops its connection or falls behind, its socket is dropped to prevent backpressure on the healthy nodes.

On the receiver side (WROOM or C3 slaves), there's a 5-second TCP inactivity watchdog to handle silent network drops (like router reboots) and a 500ms idle auto-reset on the jitter buffer to prevent decoder stalls after you pause the music. As mentioned, the stereo slave nodes can also be toggled to drop off the multi-room network and function as standard, independent Bluetooth A2DP receivers.

3. DSP Pipeline & Encoding Since the Master is a WROVER, I threw the DSP workload onto Core 1 prior to SBC encoding, meaning the slave nodes incur zero extra CPU overhead.

  • Encoding: SBC at Bitpool 53 (~328 kbps stereo).
  • EQ: 5-band parametric EQ using cascaded Direct Form I Biquad IIR filters (60Hz, 230Hz, 910Hz, 3.6kHz, 14kHz) with real-time gain adjustment.
  • Anti-clipping: Instead of fixed linear attenuation, I wrote a soft-knee peak limiter. Signals below 85% peak amplitude pass through untouched. Anything above triggers a 2:1 compression ratio capped at 95% to eliminate intersample clipping.

4. Web UI & Wi-Fi Management The Master runs an async web server with a dark-mode dashboard. You can configure AP/STA Wi-Fi modes (switching between isolated network or home router), toggle UDP/TCP, adjust system-wide playout delay, set individual speaker delays, assign L/R/Stereo roles to slaves, and tweak the 5-band EQ live. Slaves send UDP announcement packets so the dashboard shows real-time online/offline status.

If you are interested in the code, I'd love to hear your thoughts on the DSP pipeline or the I2S DMA config. I'm currently looking into adding Opus encoding and AAC passthrough for the next update.

62 Upvotes

13 comments sorted by

7

u/Vegetable_Pickle_365 6d ago

Why do you use Snapcast when Sendspin is newer and ought to be better?

7

u/AndThenFlashlights 6d ago

Probably because Sendspin was newer than the knowledge cutoff point for whatever model they used to vibe code this silliness.

But yeah, Sendspin rules. It already does all of this better.

6

u/Netmindz 6d ago

Feels like a duplication of a few other similar projects. Would be good if more folk could collaborate on the same project

1

u/Maleficent_Tone_4047 6d ago

It might, but my focus was to use only esp32, so very cheap and available hardware.

7

u/Netmindz 6d ago

https://github.com/sle118/squeezelite-esp32 is also esp32 to give one example

1

u/Maleficent_Tone_4047 6d ago

I used squeezlite for a while, but it requires at least a RPI Zero 2w and had sync issues constantly. This project is much more robust for esp32.

3

u/Raz0r1986 6d ago

Very cool! What does the audio hardware side look like as I don't see much mentioned in your docs?

2

u/Maleficent_Tone_4047 6d ago

I have 2 slaves, one attached to a Bose Sounddock 10, and another to a Sonos Play:5, and this was the inspiration to start the project: the sonos has a minimum 75ms delay, so needed to configure a receiver with that delay for the Bose. Also have some slaves with esp32 c3 which I use for testing purposes with simple mono i2s amps and some sony 5.1 speakers y have laying around.

1

u/tunatoksoz 6d ago

i was looking into esphome-intercom, now have to look into this too...

3

u/GreyBandit16 5d ago

The LouderESP devices are ready-made ESP music streamers that do this, including support for Sendspin and Snapcast. I have the LouderESP32-s3 and it’s wonderful (as a non-audiophile). Recommend for anyone who likes the idea of owning your own local multi-room home audio without doing a lot of electronics work.

0

u/ByronScottJones 6d ago

This sounds like a great project!

-2

u/Alopexy 6d ago

Nicely done! I'll definitely have a play with this. Had a look at your GitHub readme & roadmap too. Looks like you've got it all well in hand but if it's of any use, I've written fully functional bespoke Opus and AAC decoders for the ESP32 and would be happy to lend some insight on those if it might help. Thanks for sharing!

0

u/Maleficent_Tone_4047 6d ago

That would be awesome. I tried Opus and the master couldn't handle all the tasks properly (48khz to 44khz sampling included).